diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/api/sniffer_api.py b/backend/src/api/sniffer_api.py index bac0683..3da4566 100644 --- a/backend/src/api/sniffer_api.py +++ b/backend/src/api/sniffer_api.py @@ -1,4 +1,4 @@ -"""HTTP API for starting, stopping, and inspecting sniffer sessions.""" +"""HTTP API for starting, stopping, and inspecting packet capture sessions.""" from typing import Any, Dict, Optional @@ -7,16 +7,16 @@ from pydantic import BaseModel, Field from src.network_sniffer import ( get_internal_debug_state, - get_sniffer_status, - start_afpacket_sniffer, - stop_afpacket_sniffer, + get_capture_session_status, + start_capture_session, + stop_capture_session, ) router = APIRouter() class SnifferStartRequest(BaseModel): - """Request payload for starting a sniffer session.""" + """Request payload for starting a packet capture session.""" bridge: Optional[str] = Field( None, @@ -60,7 +60,7 @@ class InterfaceSnifferStatus(BaseModel): running: bool = Field(..., description="Whether a sniffer is currently active.") exists: bool = Field(..., description="Whether the interface exists on the host.") up: bool = Field(..., description="Whether the interface is operationally up.") - session_id: Optional[str] = Field(None, description="Owning sniffer session ID.") + session_id: Optional[str] = Field(None, description="Owning capture session ID.") session_label: Optional[str] = Field(None, description="Human-readable session label.") @@ -75,13 +75,13 @@ class SnifferStatusResponse(BaseModel): @router.post("/start", response_model=SnifferStartResponse) def sniffer_start(req: SnifferStartRequest) -> SnifferStartResponse: - """Start one sniffer session for exactly one target.""" + """Start one packet capture session for exactly one target.""" if bool(req.bridge) == bool(req.interface): raise HTTPException(status_code=400, detail="Exactly one of 'bridge' or 'interface' must be provided") try: if req.interface: - session_id = start_afpacket_sniffer(req.interface, target_is_interface=True) + session_id = start_capture_session(req.interface, target_is_interface=True) return SnifferStartResponse( started=True, session_id=session_id, @@ -89,7 +89,7 @@ def sniffer_start(req: SnifferStartRequest) -> SnifferStartResponse: target_type="interface", ) - session_id = start_afpacket_sniffer(req.bridge, target_is_interface=False) + session_id = start_capture_session(req.bridge, target_is_interface=False) return SnifferStartResponse( started=True, session_id=session_id, @@ -117,7 +117,7 @@ def sniffer_stop( """Stop by session ID, target query, or globally when no selector is given.""" if body and body.session_id: try: - stop_afpacket_sniffer(session_id=body.session_id) + stop_capture_session(session_id=body.session_id) return SnifferStopResponse(stopped=True, session_id=body.session_id, target=None, target_type=None) except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to stop session {body.session_id}: {exc}") from exc @@ -127,14 +127,14 @@ def sniffer_stop( try: if q_interface: - stop_afpacket_sniffer(target=q_interface, target_is_interface=True) + stop_capture_session(target=q_interface, target_is_interface=True) return SnifferStopResponse(stopped=True, session_id=None, target=q_interface, target_type="interface") if q_bridge: - stop_afpacket_sniffer(target=q_bridge, target_is_interface=False) + stop_capture_session(target=q_bridge, target_is_interface=False) return SnifferStopResponse(stopped=True, session_id=None, target=q_bridge, target_type="bridge") - stop_afpacket_sniffer() + stop_capture_session() return SnifferStopResponse(stopped=True, session_id=None, target=None, target_type=None) except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to stop sniffer: {exc}") from exc @@ -142,9 +142,9 @@ def sniffer_stop( @router.get("/status", response_model=SnifferStatusResponse) def sniffer_status() -> SnifferStatusResponse: - """Return current sniffer status per interface.""" + """Return current capture-session status per interface.""" try: - raw: Dict[str, Dict[str, Any]] = get_sniffer_status() + raw: Dict[str, Dict[str, Any]] = get_capture_session_status() typed = {key: InterfaceSnifferStatus(**value) for key, value in raw.items()} return SnifferStatusResponse(interfaces=typed) except Exception as exc: @@ -153,7 +153,7 @@ def sniffer_status() -> SnifferStatusResponse: @router.get("/debug") def sniffer_debug() -> Dict[str, Any]: - """Return internal sniffer and packet-tracker debug state.""" + """Return internal capture-session and packet-tracker debug state.""" try: return get_internal_debug_state() except Exception as exc: diff --git a/backend/src/main.py b/backend/src/main.py index fe0ecfe..3e288e2 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -82,11 +82,11 @@ async def on_startup() -> None: async def shutdown_event() -> None: """Stop network resources and release shared runtime objects.""" try: - from src.network_sniffer import stop_afpacket_sniffer + from src.network_sniffer import stop_capture_session - stop_afpacket_sniffer() + stop_capture_session() except Exception: - logging.exception("Failed to stop sniffer sessions during shutdown") + logging.exception("Failed to stop capture sessions during shutdown") try: network_api.shutdown_network_api() diff --git a/backend/src/network_sniffer.py b/backend/src/network_sniffer.py index aeae75e..1310ff5 100644 --- a/backend/src/network_sniffer.py +++ b/backend/src/network_sniffer.py @@ -41,7 +41,7 @@ from src.Models.etherType import EtherTypeEnum, ethertype_from_int from src.Models.ip_protocol import IPProtocolEnum, protocol_from_number # ---- Logging ---------------------------------------------------------- -logger = logging.getLogger("af_packet_sniffer") +logger = logging.getLogger("packet_capture") # ---- Session model ------------------------------------------- # sessions: session_id -> session dict @@ -471,11 +471,11 @@ def parse_packet_bytes( # ------------------------- -# AF_PACKET socket utilities +# Raw socket utilities for interface capture # ------------------------- def _create_af_packet_socket(ifname: str, rx_buf: Optional[int] = None) -> Optional[socket.socket]: """ - Create and bind an AF_PACKET raw socket to interface. + Create and bind an AF_PACKET raw socket for direct interface capture. Non-blocking socket returned or None on failure. """ try: @@ -677,12 +677,13 @@ def _session_reader_loop(session_id: str) -> None: # ------------------------- # Public API: start/stop/status # ------------------------- -def start_afpacket_sniffer(target: str, target_is_interface: bool = False) -> str: +def start_capture_session(target: str, target_is_interface: bool = False) -> str: """ - Start a sniffer session. Returns session_id string. + Start a packet capture session. Returns session_id string. - If target_is_interface == False: target is treated as bridge name and ports are snapshotted. - If target_is_interface == True: target is treated as interface name. + If target_is_interface == True, capture uses an AF_PACKET raw socket on that interface. + If target_is_interface == False, target is treated as a bridge and capture uses the + tc/eBPF bridge telemetry path for the bridge ports. """ session_id = str(uuid4()) session: Dict[str, Any] = { @@ -728,7 +729,7 @@ def start_afpacket_sniffer(target: str, target_is_interface: bool = False) -> st session["thread"] = None _sync_bridge_telemetry() logger.info( - "Started sniffer session %s label=%s ports=%s capture_ifaces=%s", + "Started capture session %s label=%s ports=%s capture_ifaces=%s", session_id, target, ports, @@ -737,7 +738,11 @@ def start_afpacket_sniffer(target: str, target_is_interface: bool = False) -> st return session_id -def stop_afpacket_sniffer(session_id: Optional[str] = None, target: Optional[str] = None, target_is_interface: bool = False) -> None: +def stop_capture_session( + session_id: Optional[str] = None, + target: Optional[str] = None, + target_is_interface: bool = False, +) -> None: """ Stop either a specific session by session_id (preferred), or stop sockets associated with target. - If session_id provided: stop that session and clean up. @@ -770,10 +775,10 @@ 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 sid, s in list(sessions.items()): if not target_is_interface and s.get("is_bridge") and s.get("label") == target: - stop_afpacket_sniffer(session_id=sid) + stop_capture_session(session_id=sid) continue if target_is_interface and not s.get("is_bridge") and s.get("label") == target: - stop_afpacket_sniffer(session_id=sid) + stop_capture_session(session_id=sid) continue if target in s.get("ports", []): # close only that iface socket in that session @@ -797,7 +802,7 @@ def stop_afpacket_sniffer(session_id: Optional[str] = None, target: Optional[str # Global stop: stop all sessions for sid in list(sessions.keys()): - stop_afpacket_sniffer(session_id=sid) + stop_capture_session(session_id=sid) # attempt to close db pool if shared.web_loop is available; otherwise leave to main try: @@ -808,14 +813,14 @@ def stop_afpacket_sniffer(session_id: Optional[str] = None, target: Optional[str except Exception: logger.exception("Failed to schedule DB pool close") - logger.info("All sniffer sessions stopped") + logger.info("All capture sessions stopped") try: bridge_telemetry_manager.stop() except Exception: logger.exception("Failed to stop bridge telemetry collector") -def get_sniffer_status() -> Dict[str, Dict[str, object]]: +def get_capture_session_status() -> Dict[str, Dict[str, object]]: """ Return simple status per managed interface, including which session owns it. Output format: @@ -870,3 +875,22 @@ def get_internal_debug_state() -> dict: "tshark": tshark_manager.get_debug_snapshot(), "packet_tracker": packet_tracker.get_debug_snapshot(), } + + +def start_afpacket_sniffer(target: str, target_is_interface: bool = False) -> str: + """Backward-compatible wrapper for start_capture_session().""" + return start_capture_session(target, target_is_interface=target_is_interface) + + +def stop_afpacket_sniffer( + session_id: Optional[str] = None, + target: Optional[str] = None, + target_is_interface: bool = False, +) -> None: + """Backward-compatible wrapper for stop_capture_session().""" + stop_capture_session(session_id=session_id, target=target, target_is_interface=target_is_interface) + + +def get_sniffer_status() -> Dict[str, Dict[str, object]]: + """Backward-compatible wrapper for get_capture_session_status().""" + return get_capture_session_status() diff --git a/backend/src/utilities/database.py b/backend/src/utilities/database.py index a5077d7..39181d8 100644 --- a/backend/src/utilities/database.py +++ b/backend/src/utilities/database.py @@ -17,7 +17,7 @@ from src.Models.etherType import EtherTypeEnum, ethertype_from_int from src.Models.ip_protocol import protocol_from_number from src.Models.packets import PacketDBModel -logger = logging.getLogger("af_packet_sniffer") +logger = logging.getLogger("packet_capture") def _db_text(value: Any) -> Any: diff --git a/backend/src/utilities/interface_bridge_helpers.py b/backend/src/utilities/interface_bridge_helpers.py index f2b9ac7..1badac9 100644 --- a/backend/src/utilities/interface_bridge_helpers.py +++ b/backend/src/utilities/interface_bridge_helpers.py @@ -6,7 +6,7 @@ import subprocess import time # ---- Logging ---------------------------------------------------------- -logger = logging.getLogger("af_packet_sniffer") +logger = logging.getLogger("packet_capture") _ETHTOOL_BIN = "/usr/sbin/ethtool" if os.path.exists("/usr/sbin/ethtool") else "ethtool" _ETHERNET_PROFILE_CACHE_TTL_SECONDS = 3.0 _ETHERNET_PROFILE_CACHE: Dict[str, tuple[float, Dict[str, Any]]] = {} diff --git a/backend/src/utilities/packet_identity.py b/backend/src/utilities/packet_identity.py index 1e3f1bc..5b613a6 100644 --- a/backend/src/utilities/packet_identity.py +++ b/backend/src/utilities/packet_identity.py @@ -1,4 +1,4 @@ -"""Helpers for stable packet identity across AF_PACKET and eBPF events.""" +"""Helpers for stable packet identity across capture and telemetry events.""" from __future__ import annotations diff --git a/backend/src/utilities/packet_tracker.py b/backend/src/utilities/packet_tracker.py index 87c4fd8..fe2ba41 100644 --- a/backend/src/utilities/packet_tracker.py +++ b/backend/src/utilities/packet_tracker.py @@ -1,4 +1,4 @@ -"""Aggregate AF_PACKET observations and kernel telemetry into one packet record.""" +"""Aggregate packet observations and kernel telemetry into one packet record.""" from __future__ import annotations diff --git a/frontend/src/components/SnifferManager.tsx b/frontend/src/components/SnifferManager.tsx index 81823c3..d4ba57c 100644 --- a/frontend/src/components/SnifferManager.tsx +++ b/frontend/src/components/SnifferManager.tsx @@ -135,11 +135,11 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement return (