Refactor: Remove NFStream and flow identity utilities; introduce Tshark manager
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 11s

- Deleted flow_identity.py and nfstream_flow_worker.py as they are no longer needed.
- Removed nfstream_manager.py and its associated logic for managing NFStream workers.
- Added tshark_manager.py to manage tshark packet enrichment and matching.
- Updated setup_build_server.sh to include default environment variables for tshark.
- Implemented packet signature generation and enrichment logic in the new TsharkManager class.
This commit is contained in:
2026-03-07 19:51:29 +01:00
parent dfad09fa21
commit 22bb6b8526
11 changed files with 603 additions and 695 deletions

View File

@@ -3,7 +3,7 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential libpcap-dev pkg-config \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential libpcap-dev pkg-config tshark \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY requirements.txt . COPY requirements.txt .

View File

@@ -12,7 +12,6 @@ pydantic==2.12.3
pydantic_core==2.41.4 pydantic_core==2.41.4
pyroute2==0.9.5 pyroute2==0.9.5
python-multipart==0.0.22 python-multipart==0.0.22
nfstream==6.5.4
scapy==2.6.1 scapy==2.6.1
sniffio==1.3.1 sniffio==1.3.1
starlette==0.48.0 starlette==0.48.0

View File

@@ -54,17 +54,12 @@ class BackendSettings:
bridge_bpf_build_dir: str bridge_bpf_build_dir: str
telemetry_process_stop_timeout_seconds: float telemetry_process_stop_timeout_seconds: float
telemetry_reader_join_timeout_seconds: float telemetry_reader_join_timeout_seconds: float
nfstream_enabled: bool tshark_enabled: bool
nfstream_promiscuous_mode: bool tshark_display_filter: str
nfstream_idle_timeout_seconds: int tshark_cache_ttl_seconds: float
nfstream_active_timeout_seconds: int tshark_match_window_ms: int
nfstream_snapshot_length: int tshark_reader_join_timeout_seconds: float
nfstream_n_dissections: int tshark_process_stop_timeout_seconds: float
nfstream_n_meters: int
nfstream_cache_ttl_seconds: float
nfstream_lookup_window_ms: int
nfstream_reader_join_timeout_seconds: float
nfstream_process_stop_timeout_seconds: float
def load_settings() -> BackendSettings: def load_settings() -> BackendSettings:
@@ -92,17 +87,12 @@ def load_settings() -> BackendSettings:
bridge_bpf_build_dir=_env_str("BACKEND_BRIDGE_BPF_BUILD_DIR", "/tmp/mitm-bpf"), bridge_bpf_build_dir=_env_str("BACKEND_BRIDGE_BPF_BUILD_DIR", "/tmp/mitm-bpf"),
telemetry_process_stop_timeout_seconds=_env_float("BACKEND_TELEMETRY_PROCESS_STOP_TIMEOUT_SECONDS", 3.0), telemetry_process_stop_timeout_seconds=_env_float("BACKEND_TELEMETRY_PROCESS_STOP_TIMEOUT_SECONDS", 3.0),
telemetry_reader_join_timeout_seconds=_env_float("BACKEND_TELEMETRY_READER_JOIN_TIMEOUT_SECONDS", 2.0), telemetry_reader_join_timeout_seconds=_env_float("BACKEND_TELEMETRY_READER_JOIN_TIMEOUT_SECONDS", 2.0),
nfstream_enabled=_env_bool("BACKEND_NFSTREAM_ENABLED", True), tshark_enabled=_env_bool("BACKEND_TSHARK_ENABLED", True),
nfstream_promiscuous_mode=_env_bool("BACKEND_NFSTREAM_PROMISCUOUS_MODE", True), tshark_display_filter=_env_str("BACKEND_TSHARK_DISPLAY_FILTER", "http or tls or dns"),
nfstream_idle_timeout_seconds=_env_int("BACKEND_NFSTREAM_IDLE_TIMEOUT_SECONDS", 120), tshark_cache_ttl_seconds=_env_float("BACKEND_TSHARK_CACHE_TTL_SECONDS", 5.0),
nfstream_active_timeout_seconds=_env_int("BACKEND_NFSTREAM_ACTIVE_TIMEOUT_SECONDS", 1800), tshark_match_window_ms=_env_int("BACKEND_TSHARK_MATCH_WINDOW_MS", 1_500),
nfstream_snapshot_length=_env_int("BACKEND_NFSTREAM_SNAPSHOT_LENGTH", 1536), tshark_reader_join_timeout_seconds=_env_float("BACKEND_TSHARK_READER_JOIN_TIMEOUT_SECONDS", 2.0),
nfstream_n_dissections=_env_int("BACKEND_NFSTREAM_N_DISSECTIONS", 20), tshark_process_stop_timeout_seconds=_env_float("BACKEND_TSHARK_PROCESS_STOP_TIMEOUT_SECONDS", 3.0),
nfstream_n_meters=_env_int("BACKEND_NFSTREAM_N_METERS", 1),
nfstream_cache_ttl_seconds=_env_float("BACKEND_NFSTREAM_CACHE_TTL_SECONDS", 10.0),
nfstream_lookup_window_ms=_env_int("BACKEND_NFSTREAM_LOOKUP_WINDOW_MS", 5_000),
nfstream_reader_join_timeout_seconds=_env_float("BACKEND_NFSTREAM_READER_JOIN_TIMEOUT_SECONDS", 2.0),
nfstream_process_stop_timeout_seconds=_env_float("BACKEND_NFSTREAM_PROCESS_STOP_TIMEOUT_SECONDS", 3.0),
) )

View File

@@ -95,11 +95,11 @@ async def shutdown_event() -> None:
logging.exception("Failed to stop bridge telemetry collector") logging.exception("Failed to stop bridge telemetry collector")
try: try:
from src.utilities.nfstream_manager import nfstream_manager from src.utilities.tshark_manager import tshark_manager
nfstream_manager.stop() tshark_manager.stop()
except Exception: except Exception:
logging.exception("Failed to stop NFStream workers") logging.exception("Failed to stop tshark workers")
try: try:
from src.utilities.packet_tracker import packet_tracker from src.utilities.packet_tracker import packet_tracker

View File

@@ -33,9 +33,9 @@ from src.utilities.interface_bridge_helpers import (
) )
from src.config import settings from src.config import settings
from src.utilities.bridge_telemetry import bridge_telemetry_manager from src.utilities.bridge_telemetry import bridge_telemetry_manager
from src.utilities.nfstream_manager import nfstream_manager
from src.utilities.packet_identity import build_packet_uid from src.utilities.packet_identity import build_packet_uid
from src.utilities.packet_tracker import packet_tracker from src.utilities.packet_tracker import packet_tracker
from src.utilities.tshark_manager import tshark_manager
from src.Models.etherType import EtherTypeEnum, ethertype_from_int from src.Models.etherType import EtherTypeEnum, ethertype_from_int
from src.Models.ip_protocol import IPProtocolEnum, protocol_from_number from src.Models.ip_protocol import IPProtocolEnum, protocol_from_number
@@ -371,13 +371,13 @@ def parse_packet(pkt, bridge_label: str, capture_metadata: Optional[Dict[str, An
if Raw in pkt and not pkt_info.get("protocol_name"): if Raw in pkt and not pkt_info.get("protocol_name"):
pkt_info["protocol_name"] = "RAW" pkt_info["protocol_name"] = "RAW"
# Flow-level enrichment using NFStream, if available. # Packet-level enrichment using tshark, if available.
try: try:
flow_info = nfstream_manager.lookup_packet(pkt, pkt_iface) packet_info = tshark_manager.lookup_packet(pkt, pkt_iface)
if flow_info: if packet_info:
_merge_enrichment(pkt_info, flow_info) _merge_enrichment(pkt_info, packet_info)
except Exception: except Exception:
logger.exception("NFStream enrichment failed") logger.exception("tshark enrichment failed")
if ICMP in pkt: if ICMP in pkt:
inner = pkt[ICMP].payload inner = pkt[ICMP].payload
@@ -536,9 +536,9 @@ def _sync_bridge_telemetry() -> None:
} }
) )
try: try:
nfstream_manager.update_interfaces(active_enrichment_ifaces) tshark_manager.update_interfaces(active_enrichment_ifaces)
except Exception: except Exception:
logger.exception("Failed to update NFStream enrichment workers") logger.exception("Failed to update tshark enrichment workers")
# ------------------------- # -------------------------
@@ -839,6 +839,6 @@ def get_internal_debug_state() -> dict:
"buffer_len": len(_PACKET_BUFFER), "buffer_len": len(_PACKET_BUFFER),
"bridge_capture_mode": "tc_ingress_raw" if any(s.get("is_bridge") for s in sessions.values()) else "af_packet", "bridge_capture_mode": "tc_ingress_raw" if any(s.get("is_bridge") for s in sessions.values()) else "af_packet",
"telemetry_ports": sorted({iface for session in sessions.values() for iface in session.get("ports", [])}), "telemetry_ports": sorted({iface for session in sessions.values() for iface in session.get("ports", [])}),
"nfstream": nfstream_manager.get_debug_snapshot(), "tshark": tshark_manager.get_debug_snapshot(),
"packet_tracker": packet_tracker.get_debug_snapshot(), "packet_tracker": packet_tracker.get_debug_snapshot(),
} }

View File

@@ -267,7 +267,7 @@ class DatabasePool:
except Exception: except Exception:
logger.exception("Failed to publish pkt_info to broadcaster") logger.exception("Failed to publish pkt_info to broadcaster")
async def backfill_flow_metadata( async def backfill_packet_metadata(
self, self,
*, *,
iface: str, iface: str,
@@ -276,17 +276,17 @@ class DatabasePool:
src_port: int, src_port: int,
dst_port: int, dst_port: int,
protocol: int, protocol: int,
first_seen_ms: int, length: int,
last_seen_ms: int, observed_at_ms: int,
enrichment: Dict[str, Any], enrichment: Dict[str, Any],
window_ms: int, window_ms: int,
) -> int: ) -> int:
"""Update recent packet rows for a flow after enrichment arrives asynchronously.""" """Update recent packet rows after tshark metadata arrives asynchronously."""
if self._pool is None: if self._pool is None:
await self.init_pool() await self.init_pool()
lower_bound = datetime.fromtimestamp(max(first_seen_ms - window_ms, 0) / 1000.0, tz=timezone.utc) lower_bound = datetime.fromtimestamp(max(observed_at_ms - window_ms, 0) / 1000.0, tz=timezone.utc)
upper_bound = datetime.fromtimestamp(max(last_seen_ms + window_ms, 0) / 1000.0, tz=timezone.utc) upper_bound = datetime.fromtimestamp(max(observed_at_ms + window_ms, 0) / 1000.0, tz=timezone.utc)
dpi_metadata = enrichment.get("dpi_metadata") dpi_metadata = enrichment.get("dpi_metadata")
try: try:
@@ -303,19 +303,19 @@ class DatabasePool:
app_hostname = COALESCE(packets.app_hostname, $13), app_hostname = COALESCE(packets.app_hostname, $13),
app_is_encrypted = COALESCE(packets.app_is_encrypted, $14), app_is_encrypted = COALESCE(packets.app_is_encrypted, $14),
dpi_metadata = CASE dpi_metadata = CASE
WHEN $15::jsonb IS NULL THEN packets.dpi_metadata WHEN $16::jsonb IS NULL THEN packets.dpi_metadata
WHEN packets.dpi_metadata IS NULL THEN $15::jsonb WHEN packets.dpi_metadata IS NULL THEN $16::jsonb
ELSE packets.dpi_metadata || $15::jsonb ELSE packets.dpi_metadata || $16::jsonb
END END
WHERE WHERE
ip_proto_raw = $1 ip_proto_raw = $1
AND (capture_iface = $2 OR ingress_if = $2 OR egress_if = $2) AND (capture_iface = $2 OR ingress_if = $2 OR egress_if = $2)
AND timestamp BETWEEN $7 AND $8 AND src_ip = $3::inet
AND ( AND dst_ip = $4::inet
(src_ip = $3::inet AND dst_ip = $4::inet AND src_port = $5 AND dst_port = $6) AND src_port = $5
OR AND dst_port = $6
(src_ip = $4::inet AND dst_ip = $3::inet AND src_port = $6 AND dst_port = $5) AND length = $7
) AND timestamp BETWEEN $8 AND $9
AND ( AND (
packets.app_protocol IS NULL packets.app_protocol IS NULL
OR packets.app_master_protocol IS NULL OR packets.app_master_protocol IS NULL
@@ -323,7 +323,7 @@ class DatabasePool:
OR packets.app_confidence IS NULL OR packets.app_confidence IS NULL
OR packets.app_hostname IS NULL OR packets.app_hostname IS NULL
OR packets.app_is_encrypted IS NULL OR packets.app_is_encrypted IS NULL
OR ($15::jsonb IS NOT NULL) OR ($16::jsonb IS NOT NULL)
) )
RETURNING * RETURNING *
""", """,
@@ -333,6 +333,7 @@ class DatabasePool:
dst_ip, dst_ip,
src_port, src_port,
dst_port, dst_port,
length,
lower_bound, lower_bound,
upper_bound, upper_bound,
enrichment.get("app_protocol"), enrichment.get("app_protocol"),
@@ -344,7 +345,7 @@ class DatabasePool:
json.dumps(dpi_metadata) if dpi_metadata is not None else None, json.dumps(dpi_metadata) if dpi_metadata is not None else None,
) )
except Exception: except Exception:
logger.exception("DB flow metadata backfill failed") logger.exception("DB packet metadata backfill failed")
return 0 return 0
if not rows: if not rows:
@@ -358,7 +359,7 @@ class DatabasePool:
try: try:
self.broadcaster.sync_publish(serialized) self.broadcaster.sync_publish(serialized)
except Exception: except Exception:
logger.exception("Failed to publish flow-enriched packet row") logger.exception("Failed to publish tshark-enriched packet row")
return updated_count return updated_count

View File

@@ -1,77 +0,0 @@
"""Helpers for canonical bidirectional flow keys used by enrichers."""
from __future__ import annotations
import time
from typing import Any, Optional, Tuple
from scapy.all import IP, IPv6, TCP, UDP # type: ignore
def flow_key_from_endpoints(
ip_version: int,
protocol: int,
src_ip: Any,
src_port: Any,
dst_ip: Any,
dst_port: Any,
) -> Optional[str]:
"""Build a stable bidirectional key for IPv4/IPv6 TCP/UDP flows."""
if src_ip in (None, "") or dst_ip in (None, ""):
return None
try:
left = (str(src_ip), int(src_port or 0))
right = (str(dst_ip), int(dst_port or 0))
ep1, ep2 = (left, right) if left <= right else (right, left)
return f"{int(ip_version)}|{int(protocol)}|{ep1[0]}|{ep1[1]}|{ep2[0]}|{ep2[1]}"
except Exception:
return None
def flow_key_from_packet(pkt: Any) -> Optional[str]:
"""Extract a canonical flow key from a Scapy packet."""
if IP in pkt:
ip_layer = pkt[IP]
src_ip = getattr(ip_layer, "src", None)
dst_ip = getattr(ip_layer, "dst", None)
protocol = int(getattr(ip_layer, "proto", 0) or 0)
ip_version = 4
elif IPv6 in pkt:
ip_layer = pkt[IPv6]
src_ip = getattr(ip_layer, "src", None)
dst_ip = getattr(ip_layer, "dst", None)
protocol = int(getattr(ip_layer, "nh", 0) or 0)
ip_version = 6
else:
return None
if protocol not in (6, 17):
return None
src_port = 0
dst_port = 0
if TCP in pkt:
src_port = int(getattr(pkt[TCP], "sport", 0) or 0)
dst_port = int(getattr(pkt[TCP], "dport", 0) or 0)
elif UDP in pkt:
src_port = int(getattr(pkt[UDP], "sport", 0) or 0)
dst_port = int(getattr(pkt[UDP], "dport", 0) or 0)
return flow_key_from_endpoints(ip_version, protocol, src_ip, src_port, dst_ip, dst_port)
def packet_observed_at_ms(pkt: Any) -> int:
"""Return packet timestamp in milliseconds with a real-time fallback."""
try:
packet_time = float(getattr(pkt, "time", 0.0) or 0.0)
if packet_time > 0:
return int(packet_time * 1000)
except Exception:
pass
return int(time.time() * 1000)
def flow_cache_key(iface: str, flow_key: str) -> Tuple[str, str]:
"""Key active flow enrichment by interface and canonical flow ID."""
return (str(iface or ""), flow_key)

View File

@@ -1,168 +0,0 @@
"""Run NFStream on one interface and emit flow metadata as JSON lines."""
from __future__ import annotations
import argparse
import json
import logging
import signal
import sys
import time
from typing import Any, Optional
logger = logging.getLogger("nfstream_flow_worker")
_STOP = False
def _handle_signal(signum: int, _frame: Any) -> None:
global _STOP
_STOP = True
logger.info("Received signal %s, stopping NFStream worker", signum)
def _safe_scalar(value: Any) -> Any:
if isinstance(value, (str, int, float, bool)) or value is None:
return value
return str(value)
def _safe_int(value: Any) -> Optional[int]:
try:
if value is None:
return None
return int(value)
except Exception:
return None
def _build_event(iface: str, flow: Any, event_name: str) -> Optional[dict[str, Any]]:
ip_version = _safe_int(getattr(flow, "ip_version", None))
protocol = _safe_int(getattr(flow, "protocol", None))
src_ip = getattr(flow, "src_ip", None)
dst_ip = getattr(flow, "dst_ip", None)
src_port = _safe_int(getattr(flow, "src_port", None))
dst_port = _safe_int(getattr(flow, "dst_port", None))
if ip_version is None or protocol is None or not src_ip or not dst_ip:
return None
return {
"type": "flow_update",
"event": event_name,
"iface": iface,
"ip_version": ip_version,
"protocol": protocol,
"src_ip": str(src_ip),
"dst_ip": str(dst_ip),
"src_port": src_port or 0,
"dst_port": dst_port or 0,
"first_seen_ms": _safe_int(getattr(flow, "bidirectional_first_seen_ms", None))
or _safe_int(getattr(flow, "src2dst_first_seen_ms", None))
or int(time.time() * 1000),
"last_seen_ms": _safe_int(getattr(flow, "bidirectional_last_seen_ms", None))
or _safe_int(getattr(flow, "src2dst_last_seen_ms", None))
or int(time.time() * 1000),
"application_name": _safe_scalar(getattr(flow, "application_name", None)),
"application_category_name": _safe_scalar(getattr(flow, "application_category_name", None)),
"application_confidence": _safe_scalar(getattr(flow, "application_confidence", None)),
"requested_server_name": _safe_scalar(getattr(flow, "requested_server_name", None)),
"client_fingerprint": _safe_scalar(getattr(flow, "client_fingerprint", None)),
"server_fingerprint": _safe_scalar(getattr(flow, "server_fingerprint", None)),
"user_agent": _safe_scalar(getattr(flow, "user_agent", None)),
"content_type": _safe_scalar(getattr(flow, "content_type", None)),
"bidirectional_packets": _safe_int(getattr(flow, "bidirectional_packets", None)),
"bidirectional_bytes": _safe_int(getattr(flow, "bidirectional_bytes", None)),
}
def _emit(payload: dict[str, Any]) -> None:
sys.stdout.write(json.dumps(payload, separators=(",", ":")) + "\n")
sys.stdout.flush()
def main() -> int:
parser = argparse.ArgumentParser(description="NFStream flow worker")
parser.add_argument("--iface", required=True)
parser.add_argument("--idle-timeout", type=int, required=True)
parser.add_argument("--active-timeout", type=int, required=True)
parser.add_argument("--snapshot-length", type=int, required=True)
parser.add_argument("--n-dissections", type=int, required=True)
parser.add_argument("--n-meters", type=int, required=True)
parser.add_argument("--promiscuous-mode", action="store_true")
args = parser.parse_args()
signal.signal(signal.SIGTERM, _handle_signal)
signal.signal(signal.SIGINT, _handle_signal)
try:
from nfstream import NFPlugin, NFStreamer
except Exception as exc:
_emit(
{
"type": "worker_error",
"iface": args.iface,
"message": f"Failed to import nfstream: {exc!r}",
}
)
return 1
class EmitFlowMetadata(NFPlugin):
def on_init(self, _packet: Any, flow: Any) -> None:
payload = _build_event(args.iface, flow, "init")
if payload:
_emit(payload)
def on_update(self, _packet: Any, flow: Any) -> None:
payload = _build_event(args.iface, flow, "update")
if payload:
_emit(payload)
def on_expire(self, flow: Any) -> None:
payload = _build_event(args.iface, flow, "expire")
if payload:
_emit(payload)
try:
streamer = NFStreamer(
source=args.iface,
promiscuous_mode=bool(args.promiscuous_mode),
snapshot_length=args.snapshot_length,
idle_timeout=args.idle_timeout,
active_timeout=args.active_timeout,
n_dissections=args.n_dissections,
n_meters=args.n_meters,
statistical_analysis=False,
accounting_mode=0,
udps=EmitFlowMetadata(),
)
except Exception as exc:
_emit(
{
"type": "worker_error",
"iface": args.iface,
"message": f"Failed to start NFStreamer: {exc!r}",
}
)
return 1
_emit({"type": "worker_ready", "iface": args.iface})
try:
for _flow in streamer:
if _STOP:
break
except Exception as exc:
_emit(
{
"type": "worker_error",
"iface": args.iface,
"message": f"NFStreamer runtime error: {exc!r}",
}
)
return 1
return 0
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
raise SystemExit(main())

View File

@@ -1,393 +0,0 @@
"""Manage optional NFStream flow enrichment workers and packet lookups."""
from __future__ import annotations
import asyncio
import json
import logging
import os
import signal
import subprocess
import sys
import threading
import time
from pathlib import Path
from typing import Any, Dict, Iterable, Optional
import src.shared_objects as shared_objects
from src.config import settings
from src.utilities.flow_identity import flow_cache_key, flow_key_from_endpoints, flow_key_from_packet, packet_observed_at_ms
logger = logging.getLogger("nfstream_manager")
def _merge_metadata(base: Optional[Dict[str, Any]], extra: Dict[str, Any]) -> Dict[str, Any]:
merged = dict(base or {})
for key, value in extra.items():
if value not in (None, "", [], {}):
merged[key] = value
return merged
def _safe_text(value: Any) -> Optional[str]:
if value is None:
return None
text = str(value).strip()
return text if text else None
def _normalized_label(value: Any) -> Optional[str]:
text = _safe_text(value)
if text is None:
return None
if text.isdigit():
return None
if text.lower() in {"unknown", "other", "unclassified", "none", "null"}:
return None
return text
def _infer_is_encrypted(payload: Dict[str, Any]) -> Optional[bool]:
application_name = str(_normalized_label(payload.get("application_name")) or "").upper()
if any(token in application_name for token in ("TLS", "HTTPS", "QUIC", "SSL")):
return True
return None
def _build_enrichment(payload: Dict[str, Any], iface: str, flow_key: str) -> Dict[str, Any]:
app_protocol = _normalized_label(payload.get("application_name"))
if app_protocol is None and any(payload.get(key) for key in ("user_agent", "content_type")):
app_protocol = "HTTP"
metadata = {
"iface": iface,
"flow_key": flow_key,
"first_seen_ms": int(payload.get("first_seen_ms") or 0) or None,
"last_seen_ms": int(payload.get("last_seen_ms") or 0) or None,
"event": payload.get("event"),
"application_name": _safe_text(payload.get("application_name")),
"application_category_name": _safe_text(payload.get("application_category_name")),
"application_confidence": _safe_text(payload.get("application_confidence")),
"bidirectional_packets": payload.get("bidirectional_packets"),
"bidirectional_bytes": payload.get("bidirectional_bytes"),
}
metadata = _merge_metadata(
metadata,
{
"requested_server_name": payload.get("requested_server_name"),
"client_fingerprint": payload.get("client_fingerprint"),
"server_fingerprint": payload.get("server_fingerprint"),
"user_agent": payload.get("user_agent"),
"content_type": payload.get("content_type"),
},
)
return {
"app_protocol": app_protocol,
"app_master_protocol": app_protocol,
"app_category": _normalized_label(payload.get("application_category_name")),
"app_confidence": _safe_text(payload.get("application_confidence")),
"app_hostname": _safe_text(payload.get("requested_server_name")),
"app_is_encrypted": _infer_is_encrypted(payload),
"dpi_metadata": {"nfstream": metadata},
}
def _has_useful_metadata(payload: Dict[str, Any]) -> bool:
return any(
payload.get(key) not in (None, "", [], {})
for key in (
"application_name",
"application_category_name",
"application_confidence",
"requested_server_name",
"client_fingerprint",
"server_fingerprint",
"user_agent",
"content_type",
)
)
class NFStreamManager:
"""Own long-lived NFStream subprocesses and a short-lived flow metadata cache."""
def __init__(self) -> None:
self._enabled = settings.nfstream_enabled
self._workers: Dict[str, Dict[str, Any]] = {}
self._cache: Dict[tuple[str, str], Dict[str, Any]] = {}
self._last_error_by_iface: Dict[str, str] = {}
self._lock = threading.Lock()
@property
def enabled(self) -> bool:
return self._enabled
def update_interfaces(self, interfaces: Iterable[str]) -> None:
"""Align worker set with currently active interfaces."""
if not self._enabled:
return
targets = {iface.strip() for iface in interfaces if iface and iface.strip()}
with self._lock:
current = set(self._workers.keys())
for iface in sorted(current - targets):
self._stop_worker_locked(iface)
for iface in sorted(targets - current):
self._start_worker_locked(iface)
self._purge_cache_locked()
def stop(self) -> None:
"""Stop all workers and clear transient state."""
with self._lock:
for iface in list(self._workers.keys()):
self._stop_worker_locked(iface)
self._cache.clear()
def lookup_packet(self, pkt: Any, iface: Optional[str]) -> Dict[str, Any]:
"""Return NFStream-derived enrichment for the packet if a recent flow is known."""
if not self._enabled or not iface:
return {}
flow_key = flow_key_from_packet(pkt)
if flow_key is None:
return {}
now_ms = packet_observed_at_ms(pkt)
with self._lock:
self._purge_cache_locked(now_ms=now_ms)
payload = self._cache.get(flow_cache_key(iface, flow_key))
if payload is None:
return {}
first_seen_ms = int(payload.get("first_seen_ms") or 0)
last_seen_ms = int(payload.get("last_seen_ms") or 0)
window_ms = settings.nfstream_lookup_window_ms
if now_ms + window_ms < first_seen_ms or now_ms - window_ms > last_seen_ms:
return {}
return _build_enrichment(payload, iface, flow_key)
def get_debug_snapshot(self) -> Dict[str, Any]:
"""Expose current worker/cache state for API debugging."""
with self._lock:
self._purge_cache_locked()
return {
"enabled": self._enabled,
"workers": {
iface: {
"running": bool(worker.get("process") and worker["process"].poll() is None),
"thread_alive": bool(worker.get("reader") and worker["reader"].is_alive()),
"last_error": self._last_error_by_iface.get(iface),
}
for iface, worker in self._workers.items()
},
"cache_entries": len(self._cache),
"last_error_by_iface": dict(self._last_error_by_iface),
}
def _start_worker_locked(self, iface: str) -> None:
helper = Path(__file__).with_name("nfstream_flow_worker.py")
python_bin = sys.executable or "python3"
env = os.environ.copy()
env["PYTHONUNBUFFERED"] = "1"
backend_root = str(helper.parents[2])
existing_pythonpath = env.get("PYTHONPATH", "")
env["PYTHONPATH"] = backend_root if not existing_pythonpath else f"{backend_root}:{existing_pythonpath}"
cmd = [
python_bin,
str(helper),
"--iface",
iface,
"--idle-timeout",
str(settings.nfstream_idle_timeout_seconds),
"--active-timeout",
str(settings.nfstream_active_timeout_seconds),
"--snapshot-length",
str(settings.nfstream_snapshot_length),
"--n-dissections",
str(settings.nfstream_n_dissections),
"--n-meters",
str(settings.nfstream_n_meters),
]
if settings.nfstream_promiscuous_mode:
cmd.append("--promiscuous-mode")
logger.info("Starting NFStream worker for %s", iface)
try:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
env=env,
start_new_session=True,
)
except Exception:
logger.exception("Failed to start NFStream worker for %s", iface)
self._last_error_by_iface[iface] = "failed_to_start_process"
return
reader = threading.Thread(
target=self._read_loop,
args=(iface, process),
daemon=True,
name=f"nfstream-reader-{iface}",
)
self._workers[iface] = {
"process": process,
"reader": reader,
}
reader.start()
def _stop_worker_locked(self, iface: str) -> None:
worker = self._workers.pop(iface, None)
if worker is None:
return
process = worker.get("process")
reader = worker.get("reader")
if process is not None and process.poll() is None:
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
process.wait(timeout=settings.nfstream_process_stop_timeout_seconds)
except subprocess.TimeoutExpired:
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except ProcessLookupError:
pass
except Exception:
logger.exception("Failed to stop NFStream worker for %s cleanly", iface)
if reader is not None and reader.is_alive():
reader.join(timeout=settings.nfstream_reader_join_timeout_seconds)
for key in [cache_key for cache_key in self._cache.keys() if cache_key[0] == iface]:
self._cache.pop(key, None)
def _read_loop(self, iface: str, process: subprocess.Popen[str]) -> None:
stdout = process.stdout
if stdout is None:
return
for line in stdout:
text = line.strip()
if not text:
continue
try:
payload = json.loads(text)
except json.JSONDecodeError:
logger.info("nfstream[%s]: %s", iface, text)
continue
payload_type = payload.get("type")
if payload_type == "worker_ready":
logger.info("NFStream worker ready on %s", iface)
continue
if payload_type == "worker_error":
message = str(payload.get("message") or "unknown error")
logger.warning("NFStream worker error on %s: %s", iface, message)
with self._lock:
self._last_error_by_iface[iface] = message
continue
if payload_type != "flow_update":
logger.debug("Ignoring NFStream payload on %s: %s", iface, payload)
continue
flow_key = flow_key_from_endpoints(
payload.get("ip_version"),
payload.get("protocol"),
payload.get("src_ip"),
payload.get("src_port"),
payload.get("dst_ip"),
payload.get("dst_port"),
)
if flow_key is None:
continue
with self._lock:
previous = self._cache.get(flow_cache_key(iface, flow_key))
self._cache[flow_cache_key(iface, flow_key)] = dict(payload)
self._purge_cache_locked()
if self._should_backfill(previous, payload):
self._schedule_backfill(iface, flow_key, payload)
rc = process.poll()
if rc not in (0, None):
logger.warning("NFStream worker for %s exited with code %s", iface, rc)
def _purge_cache_locked(self, now_ms: Optional[int] = None) -> None:
if now_ms is None:
now_ms = int(time.time() * 1000)
expiry_ms = int(settings.nfstream_cache_ttl_seconds * 1000)
stale_keys = []
for key, payload in self._cache.items():
last_seen_ms = int(payload.get("last_seen_ms") or 0)
if last_seen_ms and now_ms - last_seen_ms > expiry_ms:
stale_keys.append(key)
for key in stale_keys:
self._cache.pop(key, None)
def _should_backfill(self, previous: Optional[Dict[str, Any]], current: Dict[str, Any]) -> bool:
if not _has_useful_metadata(current):
return False
if previous is None:
return True
interesting_keys = (
"application_name",
"application_category_name",
"application_confidence",
"requested_server_name",
"client_fingerprint",
"server_fingerprint",
"user_agent",
"content_type",
)
return any(previous.get(key) != current.get(key) for key in interesting_keys)
def _schedule_backfill(self, iface: str, flow_key: str, payload: Dict[str, Any]) -> None:
web_db = getattr(shared_objects, "db", None)
web_loop = getattr(shared_objects, "web_loop", None)
if web_db is None or web_loop is None:
return
enrichment = _build_enrichment(payload, iface, flow_key)
async def _backfill() -> None:
updated = await web_db.backfill_flow_metadata(
iface=iface,
src_ip=str(payload.get("src_ip")),
dst_ip=str(payload.get("dst_ip")),
src_port=int(payload.get("src_port") or 0),
dst_port=int(payload.get("dst_port") or 0),
protocol=int(payload.get("protocol") or 0),
first_seen_ms=int(payload.get("first_seen_ms") or 0),
last_seen_ms=int(payload.get("last_seen_ms") or 0),
enrichment=enrichment,
window_ms=settings.nfstream_lookup_window_ms,
)
if updated:
logger.debug("Backfilled NFStream metadata for %s packets on %s flow=%s", updated, iface, flow_key)
try:
future = asyncio.run_coroutine_threadsafe(_backfill(), web_loop)
future.add_done_callback(self._log_backfill_result)
except Exception:
logger.exception("Failed to schedule NFStream metadata backfill for %s", flow_key)
@staticmethod
def _log_backfill_result(future: Any) -> None:
try:
future.result()
except Exception:
logger.exception("NFStream metadata backfill task failed")
nfstream_manager = NFStreamManager()

View File

@@ -0,0 +1,542 @@
"""Manage optional tshark packet enrichment workers and matching."""
from __future__ import annotations
import asyncio
import logging
import os
import signal
import subprocess
import threading
import time
from typing import Any, Dict, Iterable, List, Optional, Tuple
import src.shared_objects as shared_objects
from scapy.all import IP, IPv6, TCP, UDP # type: ignore
from src.config import settings
logger = logging.getLogger("tshark_manager")
_FIELDS: List[str] = [
"frame.time_epoch",
"frame.interface_name",
"frame.len",
"ip.src",
"ipv6.src",
"ip.dst",
"ipv6.dst",
"ip.proto",
"ipv6.nxt",
"tcp.srcport",
"udp.srcport",
"tcp.dstport",
"udp.dstport",
"frame.protocols",
"http.request.method",
"http.request.uri",
"http.host",
"http.user_agent",
"http.response.code",
"http.response.phrase",
"http.server",
"http.content_type",
"tls.handshake.extensions_server_name",
"tls.handshake.version",
"dns.flags.response",
"dns.qry.name",
"dns.qry.type",
"dns.resp.name",
"dns.a",
"dns.aaaa",
"dns.cname",
]
def _safe_text(value: Any) -> Optional[str]:
if value is None:
return None
text = str(value).strip()
return text if text else None
def _safe_int(value: Any) -> Optional[int]:
try:
text = _safe_text(value)
if text is None:
return None
return int(text)
except Exception:
return None
def _safe_float(value: Any) -> Optional[float]:
try:
text = _safe_text(value)
if text is None:
return None
return float(text)
except Exception:
return None
def _safe_bool_flag(value: Any) -> Optional[bool]:
text = _safe_text(value)
if text is None:
return None
if text in {"1", "true", "True"}:
return True
if text in {"0", "false", "False"}:
return False
return None
def _jsonable(metadata: Dict[str, Any]) -> Dict[str, Any]:
out: Dict[str, Any] = {}
for key, value in metadata.items():
if value in (None, "", [], {}):
continue
out[key] = value
return out
def _packet_signature(
iface: str,
protocol: int,
src_ip: str,
dst_ip: str,
src_port: int,
dst_port: int,
length: int,
) -> Tuple[str, int, str, str, int, int, int]:
return (str(iface or ""), int(protocol), str(src_ip), str(dst_ip), int(src_port), int(dst_port), int(length))
def _signature_from_packet(pkt: Any, iface: str) -> Optional[Tuple[str, int, str, str, int, int, int]]:
if IP in pkt:
ip_layer = pkt[IP]
protocol = int(getattr(ip_layer, "proto", 0) or 0)
src_ip = str(getattr(ip_layer, "src", "") or "")
dst_ip = str(getattr(ip_layer, "dst", "") or "")
elif IPv6 in pkt:
ip_layer = pkt[IPv6]
protocol = int(getattr(ip_layer, "nh", 0) or 0)
src_ip = str(getattr(ip_layer, "src", "") or "")
dst_ip = str(getattr(ip_layer, "dst", "") or "")
else:
return None
src_port = 0
dst_port = 0
if TCP in pkt:
src_port = int(getattr(pkt[TCP], "sport", 0) or 0)
dst_port = int(getattr(pkt[TCP], "dport", 0) or 0)
elif UDP in pkt:
src_port = int(getattr(pkt[UDP], "sport", 0) or 0)
dst_port = int(getattr(pkt[UDP], "dport", 0) or 0)
if not src_ip or not dst_ip:
return None
return _packet_signature(iface, protocol, src_ip, dst_ip, src_port, dst_port, len(pkt))
def _packet_observed_at_ms(pkt: Any) -> int:
try:
packet_time = float(getattr(pkt, "time", 0.0) or 0.0)
if packet_time > 0:
return int(packet_time * 1000)
except Exception:
pass
return int(time.time() * 1000)
def _parse_line(line: str, fallback_iface: str) -> Optional[Dict[str, Any]]:
parts = line.rstrip("\n").split("\t")
if len(parts) < len(_FIELDS):
parts.extend([""] * (len(_FIELDS) - len(parts)))
row = dict(zip(_FIELDS, parts))
timestamp = _safe_float(row["frame.time_epoch"])
length = _safe_int(row["frame.len"])
protocol = _safe_int(row["ip.proto"]) or _safe_int(row["ipv6.nxt"])
src_ip = _safe_text(row["ip.src"]) or _safe_text(row["ipv6.src"])
dst_ip = _safe_text(row["ip.dst"]) or _safe_text(row["ipv6.dst"])
src_port = _safe_int(row["tcp.srcport"]) or _safe_int(row["udp.srcport"]) or 0
dst_port = _safe_int(row["tcp.dstport"]) or _safe_int(row["udp.dstport"]) or 0
iface = _safe_text(row["frame.interface_name"]) or fallback_iface
if timestamp is None or length is None or protocol is None or src_ip is None or dst_ip is None:
return None
observed_at_ms = int(timestamp * 1000)
return {
"iface": iface,
"observed_at_ms": observed_at_ms,
"length": length,
"protocol": protocol,
"src_ip": src_ip,
"dst_ip": dst_ip,
"src_port": src_port,
"dst_port": dst_port,
"frame_protocols": _safe_text(row["frame.protocols"]),
"http": _jsonable(
{
"method": _safe_text(row["http.request.method"]),
"uri": _safe_text(row["http.request.uri"]),
"host": _safe_text(row["http.host"]),
"user_agent": _safe_text(row["http.user_agent"]),
"response_code": _safe_int(row["http.response.code"]),
"response_phrase": _safe_text(row["http.response.phrase"]),
"server": _safe_text(row["http.server"]),
"content_type": _safe_text(row["http.content_type"]),
}
),
"tls": _jsonable(
{
"server_name": _safe_text(row["tls.handshake.extensions_server_name"]),
"handshake_version": _safe_text(row["tls.handshake.version"]),
}
),
"dns": _jsonable(
{
"is_response": _safe_bool_flag(row["dns.flags.response"]),
"query_name": _safe_text(row["dns.qry.name"]),
"query_type": _safe_text(row["dns.qry.type"]),
"response_name": _safe_text(row["dns.resp.name"]),
"a": _safe_text(row["dns.a"]),
"aaaa": _safe_text(row["dns.aaaa"]),
"cname": _safe_text(row["dns.cname"]),
}
),
}
def _build_enrichment(event: Dict[str, Any]) -> Dict[str, Any]:
http_meta = dict(event.get("http") or {})
tls_meta = dict(event.get("tls") or {})
dns_meta = dict(event.get("dns") or {})
protocols = str(event.get("frame_protocols") or "")
app_protocol: Optional[str] = None
app_category: Optional[str] = None
app_hostname: Optional[str] = None
app_is_encrypted: Optional[bool] = None
if http_meta:
app_protocol = "HTTP"
app_category = "Web"
app_hostname = http_meta.get("host")
app_is_encrypted = False
elif dns_meta:
app_protocol = "DNS"
app_category = "Infrastructure"
app_hostname = dns_meta.get("query_name") or dns_meta.get("response_name")
app_is_encrypted = False
elif tls_meta or "quic" in protocols.lower():
app_protocol = "QUIC" if "quic" in protocols.lower() and not tls_meta else "TLS"
app_category = "Encrypted"
app_hostname = tls_meta.get("server_name")
app_is_encrypted = True
tshark_meta = _jsonable(
{
"observed_at_ms": event.get("observed_at_ms"),
"frame_protocols": event.get("frame_protocols"),
"length": event.get("length"),
}
)
dpi_metadata = _jsonable(
{
"tshark": tshark_meta,
"http": http_meta,
"tls": tls_meta,
"dns": dns_meta,
}
)
return {
"app_protocol": app_protocol,
"app_master_protocol": app_protocol,
"app_category": app_category,
"app_confidence": "high" if app_protocol else None,
"app_hostname": app_hostname,
"app_is_encrypted": app_is_encrypted,
"dpi_metadata": dpi_metadata or None,
}
def _has_useful_enrichment(enrichment: Dict[str, Any]) -> bool:
return any(
enrichment.get(key) is not None
for key in ("app_protocol", "app_hostname", "app_is_encrypted", "dpi_metadata")
)
class TsharkManager:
"""Own long-lived tshark subprocesses and recent packet metadata cache."""
def __init__(self) -> None:
self._enabled = settings.tshark_enabled
self._workers: Dict[str, Dict[str, Any]] = {}
self._cache: Dict[Tuple[str, int, str, str, int, int, int], List[Dict[str, Any]]] = {}
self._last_error_by_iface: Dict[str, str] = {}
self._lock = threading.Lock()
@property
def enabled(self) -> bool:
return self._enabled
def update_interfaces(self, interfaces: Iterable[str]) -> None:
if not self._enabled:
return
targets = {iface.strip() for iface in interfaces if iface and iface.strip()}
with self._lock:
current = set(self._workers.keys())
for iface in sorted(current - targets):
self._stop_worker_locked(iface)
for iface in sorted(targets - current):
self._start_worker_locked(iface)
self._purge_cache_locked()
def stop(self) -> None:
with self._lock:
for iface in list(self._workers.keys()):
self._stop_worker_locked(iface)
self._cache.clear()
def lookup_packet(self, pkt: Any, iface: Optional[str]) -> Dict[str, Any]:
if not self._enabled or not iface:
return {}
signature = _signature_from_packet(pkt, iface)
if signature is None:
return {}
packet_time_ms = _packet_observed_at_ms(pkt)
with self._lock:
self._purge_cache_locked(now_ms=packet_time_ms)
entries = self._cache.get(signature)
if not entries:
return {}
window_ms = settings.tshark_match_window_ms
best_index: Optional[int] = None
best_delta: Optional[int] = None
for index, event in enumerate(entries):
delta = abs(int(event.get("observed_at_ms") or 0) - packet_time_ms)
if delta > window_ms:
continue
if best_delta is None or delta < best_delta:
best_index = index
best_delta = delta
if best_index is None:
return {}
event = entries.pop(best_index)
if not entries:
self._cache.pop(signature, None)
return _build_enrichment(event)
def get_debug_snapshot(self) -> Dict[str, Any]:
with self._lock:
self._purge_cache_locked()
return {
"enabled": self._enabled,
"workers": {
iface: {
"running": bool(worker.get("process") and worker["process"].poll() is None),
"thread_alive": bool(worker.get("reader") and worker["reader"].is_alive()),
"last_error": self._last_error_by_iface.get(iface),
}
for iface, worker in self._workers.items()
},
"cache_entries": sum(len(entries) for entries in self._cache.values()),
"last_error_by_iface": dict(self._last_error_by_iface),
}
def _start_worker_locked(self, iface: str) -> None:
cmd = [
"tshark",
"-l",
"-n",
"-Q",
"-i",
iface,
"-T",
"fields",
"-E",
"header=n",
"-E",
"separator=\t",
"-E",
"quote=n",
"-E",
"occurrence=f",
]
if settings.tshark_display_filter:
cmd.extend(["-Y", settings.tshark_display_filter])
for field in _FIELDS:
cmd.extend(["-e", field])
logger.info("Starting tshark worker for %s", iface)
try:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
env=os.environ.copy(),
start_new_session=True,
)
except Exception:
logger.exception("Failed to start tshark worker for %s", iface)
self._last_error_by_iface[iface] = "failed_to_start_process"
return
reader = threading.Thread(
target=self._read_loop,
args=(iface, process),
daemon=True,
name=f"tshark-reader-{iface}",
)
self._workers[iface] = {
"process": process,
"reader": reader,
}
reader.start()
def _stop_worker_locked(self, iface: str) -> None:
worker = self._workers.pop(iface, None)
if worker is None:
return
process = worker.get("process")
reader = worker.get("reader")
if process is not None and process.poll() is None:
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
process.wait(timeout=settings.tshark_process_stop_timeout_seconds)
except subprocess.TimeoutExpired:
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except ProcessLookupError:
pass
except Exception:
logger.exception("Failed to stop tshark worker for %s cleanly", iface)
if reader is not None and reader.is_alive():
reader.join(timeout=settings.tshark_reader_join_timeout_seconds)
stale_keys = [key for key in self._cache.keys() if key[0] == iface]
for key in stale_keys:
self._cache.pop(key, None)
def _read_loop(self, iface: str, process: subprocess.Popen[str]) -> None:
stdout = process.stdout
if stdout is None:
return
for line in stdout:
text = line.rstrip("\n")
if not text:
continue
event = _parse_line(text, iface)
if event is None:
logger.debug("Ignoring unparsable tshark line on %s: %s", iface, text)
continue
signature = _packet_signature(
str(event["iface"]),
int(event["protocol"]),
str(event["src_ip"]),
str(event["dst_ip"]),
int(event["src_port"]),
int(event["dst_port"]),
int(event["length"]),
)
with self._lock:
entries = self._cache.setdefault(signature, [])
entries.append(event)
self._purge_cache_locked(now_ms=int(event["observed_at_ms"]))
enrichment = _build_enrichment(event)
if _has_useful_enrichment(enrichment):
self._schedule_backfill(event, enrichment)
rc = process.poll()
if rc not in (0, None):
logger.warning("tshark worker for %s exited with code %s", iface, rc)
def _schedule_backfill(self, event: Dict[str, Any], enrichment: Dict[str, Any]) -> None:
web_db = getattr(shared_objects, "db", None)
web_loop = getattr(shared_objects, "web_loop", None)
if web_db is None or web_loop is None:
return
async def _backfill() -> None:
updated = await web_db.backfill_packet_metadata(
iface=str(event["iface"]),
src_ip=str(event["src_ip"]),
dst_ip=str(event["dst_ip"]),
src_port=int(event["src_port"]),
dst_port=int(event["dst_port"]),
protocol=int(event["protocol"]),
length=int(event["length"]),
observed_at_ms=int(event["observed_at_ms"]),
enrichment=enrichment,
window_ms=settings.tshark_match_window_ms,
)
if updated:
logger.debug(
"Backfilled tshark metadata for %s packets on %s %s:%s -> %s:%s",
updated,
event["iface"],
event["src_ip"],
event["src_port"],
event["dst_ip"],
event["dst_port"],
)
try:
future = asyncio.run_coroutine_threadsafe(_backfill(), web_loop)
future.add_done_callback(self._log_backfill_result)
except Exception:
logger.exception("Failed to schedule tshark metadata backfill")
@staticmethod
def _log_backfill_result(future: Any) -> None:
try:
future.result()
except Exception:
logger.exception("tshark metadata backfill task failed")
def _purge_cache_locked(self, now_ms: Optional[int] = None) -> None:
if now_ms is None:
now_ms = int(time.time() * 1000)
expiry_ms = int(settings.tshark_cache_ttl_seconds * 1000)
stale_keys: List[Tuple[str, int, str, str, int, int, int]] = []
for key, entries in self._cache.items():
fresh_entries = [
event
for event in entries
if now_ms - int(event.get("observed_at_ms") or 0) <= expiry_ms
]
if fresh_entries:
self._cache[key] = fresh_entries
else:
stale_keys.append(key)
for key in stale_keys:
self._cache.pop(key, None)
tshark_manager = TsharkManager()

View File

@@ -11,6 +11,7 @@ BACKEND_DIR="$APP_DIR/backend"
BACKEND_SERVICE="mitm-backend" BACKEND_SERVICE="mitm-backend"
NGINX_SITE="/etc/nginx/sites-available/mitm-webserver" NGINX_SITE="/etc/nginx/sites-available/mitm-webserver"
USER_ROOT="root" USER_ROOT="root"
BACKEND_ENV_FILE="$BACKEND_DIR/.env"
GITEA_RUNNER_URL="https://gitea.malmert.de//api/v1/repos/marcus/mitm-webserver/actions/runners/register" GITEA_RUNNER_URL="https://gitea.malmert.de//api/v1/repos/marcus/mitm-webserver/actions/runners/register"
RUNNER_TOKEN="cThC2xmAZWaOAqRRMENuVVTJckxaHiJxVGx2NCQY" RUNNER_TOKEN="cThC2xmAZWaOAqRRMENuVVTJckxaHiJxVGx2NCQY"
@@ -24,11 +25,11 @@ PYTHON_VERSION="3" # aktuelle Python 3 Version
# ----------------------------- # -----------------------------
echo "==> Update & Upgrade" echo "==> Update & Upgrade"
apt update && apt upgrade -y apt update && apt upgrade -y
apt install -y git curl build-essential nginx python3 python3-pip python3-venv unzip wget python3-dev \ DEBIAN_FRONTEND=noninteractive apt install -y git curl build-essential nginx python3 python3-pip python3-venv unzip wget python3-dev \
libpcap-dev autoconf automake libtool pkg-config libjson-c-dev gettext flex bison libnuma-dev \ libpcap-dev autoconf automake libtool pkg-config libjson-c-dev gettext flex bison libnuma-dev \
libpcre2-dev libmaxminddb-dev librrd-dev python3-bpfcc bpfcc-tools linux-headers-generic \ libpcre2-dev libmaxminddb-dev librrd-dev python3-bpfcc bpfcc-tools linux-headers-generic tshark \
clang llvm libelf-dev libbpf-dev iproute2 clang llvm libelf-dev libbpf-dev iproute2
apt install -y linux-headers-$(uname -r) || apt install -y linux-headers-generic DEBIAN_FRONTEND=noninteractive apt install -y linux-headers-$(uname -r) || DEBIAN_FRONTEND=noninteractive apt install -y linux-headers-generic
# ----------------------------- # -----------------------------
# Node.js installieren (LTS) # Node.js installieren (LTS)
@@ -70,6 +71,19 @@ cd "$BACKEND_DIR"
pip install -r requirements.txt pip install -r requirements.txt
deactivate deactivate
# -----------------------------
# Backend Environment Defaults
# -----------------------------
echo "==> Write backend environment defaults"
cat >"$BACKEND_ENV_FILE" <<EOL
BACKEND_TSHARK_ENABLED=true
BACKEND_TSHARK_DISPLAY_FILTER=http or tls or dns
BACKEND_TSHARK_CACHE_TTL_SECONDS=5.0
BACKEND_TSHARK_MATCH_WINDOW_MS=1500
BACKEND_TSHARK_READER_JOIN_TIMEOUT_SECONDS=2.0
BACKEND_TSHARK_PROCESS_STOP_TIMEOUT_SECONDS=3.0
EOL
# ----------------------------- # -----------------------------
# Systemd Service für Backend # Systemd Service für Backend
# ----------------------------- # -----------------------------