manag veth directly with sniffer
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 10s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 10s
This commit is contained in:
@@ -17,6 +17,10 @@ BACKEND_SNIFFER_BUFFER_DRAIN_INTERVAL_SECONDS=5.0
|
|||||||
BACKEND_SNIFFER_THREAD_JOIN_TIMEOUT_SECONDS=2.0
|
BACKEND_SNIFFER_THREAD_JOIN_TIMEOUT_SECONDS=2.0
|
||||||
BACKEND_CAPTURE_INTERFACE=
|
BACKEND_CAPTURE_INTERFACE=
|
||||||
BACKEND_CAPTURE_HEADER_ENABLED=true
|
BACKEND_CAPTURE_HEADER_ENABLED=true
|
||||||
|
BACKEND_CAPTURE_AUTOMANAGE=true
|
||||||
|
BACKEND_CAPTURE_MIRROR_INTERFACE=mitmcap0
|
||||||
|
BACKEND_CAPTURE_AUTOMANAGED_INTERFACE=mitmcap1
|
||||||
|
BACKEND_CAPTURE_BUILD_DIR=/tmp/mitm-bpf
|
||||||
BACKEND_TELEMETRY_PROCESS_STOP_TIMEOUT_SECONDS=3.0
|
BACKEND_TELEMETRY_PROCESS_STOP_TIMEOUT_SECONDS=3.0
|
||||||
BACKEND_TELEMETRY_READER_JOIN_TIMEOUT_SECONDS=2.0
|
BACKEND_TELEMETRY_READER_JOIN_TIMEOUT_SECONDS=2.0
|
||||||
BACKEND_NDPI_MAX_FLOWS=200000
|
BACKEND_NDPI_MAX_FLOWS=200000
|
||||||
|
|||||||
@@ -3,12 +3,58 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, IPvAnyAddress
|
from pydantic import BaseModel, ConfigDict, Field, IPvAnyAddress
|
||||||
|
|
||||||
|
|
||||||
class PacketDBModel(BaseModel):
|
class PacketDBModel(BaseModel):
|
||||||
"""Normalized packet representation used across DB and API layers."""
|
"""Normalized packet representation used across DB and API layers."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"id": 123,
|
||||||
|
"timestamp": "2026-03-05T12:34:56.789Z",
|
||||||
|
"updated_at": "2026-03-05T12:34:56.900Z",
|
||||||
|
"correlation_key": "pid:123456",
|
||||||
|
"correlation_source": "kernel_mark",
|
||||||
|
"packet_id": "123456",
|
||||||
|
"packet_uid": "9f6d3af0d3c81cb20ee8e7d32df7c56414460542",
|
||||||
|
"skb_mark": 123456,
|
||||||
|
"ingress_if": "eth0",
|
||||||
|
"egress_if": "eth1",
|
||||||
|
"capture_iface": "mitmcap0",
|
||||||
|
"src_mac": "aa:bb:cc:dd:ee:ff",
|
||||||
|
"dst_mac": "11:22:33:44:55:66",
|
||||||
|
"eth_type_raw": 2048,
|
||||||
|
"eth_type": "IPv4",
|
||||||
|
"ip_proto_raw": 6,
|
||||||
|
"ip_proto": "TCP",
|
||||||
|
"src_ip": "192.168.1.10",
|
||||||
|
"dst_ip": "192.168.1.1",
|
||||||
|
"src_port": 54321,
|
||||||
|
"dst_port": 80,
|
||||||
|
"vlan_id": None,
|
||||||
|
"length": 128,
|
||||||
|
"raw_present": True,
|
||||||
|
"capture_sources": ["af_packet", "telemetry"],
|
||||||
|
"raw_b64": "BASE64...",
|
||||||
|
"app_protocol": "HTTP",
|
||||||
|
"app_master_protocol": "HTTP",
|
||||||
|
"app_category": "Web",
|
||||||
|
"app_confidence": "high",
|
||||||
|
"app_hostname": "example.org",
|
||||||
|
"app_is_encrypted": False,
|
||||||
|
"app_risk_score": 0,
|
||||||
|
"dpi_metadata": {"method": "GET"},
|
||||||
|
"capture_metadata": {"header_magic": "MTCP", "header_version": 1},
|
||||||
|
"telemetry_metadata": {"event_type": "egress", "iface": "eth1"},
|
||||||
|
"verdict": "accept",
|
||||||
|
"verdict_reason": "egress-observed",
|
||||||
|
"verdict_confidence": "high",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
id: Union[int, str]
|
id: Union[int, str]
|
||||||
timestamp: datetime = Field(..., description="Packet timestamp in ISO format.")
|
timestamp: datetime = Field(..., description="Packet timestamp in ISO format.")
|
||||||
updated_at: Optional[datetime] = Field(None, description="Last DB update time for this row.")
|
updated_at: Optional[datetime] = Field(None, description="Last DB update time for this row.")
|
||||||
@@ -52,48 +98,3 @@ class PacketDBModel(BaseModel):
|
|||||||
egress_seen_at: Optional[datetime] = None
|
egress_seen_at: Optional[datetime] = None
|
||||||
verdict_seen_at: Optional[datetime] = None
|
verdict_seen_at: Optional[datetime] = None
|
||||||
packets: Optional[int] = None
|
packets: Optional[int] = None
|
||||||
|
|
||||||
class Config:
|
|
||||||
schema_extra = {
|
|
||||||
"example": {
|
|
||||||
"id": 123,
|
|
||||||
"timestamp": "2026-03-05T12:34:56.789Z",
|
|
||||||
"updated_at": "2026-03-05T12:34:56.900Z",
|
|
||||||
"correlation_key": "pid:123456",
|
|
||||||
"correlation_source": "kernel_mark",
|
|
||||||
"packet_id": "123456",
|
|
||||||
"packet_uid": "9f6d3af0d3c81cb20ee8e7d32df7c56414460542",
|
|
||||||
"skb_mark": 123456,
|
|
||||||
"ingress_if": "eth0",
|
|
||||||
"egress_if": "eth1",
|
|
||||||
"capture_iface": "mitmcap0",
|
|
||||||
"src_mac": "aa:bb:cc:dd:ee:ff",
|
|
||||||
"dst_mac": "11:22:33:44:55:66",
|
|
||||||
"eth_type_raw": 2048,
|
|
||||||
"eth_type": "IPv4",
|
|
||||||
"ip_proto_raw": 6,
|
|
||||||
"ip_proto": "TCP",
|
|
||||||
"src_ip": "192.168.1.10",
|
|
||||||
"dst_ip": "192.168.1.1",
|
|
||||||
"src_port": 54321,
|
|
||||||
"dst_port": 80,
|
|
||||||
"vlan_id": None,
|
|
||||||
"length": 128,
|
|
||||||
"raw_present": True,
|
|
||||||
"capture_sources": ["af_packet", "telemetry"],
|
|
||||||
"raw_b64": "BASE64...",
|
|
||||||
"app_protocol": "HTTP",
|
|
||||||
"app_master_protocol": "HTTP",
|
|
||||||
"app_category": "Web",
|
|
||||||
"app_confidence": "high",
|
|
||||||
"app_hostname": "example.org",
|
|
||||||
"app_is_encrypted": False,
|
|
||||||
"app_risk_score": 0,
|
|
||||||
"dpi_metadata": {"method": "GET"},
|
|
||||||
"capture_metadata": {"header_magic": "MTCP", "header_version": 1},
|
|
||||||
"telemetry_metadata": {"event_type": "egress", "iface": "eth1"},
|
|
||||||
"verdict": "accept",
|
|
||||||
"verdict_reason": "egress-observed",
|
|
||||||
"verdict_confidence": "high",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ class BackendSettings:
|
|||||||
sniffer_thread_join_timeout_seconds: float
|
sniffer_thread_join_timeout_seconds: float
|
||||||
capture_interface: str
|
capture_interface: str
|
||||||
capture_header_enabled: bool
|
capture_header_enabled: bool
|
||||||
|
capture_automanage: bool
|
||||||
|
capture_automanaged_mirror_if: str
|
||||||
|
capture_automanaged_capture_if: str
|
||||||
|
capture_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
|
||||||
ndpi_max_flows: int
|
ndpi_max_flows: int
|
||||||
@@ -84,6 +88,10 @@ def load_settings() -> BackendSettings:
|
|||||||
sniffer_thread_join_timeout_seconds=_env_float("BACKEND_SNIFFER_THREAD_JOIN_TIMEOUT_SECONDS", 2.0),
|
sniffer_thread_join_timeout_seconds=_env_float("BACKEND_SNIFFER_THREAD_JOIN_TIMEOUT_SECONDS", 2.0),
|
||||||
capture_interface=_env_str("BACKEND_CAPTURE_INTERFACE", ""),
|
capture_interface=_env_str("BACKEND_CAPTURE_INTERFACE", ""),
|
||||||
capture_header_enabled=_env_bool("BACKEND_CAPTURE_HEADER_ENABLED", True),
|
capture_header_enabled=_env_bool("BACKEND_CAPTURE_HEADER_ENABLED", True),
|
||||||
|
capture_automanage=_env_bool("BACKEND_CAPTURE_AUTOMANAGE", True),
|
||||||
|
capture_automanaged_mirror_if=_env_str("BACKEND_CAPTURE_MIRROR_INTERFACE", "mitmcap0"),
|
||||||
|
capture_automanaged_capture_if=_env_str("BACKEND_CAPTURE_AUTOMANAGED_INTERFACE", "mitmcap1"),
|
||||||
|
capture_build_dir=_env_str("BACKEND_CAPTURE_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),
|
||||||
ndpi_max_flows=_env_int("BACKEND_NDPI_MAX_FLOWS", 200_000),
|
ndpi_max_flows=_env_int("BACKEND_NDPI_MAX_FLOWS", 200_000),
|
||||||
|
|||||||
@@ -75,6 +75,13 @@ async def on_startup() -> None:
|
|||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def shutdown_event() -> None:
|
async def shutdown_event() -> None:
|
||||||
"""Stop network resources and release shared runtime objects."""
|
"""Stop network resources and release shared runtime objects."""
|
||||||
|
try:
|
||||||
|
from src.network_sniffer import stop_afpacket_sniffer
|
||||||
|
|
||||||
|
stop_afpacket_sniffer()
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Failed to stop sniffer sessions during shutdown")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
network_api.shutdown_network_api()
|
network_api.shutdown_network_api()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -87,6 +94,13 @@ async def shutdown_event() -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Failed to stop bridge telemetry collector")
|
logging.exception("Failed to stop bridge telemetry collector")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from src.utilities.capture_pipeline import capture_pipeline_manager
|
||||||
|
|
||||||
|
capture_pipeline_manager.stop_all()
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Failed to stop capture pipelines")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from src.utilities.packet_tracker import packet_tracker
|
from src.utilities.packet_tracker import packet_tracker
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ 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.capture_header import parse_capture_header
|
from src.utilities.capture_header import parse_capture_header
|
||||||
|
from src.utilities.capture_pipeline import capture_pipeline_manager
|
||||||
from src.utilities.ndpi_classifier import ndpi_classifier
|
from src.utilities.ndpi_classifier import ndpi_classifier
|
||||||
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
|
||||||
@@ -567,7 +568,7 @@ def _session_reader_loop(session_id: str) -> None:
|
|||||||
try:
|
try:
|
||||||
capture_meta = None
|
capture_meta = None
|
||||||
packet_bytes = raw
|
packet_bytes = raw
|
||||||
if settings.capture_header_enabled and settings.capture_interface and iface == settings.capture_interface:
|
if settings.capture_header_enabled:
|
||||||
capture_meta, packet_bytes = parse_capture_header(raw)
|
capture_meta, packet_bytes = parse_capture_header(raw)
|
||||||
pkt = Ether(packet_bytes)
|
pkt = Ether(packet_bytes)
|
||||||
pkt.sniffed_on = iface
|
pkt.sniffed_on = iface
|
||||||
@@ -617,8 +618,8 @@ def start_afpacket_sniffer(target: str, target_is_interface: bool = False) -> st
|
|||||||
"is_bridge": not target_is_interface,
|
"is_bridge": not target_is_interface,
|
||||||
"ports": [],
|
"ports": [],
|
||||||
"capture_ifaces": [],
|
"capture_ifaces": [],
|
||||||
|
"capture_bridge": None,
|
||||||
}
|
}
|
||||||
sessions[session_id] = session
|
|
||||||
|
|
||||||
# determine ports for this session
|
# determine ports for this session
|
||||||
if target_is_interface:
|
if target_is_interface:
|
||||||
@@ -626,19 +627,35 @@ def start_afpacket_sniffer(target: str, target_is_interface: bool = False) -> st
|
|||||||
capture_ifaces = [target]
|
capture_ifaces = [target]
|
||||||
else:
|
else:
|
||||||
ports = get_bridge_ports_once(target)
|
ports = get_bridge_ports_once(target)
|
||||||
capture_ifaces = [settings.capture_interface] if settings.capture_interface else list(ports)
|
capture_iface = None
|
||||||
|
if settings.capture_automanage:
|
||||||
|
capture_iface = capture_pipeline_manager.acquire(target)
|
||||||
|
session["capture_bridge"] = target
|
||||||
|
elif settings.capture_interface:
|
||||||
|
capture_iface = settings.capture_interface
|
||||||
|
capture_ifaces = [capture_iface] if capture_iface else list(ports)
|
||||||
|
|
||||||
session["ports"] = ports
|
session["ports"] = ports
|
||||||
session["capture_ifaces"] = capture_ifaces
|
session["capture_ifaces"] = capture_ifaces
|
||||||
|
sessions[session_id] = session
|
||||||
|
|
||||||
# create sockets for this session only
|
# create sockets for this session only
|
||||||
for iface in capture_ifaces:
|
try:
|
||||||
if not check_interface_exists(iface):
|
for iface in capture_ifaces:
|
||||||
logger.warning("Snapshot port %s missing for session %s, skipping", iface, session_id)
|
if not check_interface_exists(iface):
|
||||||
continue
|
logger.warning("Snapshot port %s missing for session %s, skipping", iface, session_id)
|
||||||
s = _create_af_packet_socket(iface)
|
continue
|
||||||
if s:
|
s = _create_af_packet_socket(iface)
|
||||||
session["sockets"][iface] = s
|
if s:
|
||||||
|
session["sockets"][iface] = s
|
||||||
|
except Exception:
|
||||||
|
sessions.pop(session_id, None)
|
||||||
|
if session.get("capture_bridge"):
|
||||||
|
try:
|
||||||
|
capture_pipeline_manager.release(session["capture_bridge"])
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to release capture pipeline after startup error")
|
||||||
|
raise
|
||||||
|
|
||||||
# start dedicated thread
|
# start dedicated thread
|
||||||
t = threading.Thread(target=_session_reader_loop, args=(session_id,), daemon=True)
|
t = threading.Thread(target=_session_reader_loop, args=(session_id,), daemon=True)
|
||||||
@@ -672,6 +689,12 @@ def stop_afpacket_sniffer(session_id: Optional[str] = None, target: Optional[str
|
|||||||
t = s.get("thread")
|
t = s.get("thread")
|
||||||
if t and isinstance(t, threading.Thread):
|
if t and isinstance(t, threading.Thread):
|
||||||
t.join(timeout=settings.sniffer_thread_join_timeout_seconds)
|
t.join(timeout=settings.sniffer_thread_join_timeout_seconds)
|
||||||
|
capture_bridge = s.get("capture_bridge")
|
||||||
|
if capture_bridge:
|
||||||
|
try:
|
||||||
|
capture_pipeline_manager.release(capture_bridge)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to release capture pipeline for bridge %s", capture_bridge)
|
||||||
_sync_bridge_telemetry()
|
_sync_bridge_telemetry()
|
||||||
logger.info("Stopped session %s", session_id)
|
logger.info("Stopped session %s", session_id)
|
||||||
return
|
return
|
||||||
@@ -681,6 +704,9 @@ def stop_afpacket_sniffer(session_id: Optional[str] = None, target: Optional[str
|
|||||||
# For interface-mode: close that iface in any session that has it
|
# For interface-mode: close that iface in any session that has it
|
||||||
for sid, s in list(sessions.items()):
|
for sid, s in list(sessions.items()):
|
||||||
if target in s.get("ports", []):
|
if target in s.get("ports", []):
|
||||||
|
if s.get("capture_bridge") == target:
|
||||||
|
stop_afpacket_sniffer(session_id=sid)
|
||||||
|
continue
|
||||||
# close only that iface socket in that session
|
# close only that iface socket in that session
|
||||||
sock = s["sockets"].pop(target, None)
|
sock = s["sockets"].pop(target, None)
|
||||||
if sock:
|
if sock:
|
||||||
@@ -748,6 +774,7 @@ def get_internal_debug_state() -> dict:
|
|||||||
sid: {
|
sid: {
|
||||||
"label": s.get("label"),
|
"label": s.get("label"),
|
||||||
"is_bridge": s.get("is_bridge"),
|
"is_bridge": s.get("is_bridge"),
|
||||||
|
"capture_bridge": s.get("capture_bridge"),
|
||||||
"ports": list(s.get("ports", [])),
|
"ports": list(s.get("ports", [])),
|
||||||
"capture_ifaces": list(s.get("capture_ifaces", [])),
|
"capture_ifaces": list(s.get("capture_ifaces", [])),
|
||||||
"sockets": list(s.get("sockets", {}).keys()),
|
"sockets": list(s.get("sockets", {}).keys()),
|
||||||
@@ -757,5 +784,6 @@ def get_internal_debug_state() -> dict:
|
|||||||
},
|
},
|
||||||
"buffer_len": len(_PACKET_BUFFER),
|
"buffer_len": len(_PACKET_BUFFER),
|
||||||
"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", [])}),
|
||||||
|
"capture_pipeline": capture_pipeline_manager.get_debug_snapshot(),
|
||||||
"packet_tracker": packet_tracker.get_debug_snapshot(),
|
"packet_tracker": packet_tracker.get_debug_snapshot(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ class BridgeTelemetryManager:
|
|||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PYTHONUNBUFFERED"] = "1"
|
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), "--ifaces", ",".join(sorted(self._interfaces))]
|
cmd = [python_bin, str(helper), "--ifaces", ",".join(sorted(self._interfaces))]
|
||||||
logger.info("Starting bridge telemetry collector for interfaces=%s", sorted(self._interfaces))
|
logger.info("Starting bridge telemetry collector for interfaces=%s", sorted(self._interfaces))
|
||||||
|
|||||||
143
backend/src/utilities/capture_pipeline.py
Normal file
143
backend/src/utilities/capture_pipeline.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
"""Manage the optional tc-based bridge capture pipeline for mirrored packet storage."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from src.config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger("capture_pipeline")
|
||||||
|
|
||||||
|
|
||||||
|
class CapturePipelineManager:
|
||||||
|
"""Reference-count mirrored capture pipelines per bridge."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._refs: Dict[str, int] = {}
|
||||||
|
self._active: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
def acquire(self, bridge: str) -> Optional[str]:
|
||||||
|
"""Ensure a bridge capture pipeline exists and return the capture interface name."""
|
||||||
|
if not settings.capture_automanage:
|
||||||
|
return settings.capture_interface or None
|
||||||
|
|
||||||
|
bridge = bridge.strip()
|
||||||
|
if not bridge:
|
||||||
|
raise ValueError("Bridge name is required for capture pipeline setup")
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
refcount = self._refs.get(bridge, 0)
|
||||||
|
if refcount > 0:
|
||||||
|
self._refs[bridge] = refcount + 1
|
||||||
|
active = self._active.get(bridge, {})
|
||||||
|
return active.get("capture_if") or settings.capture_interface or settings.capture_automanaged_capture_if
|
||||||
|
|
||||||
|
self._setup_bridge_locked(bridge)
|
||||||
|
self._refs[bridge] = 1
|
||||||
|
active = self._active.get(bridge, {})
|
||||||
|
return active.get("capture_if") or settings.capture_interface or settings.capture_automanaged_capture_if
|
||||||
|
|
||||||
|
def release(self, bridge: str) -> None:
|
||||||
|
"""Tear down a bridge capture pipeline when the last session releases it."""
|
||||||
|
if not settings.capture_automanage:
|
||||||
|
return
|
||||||
|
|
||||||
|
bridge = bridge.strip()
|
||||||
|
if not bridge:
|
||||||
|
return
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
refcount = self._refs.get(bridge, 0)
|
||||||
|
if refcount <= 1:
|
||||||
|
self._refs.pop(bridge, None)
|
||||||
|
try:
|
||||||
|
self._teardown_bridge_locked(bridge)
|
||||||
|
finally:
|
||||||
|
self._active.pop(bridge, None)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._refs[bridge] = refcount - 1
|
||||||
|
|
||||||
|
def stop_all(self) -> None:
|
||||||
|
"""Remove all managed capture pipelines."""
|
||||||
|
if not settings.capture_automanage:
|
||||||
|
return
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
bridges = list(self._active.keys())
|
||||||
|
self._refs.clear()
|
||||||
|
for bridge in bridges:
|
||||||
|
try:
|
||||||
|
self._teardown_bridge_locked(bridge)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to tear down capture pipeline for bridge %s", bridge)
|
||||||
|
self._active.clear()
|
||||||
|
|
||||||
|
def get_debug_snapshot(self) -> Dict[str, object]:
|
||||||
|
with self._lock:
|
||||||
|
return {
|
||||||
|
"automanage_enabled": settings.capture_automanage,
|
||||||
|
"refs": dict(self._refs),
|
||||||
|
"active": dict(self._active),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _setup_bridge_locked(self, bridge: str) -> None:
|
||||||
|
script = self._repo_root() / "tools" / "setup_bridge_capture.sh"
|
||||||
|
mirror_if = settings.capture_automanaged_mirror_if
|
||||||
|
capture_if = settings.capture_interface or settings.capture_automanaged_capture_if
|
||||||
|
cmd = [
|
||||||
|
str(script),
|
||||||
|
"--bridge",
|
||||||
|
bridge,
|
||||||
|
"--mirror-if",
|
||||||
|
mirror_if,
|
||||||
|
"--capture-if",
|
||||||
|
capture_if,
|
||||||
|
"--build-dir",
|
||||||
|
settings.capture_build_dir,
|
||||||
|
]
|
||||||
|
logger.info(
|
||||||
|
"Setting up capture pipeline for bridge=%s mirror_if=%s capture_if=%s",
|
||||||
|
bridge,
|
||||||
|
mirror_if,
|
||||||
|
capture_if,
|
||||||
|
)
|
||||||
|
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||||
|
self._active[bridge] = {
|
||||||
|
"mirror_if": mirror_if,
|
||||||
|
"capture_if": capture_if,
|
||||||
|
"build_dir": settings.capture_build_dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _teardown_bridge_locked(self, bridge: str) -> None:
|
||||||
|
script = self._repo_root() / "tools" / "teardown_bridge_capture.sh"
|
||||||
|
active = self._active.get(bridge, {})
|
||||||
|
mirror_if = active.get("mirror_if") or settings.capture_automanaged_mirror_if
|
||||||
|
capture_if = active.get("capture_if") or settings.capture_interface or settings.capture_automanaged_capture_if
|
||||||
|
cmd = [
|
||||||
|
str(script),
|
||||||
|
"--bridge",
|
||||||
|
bridge,
|
||||||
|
"--mirror-if",
|
||||||
|
mirror_if,
|
||||||
|
"--capture-if",
|
||||||
|
capture_if,
|
||||||
|
]
|
||||||
|
logger.info(
|
||||||
|
"Tearing down capture pipeline for bridge=%s mirror_if=%s capture_if=%s",
|
||||||
|
bridge,
|
||||||
|
mirror_if,
|
||||||
|
capture_if,
|
||||||
|
)
|
||||||
|
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||||
|
|
||||||
|
def _repo_root(self) -> Path:
|
||||||
|
return Path(__file__).resolve().parents[3]
|
||||||
|
|
||||||
|
|
||||||
|
capture_pipeline_manager = CapturePipelineManager()
|
||||||
Reference in New Issue
Block a user