This commit is contained in:
@@ -15,12 +15,7 @@ BACKEND_SNIFFER_SELECTOR_TIMEOUT_SECONDS=1.0
|
|||||||
BACKEND_SNIFFER_RECV_BYTES=65536
|
BACKEND_SNIFFER_RECV_BYTES=65536
|
||||||
BACKEND_SNIFFER_BUFFER_DRAIN_INTERVAL_SECONDS=5.0
|
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_BRIDGE_BPF_BUILD_DIR=/tmp/mitm-bpf
|
||||||
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
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class PacketDBModel(BaseModel):
|
|||||||
"skb_mark": 123456,
|
"skb_mark": 123456,
|
||||||
"ingress_if": "eth0",
|
"ingress_if": "eth0",
|
||||||
"egress_if": "eth1",
|
"egress_if": "eth1",
|
||||||
"capture_iface": "mitmcap0",
|
"capture_iface": None,
|
||||||
"src_mac": "aa:bb:cc:dd:ee:ff",
|
"src_mac": "aa:bb:cc:dd:ee:ff",
|
||||||
"dst_mac": "11:22:33:44:55:66",
|
"dst_mac": "11:22:33:44:55:66",
|
||||||
"eth_type_raw": 2048,
|
"eth_type_raw": 2048,
|
||||||
@@ -36,7 +36,7 @@ class PacketDBModel(BaseModel):
|
|||||||
"vlan_id": None,
|
"vlan_id": None,
|
||||||
"length": 128,
|
"length": 128,
|
||||||
"raw_present": True,
|
"raw_present": True,
|
||||||
"capture_sources": ["af_packet", "telemetry"],
|
"capture_sources": ["tc_ingress_raw", "telemetry"],
|
||||||
"raw_b64": "BASE64...",
|
"raw_b64": "BASE64...",
|
||||||
"app_protocol": "HTTP",
|
"app_protocol": "HTTP",
|
||||||
"app_master_protocol": "HTTP",
|
"app_master_protocol": "HTTP",
|
||||||
@@ -46,8 +46,8 @@ class PacketDBModel(BaseModel):
|
|||||||
"app_is_encrypted": False,
|
"app_is_encrypted": False,
|
||||||
"app_risk_score": 0,
|
"app_risk_score": 0,
|
||||||
"dpi_metadata": {"method": "GET"},
|
"dpi_metadata": {"method": "GET"},
|
||||||
"capture_metadata": {"header_magic": "MTCP", "header_version": 1},
|
"capture_metadata": {"capture_mode": "tc_ingress", "packet_id": "123456"},
|
||||||
"telemetry_metadata": {"event_type": "egress", "iface": "eth1"},
|
"telemetry_metadata": {"event_type": "egress", "iface": "eth1", "packet_id": "123456"},
|
||||||
"verdict": "accept",
|
"verdict": "accept",
|
||||||
"verdict_reason": "egress-observed",
|
"verdict_reason": "egress-observed",
|
||||||
"verdict_confidence": "high",
|
"verdict_confidence": "high",
|
||||||
@@ -89,7 +89,7 @@ class PacketDBModel(BaseModel):
|
|||||||
app_is_encrypted: Optional[bool] = Field(None, description="Whether detected protocol appears encrypted.")
|
app_is_encrypted: Optional[bool] = Field(None, description="Whether detected protocol appears encrypted.")
|
||||||
app_risk_score: Optional[int] = Field(None, description="Count/score of detected nDPI risks.")
|
app_risk_score: Optional[int] = Field(None, description="Count/score of detected nDPI risks.")
|
||||||
dpi_metadata: Optional[dict] = Field(None, description="Raw DPI metadata from nDPI.")
|
dpi_metadata: Optional[dict] = Field(None, description="Raw DPI metadata from nDPI.")
|
||||||
capture_metadata: Optional[dict] = Field(None, description="Capture-side metadata from the mirrored packet shim.")
|
capture_metadata: Optional[dict] = Field(None, description="Raw-capture metadata from the bridge tc ingress exporter.")
|
||||||
telemetry_metadata: Optional[dict] = Field(None, description="Kernel telemetry details from eBPF collector.")
|
telemetry_metadata: Optional[dict] = Field(None, description="Kernel telemetry details from eBPF collector.")
|
||||||
verdict: Optional[str] = None
|
verdict: Optional[str] = None
|
||||||
verdict_reason: Optional[str] = None
|
verdict_reason: Optional[str] = None
|
||||||
|
|||||||
@@ -25,13 +25,6 @@ def _env_float(name: str, default: float) -> float:
|
|||||||
return float(value)
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
def _env_bool(name: str, default: bool) -> bool:
|
|
||||||
value = os.getenv(name)
|
|
||||||
if value in (None, ""):
|
|
||||||
return default
|
|
||||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BackendSettings:
|
class BackendSettings:
|
||||||
db_dsn: str
|
db_dsn: str
|
||||||
@@ -51,12 +44,7 @@ class BackendSettings:
|
|||||||
sniffer_recv_bytes: int
|
sniffer_recv_bytes: int
|
||||||
sniffer_buffer_drain_interval_seconds: float
|
sniffer_buffer_drain_interval_seconds: float
|
||||||
sniffer_thread_join_timeout_seconds: float
|
sniffer_thread_join_timeout_seconds: float
|
||||||
capture_interface: str
|
bridge_bpf_build_dir: str
|
||||||
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
|
||||||
@@ -86,12 +74,7 @@ def load_settings() -> BackendSettings:
|
|||||||
sniffer_recv_bytes=_env_int("BACKEND_SNIFFER_RECV_BYTES", 65_536),
|
sniffer_recv_bytes=_env_int("BACKEND_SNIFFER_RECV_BYTES", 65_536),
|
||||||
sniffer_buffer_drain_interval_seconds=_env_float("BACKEND_SNIFFER_BUFFER_DRAIN_INTERVAL_SECONDS", 5.0),
|
sniffer_buffer_drain_interval_seconds=_env_float("BACKEND_SNIFFER_BUFFER_DRAIN_INTERVAL_SECONDS", 5.0),
|
||||||
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", ""),
|
bridge_bpf_build_dir=_env_str("BACKEND_BRIDGE_BPF_BUILD_DIR", "/tmp/mitm-bpf"),
|
||||||
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),
|
||||||
|
|||||||
@@ -94,13 +94,6 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ 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_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
|
||||||
@@ -68,6 +66,7 @@ class PacketInfo(TypedDict, total=False):
|
|||||||
packet_id: Optional[str]
|
packet_id: Optional[str]
|
||||||
packet_uid: str
|
packet_uid: str
|
||||||
skb_mark: Optional[int]
|
skb_mark: Optional[int]
|
||||||
|
capture_source: str
|
||||||
iface: str
|
iface: str
|
||||||
capture_iface: Optional[str]
|
capture_iface: Optional[str]
|
||||||
length: int
|
length: int
|
||||||
@@ -176,9 +175,10 @@ def parse_packet(pkt, bridge_label: str, capture_metadata: Optional[Dict[str, An
|
|||||||
|
|
||||||
pkt_info: PacketInfo = {
|
pkt_info: PacketInfo = {
|
||||||
"iface": pkt_iface,
|
"iface": pkt_iface,
|
||||||
"capture_iface": pkt_iface if capture_metadata else None,
|
"capture_iface": None,
|
||||||
"length": len(pkt),
|
"length": len(pkt),
|
||||||
"raw": bytes(pkt),
|
"raw": bytes(pkt),
|
||||||
|
"capture_source": "tc_ingress_raw" if capture_metadata else "af_packet",
|
||||||
"packet_id": capture_metadata.get("packet_id") if capture_metadata else None,
|
"packet_id": capture_metadata.get("packet_id") if capture_metadata else None,
|
||||||
"skb_mark": capture_metadata.get("skb_mark") if capture_metadata else None,
|
"skb_mark": capture_metadata.get("skb_mark") if capture_metadata else None,
|
||||||
"src_mac": None,
|
"src_mac": None,
|
||||||
@@ -412,6 +412,17 @@ def parse_packet(pkt, bridge_label: str, capture_metadata: Optional[Dict[str, An
|
|||||||
logger.exception("Failed to schedule DB insert (unexpected)")
|
logger.exception("Failed to schedule DB insert (unexpected)")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_packet_bytes(
|
||||||
|
packet_bytes: bytes,
|
||||||
|
iface: str,
|
||||||
|
capture_metadata: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Parse one raw Ethernet frame using the shared Scapy packet path."""
|
||||||
|
pkt = Ether(packet_bytes)
|
||||||
|
pkt.sniffed_on = iface
|
||||||
|
parse_packet(pkt, iface, capture_metadata=capture_metadata)
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# AF_PACKET socket utilities
|
# AF_PACKET socket utilities
|
||||||
# -------------------------
|
# -------------------------
|
||||||
@@ -484,22 +495,20 @@ def _ensure_socket_for_session(sockets: Dict[str, socket.socket], iface: str, br
|
|||||||
|
|
||||||
|
|
||||||
def _sync_bridge_telemetry() -> None:
|
def _sync_bridge_telemetry() -> None:
|
||||||
interfaces = sorted({iface for session in sessions.values() for iface in session.get("ports", [])})
|
interfaces = sorted(
|
||||||
|
{
|
||||||
|
iface
|
||||||
|
for session in sessions.values()
|
||||||
|
if session.get("is_bridge")
|
||||||
|
for iface in session.get("ports", [])
|
||||||
|
}
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
bridge_telemetry_manager.update_interfaces(interfaces)
|
bridge_telemetry_manager.update_interfaces(interfaces)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to update bridge telemetry collector")
|
logger.exception("Failed to update bridge telemetry collector")
|
||||||
|
|
||||||
|
|
||||||
def _managed_capture_ifaces() -> set[str]:
|
|
||||||
return {
|
|
||||||
iface
|
|
||||||
for session in sessions.values()
|
|
||||||
if session.get("is_bridge")
|
|
||||||
for iface in session.get("capture_ifaces", [])
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Per-session reader loop
|
# Per-session reader loop
|
||||||
# -------------------------
|
# -------------------------
|
||||||
@@ -575,16 +584,9 @@ def _session_reader_loop(session_id: str) -> None:
|
|||||||
|
|
||||||
# parse with scapy
|
# parse with scapy
|
||||||
try:
|
try:
|
||||||
capture_meta = None
|
pkt = Ether(raw)
|
||||||
packet_bytes = raw
|
|
||||||
if settings.capture_header_enabled:
|
|
||||||
capture_meta, packet_bytes = parse_capture_header(raw)
|
|
||||||
if capture_meta is None and iface in _managed_capture_ifaces():
|
|
||||||
logger.debug("Skipping unshimmed packet on managed capture interface %s", iface)
|
|
||||||
continue
|
|
||||||
pkt = Ether(packet_bytes)
|
|
||||||
pkt.sniffed_on = iface
|
pkt.sniffed_on = iface
|
||||||
parse_packet(pkt, label, capture_metadata=capture_meta)
|
parse_packet(pkt, label)
|
||||||
logger.debug("Captured packet on %s in session %s (len=%d)", iface, session_id, len(raw))
|
logger.debug("Captured packet on %s in session %s (len=%d)", iface, session_id, len(raw))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to parse/process packet from %s in session %s", iface, session_id)
|
logger.exception("Failed to parse/process packet from %s in session %s", iface, session_id)
|
||||||
@@ -630,7 +632,6 @@ 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# determine ports for this session
|
# determine ports for this session
|
||||||
@@ -639,13 +640,7 @@ 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_iface = None
|
capture_ifaces = []
|
||||||
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
|
||||||
@@ -662,17 +657,15 @@ def start_afpacket_sniffer(target: str, target_is_interface: bool = False) -> st
|
|||||||
session["sockets"][iface] = s
|
session["sockets"][iface] = s
|
||||||
except Exception:
|
except Exception:
|
||||||
sessions.pop(session_id, None)
|
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
|
raise
|
||||||
|
|
||||||
# start dedicated thread
|
# start dedicated thread
|
||||||
|
if capture_ifaces:
|
||||||
t = threading.Thread(target=_session_reader_loop, args=(session_id,), daemon=True)
|
t = threading.Thread(target=_session_reader_loop, args=(session_id,), daemon=True)
|
||||||
session["thread"] = t
|
session["thread"] = t
|
||||||
t.start()
|
t.start()
|
||||||
|
else:
|
||||||
|
session["thread"] = None
|
||||||
_sync_bridge_telemetry()
|
_sync_bridge_telemetry()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Started sniffer session %s label=%s ports=%s capture_ifaces=%s",
|
"Started sniffer session %s label=%s ports=%s capture_ifaces=%s",
|
||||||
@@ -701,12 +694,6 @@ 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
|
||||||
@@ -715,10 +702,13 @@ def stop_afpacket_sniffer(session_id: Optional[str] = None, target: Optional[str
|
|||||||
if target:
|
if target:
|
||||||
# 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 not target_is_interface and s.get("is_bridge") and s.get("label") == target:
|
||||||
if s.get("capture_bridge") == target:
|
|
||||||
stop_afpacket_sniffer(session_id=sid)
|
stop_afpacket_sniffer(session_id=sid)
|
||||||
continue
|
continue
|
||||||
|
if target_is_interface and not s.get("is_bridge") and s.get("label") == target:
|
||||||
|
stop_afpacket_sniffer(session_id=sid)
|
||||||
|
continue
|
||||||
|
if target in s.get("ports", []):
|
||||||
# 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:
|
||||||
@@ -774,6 +764,15 @@ def get_sniffer_status() -> Dict[str, Dict[str, object]]:
|
|||||||
"session_id": sid,
|
"session_id": sid,
|
||||||
"session_label": s.get("label"),
|
"session_label": s.get("label"),
|
||||||
}
|
}
|
||||||
|
if not s.get("sockets") and s.get("is_bridge"):
|
||||||
|
for iface in s.get("ports", []):
|
||||||
|
out[iface] = {
|
||||||
|
"running": not s.get("stop_event").is_set(),
|
||||||
|
"exists": check_interface_exists(iface),
|
||||||
|
"up": check_interface_up(iface),
|
||||||
|
"session_id": sid,
|
||||||
|
"session_label": s.get("label"),
|
||||||
|
}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -786,16 +785,16 @@ 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()),
|
||||||
"thread_alive": bool(s.get("thread") and s.get("thread").is_alive()),
|
"thread_alive": bool(s.get("thread") and s.get("thread").is_alive()),
|
||||||
|
"active": not s.get("stop_event").is_set(),
|
||||||
}
|
}
|
||||||
for sid, s in sessions.items()
|
for sid, s in sessions.items()
|
||||||
},
|
},
|
||||||
"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",
|
||||||
"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(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""Manage the eBPF telemetry subprocess used for bridge direction and verdict events."""
|
"""Manage the eBPF/tc telemetry subprocess used for bridge packet capture and verdict events."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -19,7 +20,7 @@ logger = logging.getLogger("bridge_telemetry")
|
|||||||
|
|
||||||
|
|
||||||
class BridgeTelemetryManager:
|
class BridgeTelemetryManager:
|
||||||
"""Run a single eBPF collector process for the active sniffed interfaces."""
|
"""Run one tc/eBPF collector for the currently sniffed bridge interfaces."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._interfaces: set[str] = set()
|
self._interfaces: set[str] = set()
|
||||||
@@ -28,7 +29,7 @@ class BridgeTelemetryManager:
|
|||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
def update_interfaces(self, interfaces: Iterable[str]) -> None:
|
def update_interfaces(self, interfaces: Iterable[str]) -> None:
|
||||||
"""Restart the collector when the active interface set changes."""
|
"""Restart the collector when the active bridge interface set changes."""
|
||||||
normalized = {iface.strip() for iface in interfaces if iface and iface.strip()}
|
normalized = {iface.strip() for iface in interfaces if iface and iface.strip()}
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if normalized == self._interfaces:
|
if normalized == self._interfaces:
|
||||||
@@ -58,7 +59,14 @@ class BridgeTelemetryManager:
|
|||||||
existing_pythonpath = env.get("PYTHONPATH", "")
|
existing_pythonpath = env.get("PYTHONPATH", "")
|
||||||
env["PYTHONPATH"] = backend_root if not existing_pythonpath else f"{backend_root}:{existing_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)),
|
||||||
|
"--build-dir",
|
||||||
|
settings.bridge_bpf_build_dir,
|
||||||
|
]
|
||||||
logger.info("Starting bridge telemetry collector for interfaces=%s", sorted(self._interfaces))
|
logger.info("Starting bridge telemetry collector for interfaces=%s", sorted(self._interfaces))
|
||||||
try:
|
try:
|
||||||
self._process = subprocess.Popen(
|
self._process = subprocess.Popen(
|
||||||
@@ -100,6 +108,31 @@ class BridgeTelemetryManager:
|
|||||||
if reader is not None and reader.is_alive():
|
if reader is not None and reader.is_alive():
|
||||||
reader.join(timeout=settings.telemetry_reader_join_timeout_seconds)
|
reader.join(timeout=settings.telemetry_reader_join_timeout_seconds)
|
||||||
|
|
||||||
|
def _handle_ingress_packet(self, event: dict[str, object]) -> None:
|
||||||
|
raw_b64 = event.pop("raw_b64", None)
|
||||||
|
if not isinstance(raw_b64, str) or not raw_b64:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
packet_bytes = base64.b64decode(raw_b64)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to decode ingress raw packet")
|
||||||
|
return
|
||||||
|
|
||||||
|
capture_metadata = {
|
||||||
|
"capture_source": "tc_ingress_raw",
|
||||||
|
"packet_id": event.get("packet_id"),
|
||||||
|
"skb_mark": event.get("skb_mark"),
|
||||||
|
"capture_mode": "tc_ingress",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
from src.network_sniffer import parse_packet_bytes
|
||||||
|
|
||||||
|
parse_packet_bytes(packet_bytes, str(event.get("iface")), capture_metadata=capture_metadata)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to process ingress raw packet event")
|
||||||
|
|
||||||
def _read_loop(self, process: subprocess.Popen[str]) -> None:
|
def _read_loop(self, process: subprocess.Popen[str]) -> None:
|
||||||
stdout = process.stdout
|
stdout = process.stdout
|
||||||
if stdout is None:
|
if stdout is None:
|
||||||
@@ -119,6 +152,9 @@ class BridgeTelemetryManager:
|
|||||||
logger.info("bridge-telemetry: %s", event)
|
logger.info("bridge-telemetry: %s", event)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if event.get("event_type") == "ingress":
|
||||||
|
self._handle_ingress_packet(event)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
packet_tracker.observe_telemetry(event)
|
packet_tracker.observe_telemetry(event)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
"""Parse the capture-side shim that carries the skb mark to AF_PACKET userspace."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import struct
|
|
||||||
from typing import Dict, Optional, Tuple
|
|
||||||
|
|
||||||
from src.utilities.packet_mark import packet_id_from_mark
|
|
||||||
|
|
||||||
|
|
||||||
CAPTURE_HEADER_MAGIC = b"MTCP"
|
|
||||||
CAPTURE_HEADER_VERSION = 1
|
|
||||||
CAPTURE_HEADER_STRUCT = struct.Struct("!4sBBHII")
|
|
||||||
CAPTURE_HEADER_SIZE = CAPTURE_HEADER_STRUCT.size
|
|
||||||
|
|
||||||
|
|
||||||
def parse_capture_header(frame: bytes) -> Tuple[Optional[Dict[str, object]], bytes]:
|
|
||||||
"""Return parsed capture metadata and the stripped Ethernet frame."""
|
|
||||||
if len(frame) < CAPTURE_HEADER_SIZE:
|
|
||||||
return None, frame
|
|
||||||
|
|
||||||
magic, version, flags, header_len, skb_mark, aux_value = CAPTURE_HEADER_STRUCT.unpack_from(frame)
|
|
||||||
if magic != CAPTURE_HEADER_MAGIC or version != CAPTURE_HEADER_VERSION:
|
|
||||||
return None, frame
|
|
||||||
if header_len < CAPTURE_HEADER_SIZE or len(frame) < header_len:
|
|
||||||
return None, frame
|
|
||||||
|
|
||||||
metadata: Dict[str, object] = {
|
|
||||||
"header_magic": magic.decode("ascii", "replace"),
|
|
||||||
"header_version": int(version),
|
|
||||||
"header_flags": int(flags),
|
|
||||||
"header_len": int(header_len),
|
|
||||||
"skb_mark": int(skb_mark),
|
|
||||||
"aux_value": int(aux_value),
|
|
||||||
"packet_id": packet_id_from_mark(skb_mark),
|
|
||||||
}
|
|
||||||
return metadata, frame[header_len:]
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
"""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")
|
|
||||||
|
|
||||||
|
|
||||||
def _run_checked(cmd: list[str]) -> subprocess.CompletedProcess[str]:
|
|
||||||
"""Run a command and surface stdout/stderr in raised errors."""
|
|
||||||
try:
|
|
||||||
return subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
||||||
except subprocess.CalledProcessError as exc:
|
|
||||||
stdout = (exc.stdout or "").strip()
|
|
||||||
stderr = (exc.stderr or "").strip()
|
|
||||||
if stdout:
|
|
||||||
logger.error("Command stdout for %s:\n%s", cmd[0], stdout)
|
|
||||||
if stderr:
|
|
||||||
logger.error("Command stderr for %s:\n%s", cmd[0], stderr)
|
|
||||||
detail = stderr or stdout or str(exc)
|
|
||||||
raise RuntimeError(detail) from exc
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
_run_checked(cmd)
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
_run_checked(cmd)
|
|
||||||
|
|
||||||
def _repo_root(self) -> Path:
|
|
||||||
return Path(__file__).resolve().parents[3]
|
|
||||||
|
|
||||||
|
|
||||||
capture_pipeline_manager = CapturePipelineManager()
|
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Emit bridge ingress, egress, and drop telemetry events via eBPF."""
|
"""Emit bridge ingress raw packets plus egress/drop telemetry via tc/eBPF."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import base64
|
||||||
import ctypes as ct
|
import ctypes as ct
|
||||||
import hashlib
|
import hashlib
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import signal
|
import signal
|
||||||
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
|
from pyroute2 import IPRoute
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from bcc import BPF # type: ignore
|
from bcc import BPF # type: ignore
|
||||||
except Exception as exc: # pragma: no cover - depends on host runtime
|
except Exception as exc: # pragma: no cover - depends on host runtime
|
||||||
@@ -23,6 +28,15 @@ except Exception as exc: # pragma: no cover - depends on host runtime
|
|||||||
from src.utilities.packet_mark import packet_id_from_mark, verdict_from_mark
|
from src.utilities.packet_mark import packet_id_from_mark, verdict_from_mark
|
||||||
|
|
||||||
|
|
||||||
|
EVENT_INGRESS = 1
|
||||||
|
EVENT_EGRESS = 2
|
||||||
|
EVENT_DROP = 3
|
||||||
|
INGRESS_PARENT = "ffff:fff2"
|
||||||
|
EGRESS_PARENT = "ffff:fff3"
|
||||||
|
MARK_FILTER_PREF = 10
|
||||||
|
INGRESS_FILTER_HANDLE = ":20"
|
||||||
|
EGRESS_FILTER_HANDLE = ":30"
|
||||||
|
|
||||||
IDENTITY_FIELDS = (
|
IDENTITY_FIELDS = (
|
||||||
"src_mac",
|
"src_mac",
|
||||||
"dst_mac",
|
"dst_mac",
|
||||||
@@ -38,9 +52,10 @@ IDENTITY_FIELDS = (
|
|||||||
|
|
||||||
BPF_SOURCE = r"""
|
BPF_SOURCE = r"""
|
||||||
#include <uapi/linux/ptrace.h>
|
#include <uapi/linux/ptrace.h>
|
||||||
|
#include <uapi/linux/pkt_cls.h>
|
||||||
|
#include <linux/bpf.h>
|
||||||
#include <linux/skbuff.h>
|
#include <linux/skbuff.h>
|
||||||
#include <linux/netdevice.h>
|
#include <linux/netdevice.h>
|
||||||
#include <linux/if.h>
|
|
||||||
#include <linux/if_ether.h>
|
#include <linux/if_ether.h>
|
||||||
#include <linux/ip.h>
|
#include <linux/ip.h>
|
||||||
#include <linux/ipv6.h>
|
#include <linux/ipv6.h>
|
||||||
@@ -48,7 +63,6 @@ BPF_SOURCE = r"""
|
|||||||
#include <linux/tcp.h>
|
#include <linux/tcp.h>
|
||||||
#include <linux/udp.h>
|
#include <linux/udp.h>
|
||||||
#include <linux/if_arp.h>
|
#include <linux/if_arp.h>
|
||||||
#include <linux/version.h>
|
|
||||||
|
|
||||||
#define EVENT_INGRESS 1
|
#define EVENT_INGRESS 1
|
||||||
#define EVENT_EGRESS 2
|
#define EVENT_EGRESS 2
|
||||||
@@ -71,6 +85,7 @@ struct event_t {
|
|||||||
__u32 skb_mark;
|
__u32 skb_mark;
|
||||||
__u32 length;
|
__u32 length;
|
||||||
__u32 reason;
|
__u32 reason;
|
||||||
|
__u32 ifindex;
|
||||||
__u16 eth_type_raw;
|
__u16 eth_type_raw;
|
||||||
__u16 vlan_id;
|
__u16 vlan_id;
|
||||||
__u16 src_port;
|
__u16 src_port;
|
||||||
@@ -78,31 +93,119 @@ struct event_t {
|
|||||||
__u32 protocol_raw;
|
__u32 protocol_raw;
|
||||||
__u8 event_type;
|
__u8 event_type;
|
||||||
__u8 ip_version;
|
__u8 ip_version;
|
||||||
char ifname[IFNAMSIZ];
|
__u8 reserved[2];
|
||||||
unsigned char src_mac[6];
|
unsigned char src_mac[6];
|
||||||
unsigned char dst_mac[6];
|
unsigned char dst_mac[6];
|
||||||
unsigned char src_ip[16];
|
unsigned char src_ip[16];
|
||||||
unsigned char dst_ip[16];
|
unsigned char dst_ip[16];
|
||||||
};
|
};
|
||||||
|
|
||||||
BPF_PERF_OUTPUT(events);
|
BPF_PERF_OUTPUT(ingress_events);
|
||||||
|
BPF_PERF_OUTPUT(meta_events);
|
||||||
|
|
||||||
static __always_inline int fill_ifname(struct sk_buff *skb, struct event_t *event) {
|
static __always_inline int parse_l3_l4_direct(struct event_t *event, void *data, void *data_end) {
|
||||||
if (!skb) {
|
struct ethhdr *eth = data;
|
||||||
|
__be16 eth_proto;
|
||||||
|
void *l3;
|
||||||
|
|
||||||
|
if ((void *)(eth + 1) > data_end) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct net_device *dev = NULL;
|
__builtin_memcpy(event->src_mac, eth->h_source, ETH_ALEN);
|
||||||
bpf_probe_read_kernel(&dev, sizeof(dev), &skb->dev);
|
__builtin_memcpy(event->dst_mac, eth->h_dest, ETH_ALEN);
|
||||||
if (!dev) {
|
eth_proto = eth->h_proto;
|
||||||
|
l3 = eth + 1;
|
||||||
|
|
||||||
|
if (eth_proto == htons(ETH_P_8021Q) || eth_proto == htons(ETH_P_8021AD)) {
|
||||||
|
struct vlan_hdr_t *vlan = l3;
|
||||||
|
if ((void *)(vlan + 1) > data_end) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
event->vlan_id = ntohs(vlan->h_vlan_TCI) & 0x0fff;
|
||||||
|
eth_proto = vlan->h_vlan_encapsulated_proto;
|
||||||
|
l3 = vlan + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
event->eth_type_raw = ntohs(eth_proto);
|
||||||
|
|
||||||
|
if (eth_proto == htons(ETH_P_ARP)) {
|
||||||
|
struct arphdr *arph = l3;
|
||||||
|
struct arp_eth_ipv4_t *body = (void *)(arph + 1);
|
||||||
|
if ((void *)(body + 1) > data_end) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (arph->ar_hrd == htons(ARPHRD_ETHER) && arph->ar_pro == htons(ETH_P_IP) &&
|
||||||
|
arph->ar_hln == ETH_ALEN && arph->ar_pln == 4) {
|
||||||
|
__builtin_memcpy(event->src_ip, body->spa, 4);
|
||||||
|
__builtin_memcpy(event->dst_ip, body->tpa, 4);
|
||||||
|
event->ip_version = 4;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eth_proto == htons(ETH_P_IP)) {
|
||||||
|
struct iphdr *iph = l3;
|
||||||
|
if ((void *)(iph + 1) > data_end) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bpf_probe_read_kernel(event->ifname, sizeof(event->ifname), dev->name);
|
event->ip_version = 4;
|
||||||
|
event->protocol_raw = iph->protocol;
|
||||||
|
__builtin_memcpy(event->src_ip, &iph->saddr, 4);
|
||||||
|
__builtin_memcpy(event->dst_ip, &iph->daddr, 4);
|
||||||
|
|
||||||
|
if (iph->protocol == IPPROTO_TCP) {
|
||||||
|
struct tcphdr *tcph = (void *)iph + (iph->ihl * 4);
|
||||||
|
if ((void *)(tcph + 1) > data_end) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
event->src_port = ntohs(tcph->source);
|
||||||
|
event->dst_port = ntohs(tcph->dest);
|
||||||
|
} else if (iph->protocol == IPPROTO_UDP) {
|
||||||
|
struct udphdr *udph = (void *)iph + (iph->ihl * 4);
|
||||||
|
if ((void *)(udph + 1) > data_end) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
event->src_port = ntohs(udph->source);
|
||||||
|
event->dst_port = ntohs(udph->dest);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eth_proto == htons(ETH_P_IPV6)) {
|
||||||
|
struct ipv6hdr *ip6h = l3;
|
||||||
|
if ((void *)(ip6h + 1) > data_end) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
event->ip_version = 6;
|
||||||
|
event->protocol_raw = ip6h->nexthdr;
|
||||||
|
__builtin_memcpy(event->src_ip, &ip6h->saddr, 16);
|
||||||
|
__builtin_memcpy(event->dst_ip, &ip6h->daddr, 16);
|
||||||
|
|
||||||
|
if (ip6h->nexthdr == IPPROTO_TCP) {
|
||||||
|
struct tcphdr *tcph = ip6h + 1;
|
||||||
|
if ((void *)(tcph + 1) > data_end) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
event->src_port = ntohs(tcph->source);
|
||||||
|
event->dst_port = ntohs(tcph->dest);
|
||||||
|
} else if (ip6h->nexthdr == IPPROTO_UDP) {
|
||||||
|
struct udphdr *udph = ip6h + 1;
|
||||||
|
if ((void *)(udph + 1) > data_end) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
event->src_port = ntohs(udph->source);
|
||||||
|
event->dst_port = ntohs(udph->dest);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static __always_inline int parse_skb(struct sk_buff *skb, struct event_t *event) {
|
static __always_inline int parse_skb_linear(struct event_t *event, struct sk_buff *skb) {
|
||||||
unsigned char *head = NULL;
|
unsigned char *head = NULL;
|
||||||
__u16 mac_header = 0;
|
__u16 mac_header = 0;
|
||||||
__u16 network_header = 0;
|
__u16 network_header = 0;
|
||||||
@@ -126,8 +229,8 @@ static __always_inline int parse_skb(struct sk_buff *skb, struct event_t *event)
|
|||||||
struct ethhdr eth = {};
|
struct ethhdr eth = {};
|
||||||
unsigned char *eth_ptr = head + mac_header;
|
unsigned char *eth_ptr = head + mac_header;
|
||||||
bpf_probe_read_kernel(ð, sizeof(eth), eth_ptr);
|
bpf_probe_read_kernel(ð, sizeof(eth), eth_ptr);
|
||||||
__builtin_memcpy(event->src_mac, eth.h_source, 6);
|
__builtin_memcpy(event->src_mac, eth.h_source, ETH_ALEN);
|
||||||
__builtin_memcpy(event->dst_mac, eth.h_dest, 6);
|
__builtin_memcpy(event->dst_mac, eth.h_dest, ETH_ALEN);
|
||||||
|
|
||||||
__be16 eth_proto = eth.h_proto;
|
__be16 eth_proto = eth.h_proto;
|
||||||
unsigned char *l3_ptr = head + network_header;
|
unsigned char *l3_ptr = head + network_header;
|
||||||
@@ -136,10 +239,8 @@ static __always_inline int parse_skb(struct sk_buff *skb, struct event_t *event)
|
|||||||
bpf_probe_read_kernel(&vlan, sizeof(vlan), eth_ptr + sizeof(struct ethhdr));
|
bpf_probe_read_kernel(&vlan, sizeof(vlan), eth_ptr + sizeof(struct ethhdr));
|
||||||
event->vlan_id = ntohs(vlan.h_vlan_TCI) & 0x0fff;
|
event->vlan_id = ntohs(vlan.h_vlan_TCI) & 0x0fff;
|
||||||
eth_proto = vlan.h_vlan_encapsulated_proto;
|
eth_proto = vlan.h_vlan_encapsulated_proto;
|
||||||
event->eth_type_raw = ntohs(eth_proto);
|
|
||||||
} else {
|
|
||||||
event->eth_type_raw = ntohs(eth_proto);
|
|
||||||
}
|
}
|
||||||
|
event->eth_type_raw = ntohs(eth_proto);
|
||||||
|
|
||||||
if (eth_proto == htons(ETH_P_ARP)) {
|
if (eth_proto == htons(ETH_P_ARP)) {
|
||||||
struct arphdr arph = {};
|
struct arphdr arph = {};
|
||||||
@@ -202,64 +303,76 @@ static __always_inline int parse_skb(struct sk_buff *skb, struct event_t *event)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static __always_inline int emit_event(struct pt_regs *ctx, struct sk_buff *skb, __u8 event_type, __u32 reason) {
|
int handle_ingress(struct __sk_buff *skb) {
|
||||||
struct event_t event = {};
|
struct event_t event = {};
|
||||||
|
void *data = (void *)(long)skb->data;
|
||||||
|
void *data_end = (void *)(long)skb->data_end;
|
||||||
|
|
||||||
event.ts_ns = bpf_ktime_get_ns();
|
event.ts_ns = bpf_ktime_get_ns();
|
||||||
event.event_type = event_type;
|
event.event_type = EVENT_INGRESS;
|
||||||
event.reason = reason;
|
event.ifindex = skb->ifindex;
|
||||||
|
event.length = skb->len;
|
||||||
|
event.skb_mark = skb->mark;
|
||||||
|
|
||||||
if (!fill_ifname(skb, &event)) {
|
if (!event.skb_mark) {
|
||||||
return 0;
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
if (!parse_skb(skb, &event)) {
|
if (!parse_l3_l4_direct(&event, data, data_end)) {
|
||||||
return 0;
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
events.perf_submit(ctx, &event, sizeof(event));
|
ingress_events.perf_submit_skb(skb, skb->len, &event, sizeof(event));
|
||||||
return 0;
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int trace_ingress(struct pt_regs *ctx, struct sk_buff *skb) {
|
int handle_egress(struct __sk_buff *skb) {
|
||||||
return emit_event(ctx, skb, EVENT_INGRESS, 0);
|
struct event_t event = {};
|
||||||
}
|
void *data = (void *)(long)skb->data;
|
||||||
|
void *data_end = (void *)(long)skb->data_end;
|
||||||
|
|
||||||
int trace_egress(struct pt_regs *ctx, struct sk_buff *skb) {
|
event.ts_ns = bpf_ktime_get_ns();
|
||||||
return emit_event(ctx, skb, EVENT_EGRESS, 0);
|
event.event_type = EVENT_EGRESS;
|
||||||
|
event.ifindex = skb->ifindex;
|
||||||
|
event.length = skb->len;
|
||||||
|
event.skb_mark = skb->mark;
|
||||||
|
|
||||||
|
if (!event.skb_mark) {
|
||||||
|
return TC_ACT_OK;
|
||||||
|
}
|
||||||
|
if (!parse_l3_l4_direct(&event, data, data_end)) {
|
||||||
|
return TC_ACT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
meta_events.perf_submit(skb, &event, sizeof(event));
|
||||||
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
TRACEPOINT_PROBE(skb, kfree_skb) {
|
TRACEPOINT_PROBE(skb, kfree_skb) {
|
||||||
struct sk_buff *skb = (struct sk_buff *)args->skbaddr;
|
struct sk_buff *skb = (struct sk_buff *)args->skbaddr;
|
||||||
struct event_t event = {};
|
struct event_t event = {};
|
||||||
|
struct net_device *dev = NULL;
|
||||||
|
|
||||||
event.ts_ns = bpf_ktime_get_ns();
|
event.ts_ns = bpf_ktime_get_ns();
|
||||||
event.event_type = EVENT_DROP;
|
event.event_type = EVENT_DROP;
|
||||||
event.reason = args->reason;
|
event.reason = args->reason;
|
||||||
|
|
||||||
if (!fill_ifname(skb, &event)) {
|
if (!skb) {
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (!parse_skb(skb, &event)) {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
events.perf_submit(args, &event, sizeof(event));
|
bpf_probe_read_kernel(&dev, sizeof(dev), &skb->dev);
|
||||||
return 0;
|
if (!dev) {
|
||||||
}
|
|
||||||
|
|
||||||
TRACEPOINT_PROBE(net, net_dev_queue) {
|
|
||||||
struct sk_buff *skb = (struct sk_buff *)args->skbaddr;
|
|
||||||
struct event_t event = {};
|
|
||||||
event.ts_ns = bpf_ktime_get_ns();
|
|
||||||
event.event_type = EVENT_EGRESS;
|
|
||||||
event.reason = 0;
|
|
||||||
|
|
||||||
if (!fill_ifname(skb, &event)) {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!parse_skb(skb, &event)) {
|
bpf_probe_read_kernel(&event.ifindex, sizeof(event.ifindex), &dev->ifindex);
|
||||||
|
if (!parse_skb_linear(&event, skb)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!event.skb_mark) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
events.perf_submit(args, &event, sizeof(event));
|
meta_events.perf_submit(args, &event, sizeof(event));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -271,6 +384,7 @@ class Event(ct.Structure):
|
|||||||
("skb_mark", ct.c_uint),
|
("skb_mark", ct.c_uint),
|
||||||
("length", ct.c_uint),
|
("length", ct.c_uint),
|
||||||
("reason", ct.c_uint),
|
("reason", ct.c_uint),
|
||||||
|
("ifindex", ct.c_uint),
|
||||||
("eth_type_raw", ct.c_ushort),
|
("eth_type_raw", ct.c_ushort),
|
||||||
("vlan_id", ct.c_ushort),
|
("vlan_id", ct.c_ushort),
|
||||||
("src_port", ct.c_ushort),
|
("src_port", ct.c_ushort),
|
||||||
@@ -278,7 +392,7 @@ class Event(ct.Structure):
|
|||||||
("protocol_raw", ct.c_uint),
|
("protocol_raw", ct.c_uint),
|
||||||
("event_type", ct.c_ubyte),
|
("event_type", ct.c_ubyte),
|
||||||
("ip_version", ct.c_ubyte),
|
("ip_version", ct.c_ubyte),
|
||||||
("ifname", ct.c_char * 16),
|
("reserved", ct.c_ubyte * 2),
|
||||||
("src_mac", ct.c_ubyte * 6),
|
("src_mac", ct.c_ubyte * 6),
|
||||||
("dst_mac", ct.c_ubyte * 6),
|
("dst_mac", ct.c_ubyte * 6),
|
||||||
("src_ip", ct.c_ubyte * 16),
|
("src_ip", ct.c_ubyte * 16),
|
||||||
@@ -286,6 +400,51 @@ class Event(ct.Structure):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
TARGET_INTERFACES: set[str] = set()
|
||||||
|
IPR: IPRoute | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _run_checked(cmd: list[str]) -> None:
|
||||||
|
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _multiarch_include_flag() -> list[str]:
|
||||||
|
if shutil.which("gcc") is None:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
triple = subprocess.run(["gcc", "-dumpmachine"], check=True, capture_output=True, text=True).stdout.strip()
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
include_dir = Path("/usr/include") / triple
|
||||||
|
return [f"-I{include_dir}"] if include_dir.is_dir() else []
|
||||||
|
|
||||||
|
|
||||||
|
def _repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[3]
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_mark_object(build_dir: str) -> Path:
|
||||||
|
build_path = Path(build_dir)
|
||||||
|
build_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
src = _repo_root() / "tools" / "ebpf" / "mark_packet_id.c"
|
||||||
|
obj = build_path / "mark_packet_id.o"
|
||||||
|
if obj.exists() and obj.stat().st_mtime >= src.stat().st_mtime:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
cmd = ["clang", "-O2", "-g", "-target", "bpf", *_multiarch_include_flag(), "-c", str(src), "-o", str(obj)]
|
||||||
|
_run_checked(cmd)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def _ifname_from_index(ifindex: int) -> str | None:
|
||||||
|
if ifindex <= 0:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return socket.if_indextoname(ifindex)
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _mac_to_str(value: Iterable[int]) -> str:
|
def _mac_to_str(value: Iterable[int]) -> str:
|
||||||
return ":".join(f"{byte:02x}" for byte in value)
|
return ":".join(f"{byte:02x}" for byte in value)
|
||||||
|
|
||||||
@@ -314,22 +473,20 @@ def _build_packet_uid(payload: dict[str, object]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _event_name(value: int) -> str:
|
def _event_name(value: int) -> str:
|
||||||
return {1: "ingress", 2: "egress", 3: "drop"}.get(value, "unknown")
|
return {EVENT_INGRESS: "ingress", EVENT_EGRESS: "egress", EVENT_DROP: "drop"}.get(value, "unknown")
|
||||||
|
|
||||||
|
|
||||||
def _reason_name(reason: int) -> str:
|
def _reason_name(reason: int) -> str:
|
||||||
return f"skb_drop_reason_{reason}"
|
return f"skb_drop_reason_{reason}"
|
||||||
|
|
||||||
|
|
||||||
def _emit_event(cpu: int, data: int, size: int) -> None:
|
def _build_payload(event: Event) -> dict[str, object] | None:
|
||||||
del cpu, size
|
iface = _ifname_from_index(int(event.ifindex))
|
||||||
event = ct.cast(data, ct.POINTER(Event)).contents
|
|
||||||
iface = bytes(event.ifname).split(b"\x00", 1)[0].decode("utf-8", "replace")
|
|
||||||
if iface not in TARGET_INTERFACES:
|
if iface not in TARGET_INTERFACES:
|
||||||
return
|
return None
|
||||||
|
|
||||||
payload: dict[str, object] = {
|
payload: dict[str, object] = {
|
||||||
"event_type": _event_name(event.event_type),
|
"event_type": _event_name(int(event.event_type)),
|
||||||
"iface": iface,
|
"iface": iface,
|
||||||
"skb_mark": int(event.skb_mark) or None,
|
"skb_mark": int(event.skb_mark) or None,
|
||||||
"length": int(event.length),
|
"length": int(event.length),
|
||||||
@@ -342,9 +499,10 @@ def _emit_event(cpu: int, data: int, size: int) -> None:
|
|||||||
"protocol_raw": int(event.protocol_raw) or None,
|
"protocol_raw": int(event.protocol_raw) or None,
|
||||||
"src_port": int(event.src_port) or None,
|
"src_port": int(event.src_port) or None,
|
||||||
"dst_port": int(event.dst_port) or None,
|
"dst_port": int(event.dst_port) or None,
|
||||||
"reason": _reason_name(int(event.reason)) if event.event_type == 3 else None,
|
"reason": _reason_name(int(event.reason)) if int(event.event_type) == EVENT_DROP else None,
|
||||||
"reason_code": int(event.reason) if event.event_type == 3 else None,
|
"reason_code": int(event.reason) if int(event.event_type) == EVENT_DROP else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
packet_id = packet_id_from_mark(payload.get("skb_mark"))
|
packet_id = packet_id_from_mark(payload.get("skb_mark"))
|
||||||
if packet_id:
|
if packet_id:
|
||||||
payload["packet_id"] = packet_id
|
payload["packet_id"] = packet_id
|
||||||
@@ -357,67 +515,37 @@ def _emit_event(cpu: int, data: int, size: int) -> None:
|
|||||||
payload["packet_uid"] = _build_packet_uid(payload)
|
payload["packet_uid"] = _build_packet_uid(payload)
|
||||||
payload["correlation_key"] = f"uid:{payload['packet_uid']}"
|
payload["correlation_key"] = f"uid:{payload['packet_uid']}"
|
||||||
payload["correlation_source"] = "legacy_hash"
|
payload["correlation_source"] = "legacy_hash"
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_ingress_event(cpu: int, data: int, size: int) -> None:
|
||||||
|
del cpu
|
||||||
|
event = ct.cast(data, ct.POINTER(Event)).contents
|
||||||
|
payload = _build_payload(event)
|
||||||
|
if payload is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
raw_size = size - ct.sizeof(Event)
|
||||||
|
if raw_size > 0:
|
||||||
|
raw = ct.string_at(data + ct.sizeof(Event), raw_size)
|
||||||
|
payload["raw_b64"] = base64.b64encode(raw).decode("ascii")
|
||||||
|
|
||||||
print(json.dumps(payload, separators=(",", ":")), flush=True)
|
print(json.dumps(payload, separators=(",", ":")), flush=True)
|
||||||
|
|
||||||
|
|
||||||
def _attach_kprobe_first(bpf: BPF, symbols: list[str], fn_name: str) -> str:
|
def _emit_meta_event(cpu: int, data: int, size: int) -> None:
|
||||||
supported = _supported_kprobe_symbols(symbols)
|
del cpu, size
|
||||||
if not supported:
|
event = ct.cast(data, ct.POINTER(Event)).contents
|
||||||
raise RuntimeError(f"No supported kprobe symbols found for {fn_name}: {symbols}")
|
payload = _build_payload(event)
|
||||||
|
if payload is None:
|
||||||
for symbol in supported:
|
return
|
||||||
try:
|
print(json.dumps(payload, separators=(",", ":")), flush=True)
|
||||||
bpf.attach_kprobe(event=symbol, fn_name=fn_name)
|
|
||||||
return symbol
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
raise RuntimeError(f"Failed to attach {fn_name} to any of {supported}")
|
|
||||||
|
|
||||||
|
|
||||||
def _supported_kprobe_symbols(symbols: list[str]) -> list[str]:
|
|
||||||
try:
|
|
||||||
available = set()
|
|
||||||
for symbol in symbols:
|
|
||||||
for candidate in BPF.get_kprobe_functions(symbol.encode()):
|
|
||||||
decoded = candidate.decode("utf-8", "replace")
|
|
||||||
if decoded == symbol:
|
|
||||||
available.add(symbol)
|
|
||||||
if available:
|
|
||||||
return [symbol for symbol in symbols if symbol in available]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if os.path.exists("/proc/kallsyms"):
|
|
||||||
try:
|
|
||||||
with open("/proc/kallsyms", "r", encoding="utf-8", errors="replace") as handle:
|
|
||||||
names = {line.rsplit(" ", 1)[-1].strip() for line in handle}
|
|
||||||
return [symbol for symbol in symbols if symbol in names]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return symbols
|
|
||||||
|
|
||||||
|
|
||||||
def _attach_egress_probe(bpf: BPF) -> str:
|
|
||||||
symbols = ["__dev_queue_xmit", "dev_queue_xmit"]
|
|
||||||
try:
|
|
||||||
return _attach_kprobe_first(bpf, symbols, "trace_egress")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
bpf.attach_tracepoint(tp="net:net_dev_queue", fn_name="tracepoint__net__net_dev_queue")
|
|
||||||
return "tracepoint:net:net_dev_queue"
|
|
||||||
except Exception as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Failed to attach egress telemetry to any of "
|
|
||||||
f"{symbols} or tracepoint net:net_dev_queue"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_args() -> argparse.Namespace:
|
def _parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description="eBPF bridge telemetry collector")
|
parser = argparse.ArgumentParser(description="tc/eBPF bridge telemetry collector")
|
||||||
parser.add_argument("--ifaces", required=True, help="Comma-separated list of interfaces to keep")
|
parser.add_argument("--ifaces", required=True, help="Comma-separated list of interfaces to instrument")
|
||||||
|
parser.add_argument("--build-dir", required=True, help="Directory for compiled tc BPF objects")
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -425,47 +553,131 @@ def _sigterm(_signum: int, _frame: object) -> None:
|
|||||||
raise KeyboardInterrupt
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_clean_clsact(iface: str) -> None:
|
||||||
|
subprocess.run(["tc", "qdisc", "del", "dev", iface, "clsact"], check=False, capture_output=True, text=True)
|
||||||
|
_run_checked(["tc", "qdisc", "add", "dev", iface, "clsact"])
|
||||||
|
|
||||||
|
|
||||||
|
def _attach_mark_filter(iface: str, obj_path: Path) -> None:
|
||||||
|
_run_checked(
|
||||||
|
[
|
||||||
|
"tc",
|
||||||
|
"filter",
|
||||||
|
"replace",
|
||||||
|
"dev",
|
||||||
|
iface,
|
||||||
|
"ingress",
|
||||||
|
"pref",
|
||||||
|
str(MARK_FILTER_PREF),
|
||||||
|
"protocol",
|
||||||
|
"all",
|
||||||
|
"bpf",
|
||||||
|
"direct-action",
|
||||||
|
"obj",
|
||||||
|
str(obj_path),
|
||||||
|
"sec",
|
||||||
|
"classifier",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _attach_tc_programs(bpf: BPF, ifaces: list[str], build_dir: str) -> tuple[str, str]:
|
||||||
|
global IPR
|
||||||
|
obj_path = _ensure_mark_object(build_dir)
|
||||||
|
ingress_fn = bpf.load_func("handle_ingress", BPF.SCHED_CLS)
|
||||||
|
egress_fn = bpf.load_func("handle_egress", BPF.SCHED_CLS)
|
||||||
|
|
||||||
|
ipr = IPRoute()
|
||||||
|
try:
|
||||||
|
for iface in ifaces:
|
||||||
|
matches = ipr.link_lookup(ifname=iface)
|
||||||
|
if not matches:
|
||||||
|
raise RuntimeError(f"Interface not found: {iface}")
|
||||||
|
ifindex = matches[0]
|
||||||
|
_ensure_clean_clsact(iface)
|
||||||
|
_attach_mark_filter(iface, obj_path)
|
||||||
|
ipr.tc(
|
||||||
|
"add-filter",
|
||||||
|
"bpf",
|
||||||
|
ifindex,
|
||||||
|
INGRESS_FILTER_HANDLE,
|
||||||
|
fd=ingress_fn.fd,
|
||||||
|
name=ingress_fn.name,
|
||||||
|
parent=INGRESS_PARENT,
|
||||||
|
classid=1,
|
||||||
|
direct_action=True,
|
||||||
|
)
|
||||||
|
ipr.tc(
|
||||||
|
"add-filter",
|
||||||
|
"bpf",
|
||||||
|
ifindex,
|
||||||
|
EGRESS_FILTER_HANDLE,
|
||||||
|
fd=egress_fn.fd,
|
||||||
|
name=egress_fn.name,
|
||||||
|
parent=EGRESS_PARENT,
|
||||||
|
classid=1,
|
||||||
|
direct_action=True,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
for iface in ifaces:
|
||||||
|
subprocess.run(["tc", "qdisc", "del", "dev", iface, "clsact"], check=False, capture_output=True, text=True)
|
||||||
|
if ipr is not None:
|
||||||
|
ipr.close()
|
||||||
|
IPR = None
|
||||||
|
raise
|
||||||
|
|
||||||
|
return ingress_fn.name, egress_fn.name
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_tc(ifaces: Iterable[str]) -> None:
|
||||||
|
for iface in ifaces:
|
||||||
|
subprocess.run(["tc", "qdisc", "del", "dev", iface, "clsact"], check=False, capture_output=True, text=True)
|
||||||
|
global IPR
|
||||||
|
if IPR is not None:
|
||||||
|
try:
|
||||||
|
IPR.close()
|
||||||
|
finally:
|
||||||
|
IPR = None
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = _parse_args()
|
args = _parse_args()
|
||||||
global TARGET_INTERFACES
|
global TARGET_INTERFACES
|
||||||
TARGET_INTERFACES = {iface.strip() for iface in args.ifaces.split(",") if iface.strip()}
|
TARGET_INTERFACES = {iface.strip() for iface in args.ifaces.split(",") if iface.strip()}
|
||||||
if not TARGET_INTERFACES:
|
if not TARGET_INTERFACES:
|
||||||
print("No interfaces provided", file=sys.stderr)
|
print("No interfaces provided", file=sys.stderr, flush=True)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, _sigterm)
|
signal.signal(signal.SIGTERM, _sigterm)
|
||||||
signal.signal(signal.SIGINT, _sigterm)
|
signal.signal(signal.SIGINT, _sigterm)
|
||||||
|
|
||||||
bpf = BPF(text=BPF_SOURCE)
|
bpf = BPF(text=BPF_SOURCE)
|
||||||
ingress_symbol = _attach_kprobe_first(
|
ingress_prog_name = ""
|
||||||
bpf,
|
egress_prog_name = ""
|
||||||
["__netif_receive_skb_core", "netif_receive_skb", "__netif_receive_skb_one_core"],
|
try:
|
||||||
"trace_ingress",
|
ingress_prog_name, egress_prog_name = _attach_tc_programs(bpf, sorted(TARGET_INTERFACES), args.build_dir)
|
||||||
)
|
|
||||||
egress_symbol = _attach_egress_probe(bpf)
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"status": "collector_started",
|
"status": "collector_started",
|
||||||
"ifaces": sorted(TARGET_INTERFACES),
|
"ifaces": sorted(TARGET_INTERFACES),
|
||||||
"ingress_symbol": ingress_symbol,
|
"ingress_program": ingress_prog_name,
|
||||||
"egress_symbol": egress_symbol,
|
"egress_program": egress_prog_name,
|
||||||
|
"build_dir": args.build_dir,
|
||||||
},
|
},
|
||||||
separators=(",", ":"),
|
separators=(",", ":"),
|
||||||
),
|
),
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
bpf["events"].open_perf_buffer(_emit_event, page_cnt=128)
|
bpf["ingress_events"].open_perf_buffer(_emit_ingress_event, page_cnt=256)
|
||||||
try:
|
bpf["meta_events"].open_perf_buffer(_emit_meta_event, page_cnt=128)
|
||||||
while True:
|
while True:
|
||||||
bpf.perf_buffer_poll()
|
bpf.perf_buffer_poll()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
return 0
|
return 0
|
||||||
|
finally:
|
||||||
|
_cleanup_tc(sorted(TARGET_INTERFACES))
|
||||||
TARGET_INTERFACES: set[str] = set()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class PacketTracker:
|
|||||||
self._entries: Dict[str, Dict[str, Any]] = {}
|
self._entries: Dict[str, Dict[str, Any]] = {}
|
||||||
self._stats: Dict[str, int] = {
|
self._stats: Dict[str, int] = {
|
||||||
"persisted_total": 0,
|
"persisted_total": 0,
|
||||||
"persisted_af_packet_only": 0,
|
"persisted_capture_only": 0,
|
||||||
"persisted_telemetry_only": 0,
|
"persisted_telemetry_only": 0,
|
||||||
"persisted_merged": 0,
|
"persisted_merged": 0,
|
||||||
"persisted_with_raw": 0,
|
"persisted_with_raw": 0,
|
||||||
@@ -60,7 +60,7 @@ class PacketTracker:
|
|||||||
now_ts = time.time()
|
now_ts = time.time()
|
||||||
correlation_key = self._ensure_correlation(pkt_info)
|
correlation_key = self._ensure_correlation(pkt_info)
|
||||||
pkt_info["raw_present"] = pkt_info.get("raw") is not None
|
pkt_info["raw_present"] = pkt_info.get("raw") is not None
|
||||||
pkt_info["capture_sources"] = ["af_packet"]
|
pkt_info["capture_sources"] = [pkt_info.get("capture_source") or "af_packet"]
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
entry = self._entries.get(correlation_key)
|
entry = self._entries.get(correlation_key)
|
||||||
@@ -205,9 +205,9 @@ class PacketTracker:
|
|||||||
def _merge_packet_info(self, entry: Dict[str, Any], pkt_info: Dict[str, Any], now_ts: float) -> None:
|
def _merge_packet_info(self, entry: Dict[str, Any], pkt_info: Dict[str, Any], now_ts: float) -> None:
|
||||||
payload = entry["payload"]
|
payload = entry["payload"]
|
||||||
changed = False
|
changed = False
|
||||||
self._add_capture_source(payload, "af_packet")
|
self._add_capture_source(payload, pkt_info.get("capture_source") or "af_packet")
|
||||||
for key, value in pkt_info.items():
|
for key, value in pkt_info.items():
|
||||||
if key == "iface":
|
if key in {"iface", "capture_source"}:
|
||||||
continue
|
continue
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
@@ -382,8 +382,8 @@ class PacketTracker:
|
|||||||
else:
|
else:
|
||||||
self._stats["persisted_legacy_hash"] = self._stats.get("persisted_legacy_hash", 0) + 1
|
self._stats["persisted_legacy_hash"] = self._stats.get("persisted_legacy_hash", 0) + 1
|
||||||
|
|
||||||
if capture_sources == {"af_packet"}:
|
if capture_sources and capture_sources != {"telemetry"} and "telemetry" not in capture_sources:
|
||||||
self._stats["persisted_af_packet_only"] += 1
|
self._stats["persisted_capture_only"] += 1
|
||||||
elif capture_sources == {"telemetry"}:
|
elif capture_sources == {"telemetry"}:
|
||||||
self._stats["persisted_telemetry_only"] += 1
|
self._stats["persisted_telemetry_only"] += 1
|
||||||
else:
|
else:
|
||||||
@@ -397,15 +397,15 @@ class PacketTracker:
|
|||||||
active_total = len(active_entries)
|
active_total = len(active_entries)
|
||||||
active_with_raw = sum(1 for entry in active_entries if entry["payload"].get("raw_present"))
|
active_with_raw = sum(1 for entry in active_entries if entry["payload"].get("raw_present"))
|
||||||
active_without_raw = active_total - active_with_raw
|
active_without_raw = active_total - active_with_raw
|
||||||
active_af_packet_only = 0
|
active_capture_only = 0
|
||||||
active_telemetry_only = 0
|
active_telemetry_only = 0
|
||||||
active_merged = 0
|
active_merged = 0
|
||||||
active_kernel_mark = 0
|
active_kernel_mark = 0
|
||||||
active_legacy_hash = 0
|
active_legacy_hash = 0
|
||||||
for entry in active_entries:
|
for entry in active_entries:
|
||||||
capture_sources = set(entry["payload"].get("capture_sources") or [])
|
capture_sources = set(entry["payload"].get("capture_sources") or [])
|
||||||
if capture_sources == {"af_packet"}:
|
if capture_sources and capture_sources != {"telemetry"} and "telemetry" not in capture_sources:
|
||||||
active_af_packet_only += 1
|
active_capture_only += 1
|
||||||
elif capture_sources == {"telemetry"}:
|
elif capture_sources == {"telemetry"}:
|
||||||
active_telemetry_only += 1
|
active_telemetry_only += 1
|
||||||
else:
|
else:
|
||||||
@@ -419,7 +419,7 @@ class PacketTracker:
|
|||||||
"active_total": active_total,
|
"active_total": active_total,
|
||||||
"active_with_raw": active_with_raw,
|
"active_with_raw": active_with_raw,
|
||||||
"active_without_raw": active_without_raw,
|
"active_without_raw": active_without_raw,
|
||||||
"active_af_packet_only": active_af_packet_only,
|
"active_capture_only": active_capture_only,
|
||||||
"active_telemetry_only": active_telemetry_only,
|
"active_telemetry_only": active_telemetry_only,
|
||||||
"active_merged": active_merged,
|
"active_merged": active_merged,
|
||||||
"active_kernel_mark": active_kernel_mark,
|
"active_kernel_mark": active_kernel_mark,
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
#include <linux/bpf.h>
|
|
||||||
#include <linux/pkt_cls.h>
|
|
||||||
#include <bpf/bpf_endian.h>
|
|
||||||
#include <bpf/bpf_helpers.h>
|
|
||||||
|
|
||||||
#define CAPTURE_MAGIC 0x4d544350
|
|
||||||
#define CAPTURE_VERSION 1
|
|
||||||
|
|
||||||
struct capture_header {
|
|
||||||
__be32 magic;
|
|
||||||
__u8 version;
|
|
||||||
__u8 flags;
|
|
||||||
__be16 header_len;
|
|
||||||
__be32 skb_mark;
|
|
||||||
__be32 aux_value;
|
|
||||||
} __attribute__((packed));
|
|
||||||
|
|
||||||
SEC("classifier")
|
|
||||||
int prepend_capture_header_prog(struct __sk_buff *skb) {
|
|
||||||
struct capture_header hdr = {
|
|
||||||
.magic = bpf_htonl(CAPTURE_MAGIC),
|
|
||||||
.version = CAPTURE_VERSION,
|
|
||||||
.flags = 0,
|
|
||||||
.header_len = bpf_htons(sizeof(struct capture_header)),
|
|
||||||
.skb_mark = bpf_htonl(skb->mark),
|
|
||||||
.aux_value = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!skb->mark) {
|
|
||||||
return TC_ACT_OK;
|
|
||||||
}
|
|
||||||
if (bpf_skb_adjust_room(skb, sizeof(hdr), BPF_ADJ_ROOM_MAC, 0)) {
|
|
||||||
return TC_ACT_OK;
|
|
||||||
}
|
|
||||||
if (bpf_skb_store_bytes(skb, 0, &hdr, sizeof(hdr), 0)) {
|
|
||||||
return TC_ACT_OK;
|
|
||||||
}
|
|
||||||
return TC_ACT_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
char LICENSE[] SEC("license") = "GPL";
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: tools/setup_bridge_capture.sh --bridge <bridge> [--mirror-if mitmcap0] [--capture-if mitmcap1] [--build-dir /tmp/mitm-bpf]
|
|
||||||
|
|
||||||
Sets up:
|
|
||||||
1. A veth pair used as a capture mirror target
|
|
||||||
2. tc ingress packet-id marking on each bridge slave
|
|
||||||
3. tc mirroring from each bridge slave into the mirror interface
|
|
||||||
4. A capture-header injector on the capture-side interface
|
|
||||||
|
|
||||||
Set BACKEND_CAPTURE_INTERFACE to the capture interface printed at the end.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
BRIDGE=""
|
|
||||||
MIRROR_IF="mitmcap0"
|
|
||||||
CAPTURE_IF="mitmcap1"
|
|
||||||
BUILD_DIR="/tmp/mitm-bpf"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--bridge) BRIDGE="$2"; shift 2 ;;
|
|
||||||
--mirror-if) MIRROR_IF="$2"; shift 2 ;;
|
|
||||||
--capture-if) CAPTURE_IF="$2"; shift 2 ;;
|
|
||||||
--build-dir) BUILD_DIR="$2"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$BRIDGE" ]]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v tc >/dev/null 2>&1 || ! command -v clang >/dev/null 2>&1 || ! command -v ip >/dev/null 2>&1 || ! command -v bridge >/dev/null 2>&1; then
|
|
||||||
echo "Missing required tools: tc, clang, ip, and bridge must be installed." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! ip link show "$BRIDGE" >/dev/null 2>&1; then
|
|
||||||
echo "Bridge interface not found: $BRIDGE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$BUILD_DIR"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
SRC_DIR="$SCRIPT_DIR/ebpf"
|
|
||||||
MARK_OBJ="$BUILD_DIR/mark_packet_id.o"
|
|
||||||
CAPTURE_OBJ="$BUILD_DIR/prepend_capture_header.o"
|
|
||||||
MULTIARCH_INCLUDE=""
|
|
||||||
if command -v gcc >/dev/null 2>&1; then
|
|
||||||
GCC_TRIPLE="$(gcc -dumpmachine 2>/dev/null || true)"
|
|
||||||
if [[ -n "$GCC_TRIPLE" && -d "/usr/include/$GCC_TRIPLE" ]]; then
|
|
||||||
MULTIARCH_INCLUDE="-I/usr/include/$GCC_TRIPLE"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
clang -O2 -g -target bpf ${MULTIARCH_INCLUDE:+$MULTIARCH_INCLUDE} -c "$SRC_DIR/mark_packet_id.c" -o "$MARK_OBJ"
|
|
||||||
clang -O2 -g -target bpf ${MULTIARCH_INCLUDE:+$MULTIARCH_INCLUDE} -c "$SRC_DIR/prepend_capture_header.c" -o "$CAPTURE_OBJ"
|
|
||||||
|
|
||||||
if ! ip link show "$MIRROR_IF" >/dev/null 2>&1; then
|
|
||||||
ip link add "$MIRROR_IF" type veth peer name "$CAPTURE_IF"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ip link set "$MIRROR_IF" up
|
|
||||||
ip link set "$CAPTURE_IF" up
|
|
||||||
sysctl -q -w "net.ipv6.conf.$MIRROR_IF.disable_ipv6=1" >/dev/null || true
|
|
||||||
sysctl -q -w "net.ipv6.conf.$CAPTURE_IF.disable_ipv6=1" >/dev/null || true
|
|
||||||
|
|
||||||
mapfile -t PORTS < <(bridge link show master "$BRIDGE" | awk -F': ' '{print $2}' | awk '{print $1}')
|
|
||||||
if [[ ${#PORTS[@]} -eq 0 ]]; then
|
|
||||||
echo "No bridge slave interfaces found for $BRIDGE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
for port in "${PORTS[@]}"; do
|
|
||||||
tc qdisc replace dev "$port" clsact
|
|
||||||
tc filter replace dev "$port" ingress pref 10 protocol all bpf direct-action obj "$MARK_OBJ" sec classifier
|
|
||||||
tc filter replace dev "$port" ingress pref 20 protocol all matchall action mirred egress mirror dev "$MIRROR_IF"
|
|
||||||
done
|
|
||||||
|
|
||||||
tc qdisc replace dev "$MIRROR_IF" clsact
|
|
||||||
tc filter replace dev "$MIRROR_IF" egress pref 10 protocol all bpf direct-action obj "$CAPTURE_OBJ" sec classifier
|
|
||||||
|
|
||||||
cat <<EOF
|
|
||||||
Bridge capture pipeline ready.
|
|
||||||
Bridge: $BRIDGE
|
|
||||||
Bridge slave ports: ${PORTS[*]}
|
|
||||||
Mirror tx interface: $MIRROR_IF
|
|
||||||
Capture interface: $CAPTURE_IF
|
|
||||||
|
|
||||||
Set this in backend/.env:
|
|
||||||
BACKEND_CAPTURE_INTERFACE=$CAPTURE_IF
|
|
||||||
EOF
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Usage: tools/teardown_bridge_capture.sh --bridge <bridge> [--mirror-if mitmcap0] [--capture-if mitmcap1]"
|
|
||||||
}
|
|
||||||
|
|
||||||
BRIDGE=""
|
|
||||||
MIRROR_IF="mitmcap0"
|
|
||||||
CAPTURE_IF="mitmcap1"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--bridge) BRIDGE="$2"; shift 2 ;;
|
|
||||||
--mirror-if) MIRROR_IF="$2"; shift 2 ;;
|
|
||||||
--capture-if) CAPTURE_IF="$2"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -n "$BRIDGE" ]] && ip link show "$BRIDGE" >/dev/null 2>&1; then
|
|
||||||
while read -r port; do
|
|
||||||
[[ -n "$port" ]] || continue
|
|
||||||
tc qdisc del dev "$port" clsact 2>/dev/null || true
|
|
||||||
done < <(bridge link show master "$BRIDGE" | awk -F': ' '{print $2}' | awk '{print $1}')
|
|
||||||
fi
|
|
||||||
|
|
||||||
tc qdisc del dev "$MIRROR_IF" clsact 2>/dev/null || true
|
|
||||||
tc qdisc del dev "$CAPTURE_IF" clsact 2>/dev/null || true
|
|
||||||
|
|
||||||
if ip link show "$MIRROR_IF" >/dev/null 2>&1; then
|
|
||||||
ip link del "$MIRROR_IF"
|
|
||||||
fi
|
|
||||||
Reference in New Issue
Block a user