test af_packet mode on bridges
All checks were successful
Build and Deploy MITM Webserver / traffic_target (push) Successful in 0s
Build and Deploy MITM Webserver / build (push) Successful in 10s

This commit is contained in:
2026-04-12 17:58:19 +02:00
parent 9ee47284e0
commit 4521b9d99e
5 changed files with 131 additions and 36 deletions

View File

@@ -6,6 +6,8 @@ from fastapi import APIRouter, Body, HTTPException, Query
from pydantic import BaseModel, Field
from src.network_sniffer import (
BRIDGE_CAPTURE_MODE_AF_PACKET,
BRIDGE_CAPTURE_MODE_TC_EBPF,
get_internal_debug_state,
get_capture_session_status,
start_capture_session,
@@ -28,6 +30,11 @@ class SnifferStartRequest(BaseModel):
example="eth0",
description="Interface name to sniff.",
)
bridge_capture_mode: Optional[str] = Field(
None,
example="tc_ebpf",
description="Bridge capture mode: 'tc_ebpf' or 'af_packet'. Ignored for interface capture.",
)
class SnifferStartResponse(BaseModel):
@@ -37,6 +44,7 @@ class SnifferStartResponse(BaseModel):
session_id: str = Field(..., description="Unique session identifier.")
target: str = Field(..., description="Started target name.")
target_type: str = Field(..., description="Either 'bridge' or 'interface'.")
capture_mode: str = Field(..., description="The effective capture mode used by the session.")
class SnifferStopRequest(BaseModel):
@@ -62,6 +70,7 @@ class InterfaceSnifferStatus(BaseModel):
up: bool = Field(..., description="Whether the interface is operationally up.")
session_id: Optional[str] = Field(None, description="Owning capture session ID.")
session_label: Optional[str] = Field(None, description="Human-readable session label.")
capture_mode: Optional[str] = Field(None, description="Capture mode used by the owning session.")
class SnifferStatusResponse(BaseModel):
@@ -87,14 +96,23 @@ def sniffer_start(req: SnifferStartRequest) -> SnifferStartResponse:
session_id=session_id,
target=req.interface,
target_type="interface",
capture_mode="af_packet",
)
session_id = start_capture_session(req.bridge, target_is_interface=False)
effective_capture_mode = req.bridge_capture_mode or BRIDGE_CAPTURE_MODE_TC_EBPF
if effective_capture_mode not in {BRIDGE_CAPTURE_MODE_TC_EBPF, BRIDGE_CAPTURE_MODE_AF_PACKET}:
raise HTTPException(status_code=400, detail="bridge_capture_mode must be 'tc_ebpf' or 'af_packet'")
session_id = start_capture_session(
req.bridge,
target_is_interface=False,
bridge_capture_mode=effective_capture_mode,
)
return SnifferStartResponse(
started=True,
session_id=session_id,
target=req.bridge,
target_type="bridge",
capture_mode=effective_capture_mode,
)
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Failed to start sniffer: {exc}") from exc

View File

@@ -56,6 +56,9 @@ sessions: Dict[str, Dict[str, Any]] = {}
# "ports": List[str],
# }
BRIDGE_CAPTURE_MODE_TC_EBPF = "tc_ebpf"
BRIDGE_CAPTURE_MODE_AF_PACKET = "af_packet"
# -------------------------
# PacketInfo typing
# -------------------------
@@ -545,7 +548,7 @@ def _sync_bridge_telemetry() -> None:
bridge_session_interfaces = {
session_id: list(session.get("ports", []))
for session_id, session in sessions.items()
if session.get("is_bridge")
if session.get("is_bridge") and session.get("capture_mode") == BRIDGE_CAPTURE_MODE_TC_EBPF
}
try:
bridge_telemetry_manager.update_sessions(bridge_session_interfaces)
@@ -558,7 +561,11 @@ def _sync_bridge_telemetry() -> None:
for session in sessions.values()
for iface in (
list(session.get("capture_ifaces", []))
+ (list(session.get("ports", [])) if session.get("is_bridge") else [])
+ (
list(session.get("ports", []))
if session.get("is_bridge") and session.get("capture_mode") == BRIDGE_CAPTURE_MODE_TC_EBPF
else []
)
)
if iface
}
@@ -677,20 +684,31 @@ def _session_reader_loop(session_id: str) -> None:
# -------------------------
# Public API: start/stop/status
# -------------------------
def start_capture_session(target: str, target_is_interface: bool = False) -> str:
def start_capture_session(
target: str,
target_is_interface: bool = False,
bridge_capture_mode: str = BRIDGE_CAPTURE_MODE_TC_EBPF,
) -> str:
"""
Start a packet capture session. Returns session_id string.
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.
configured bridge capture mode for the bridge ports.
"""
effective_capture_mode = (
BRIDGE_CAPTURE_MODE_AF_PACKET
if target_is_interface or bridge_capture_mode == BRIDGE_CAPTURE_MODE_AF_PACKET
else BRIDGE_CAPTURE_MODE_TC_EBPF
)
session_id = str(uuid4())
session: Dict[str, Any] = {
"stop_event": threading.Event(),
"sockets": {},
"label": target,
"is_bridge": not target_is_interface,
"capture_mode": effective_capture_mode,
"ports": [],
"capture_ifaces": [],
}
@@ -701,7 +719,7 @@ def start_capture_session(target: str, target_is_interface: bool = False) -> str
capture_ifaces = [target]
else:
ports = get_bridge_ports_once(target)
capture_ifaces = []
capture_ifaces = ports if effective_capture_mode == BRIDGE_CAPTURE_MODE_AF_PACKET else []
session["ports"] = ports
session["capture_ifaces"] = capture_ifaces
@@ -729,9 +747,10 @@ def start_capture_session(target: str, target_is_interface: bool = False) -> str
session["thread"] = None
_sync_bridge_telemetry()
logger.info(
"Started capture session %s label=%s ports=%s capture_ifaces=%s",
"Started capture session %s label=%s capture_mode=%s ports=%s capture_ifaces=%s",
session_id,
target,
effective_capture_mode,
ports,
capture_ifaces,
)
@@ -839,6 +858,7 @@ def get_capture_session_status() -> Dict[str, Dict[str, object]]:
"up": check_interface_up(iface),
"session_id": sid,
"session_label": s.get("label"),
"capture_mode": s.get("capture_mode"),
}
if not s.get("sockets") and s.get("is_bridge"):
for iface in s.get("ports", []):
@@ -848,6 +868,7 @@ def get_capture_session_status() -> Dict[str, Dict[str, object]]:
"up": check_interface_up(iface),
"session_id": sid,
"session_label": s.get("label"),
"capture_mode": s.get("capture_mode"),
}
return out
@@ -861,6 +882,7 @@ def get_internal_debug_state() -> dict:
sid: {
"label": s.get("label"),
"is_bridge": s.get("is_bridge"),
"capture_mode": s.get("capture_mode"),
"ports": list(s.get("ports", [])),
"capture_ifaces": list(s.get("capture_ifaces", [])),
"sockets": list(s.get("sockets", {}).keys()),
@@ -870,16 +892,35 @@ def get_internal_debug_state() -> dict:
for sid, s in sessions.items()
},
"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", [])}),
"bridge_capture_mode": (
"tc_ingress_raw"
if any(s.get("is_bridge") and s.get("capture_mode") == BRIDGE_CAPTURE_MODE_TC_EBPF for s in sessions.values())
else "af_packet"
),
"telemetry_ports": sorted(
{
iface
for session in sessions.values()
if session.get("capture_mode") == BRIDGE_CAPTURE_MODE_TC_EBPF
for iface in session.get("ports", [])
}
),
"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:
def start_afpacket_sniffer(
target: str,
target_is_interface: bool = False,
bridge_capture_mode: str = BRIDGE_CAPTURE_MODE_TC_EBPF,
) -> str:
"""Backward-compatible wrapper for start_capture_session()."""
return start_capture_session(target, target_is_interface=target_is_interface)
return start_capture_session(
target,
target_is_interface=target_is_interface,
bridge_capture_mode=bridge_capture_mode,
)
def stop_afpacket_sniffer(