test af_packet mode on bridges
This commit is contained in:
@@ -6,6 +6,8 @@ from fastapi import APIRouter, Body, HTTPException, Query
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from src.network_sniffer import (
|
from src.network_sniffer import (
|
||||||
|
BRIDGE_CAPTURE_MODE_AF_PACKET,
|
||||||
|
BRIDGE_CAPTURE_MODE_TC_EBPF,
|
||||||
get_internal_debug_state,
|
get_internal_debug_state,
|
||||||
get_capture_session_status,
|
get_capture_session_status,
|
||||||
start_capture_session,
|
start_capture_session,
|
||||||
@@ -28,6 +30,11 @@ class SnifferStartRequest(BaseModel):
|
|||||||
example="eth0",
|
example="eth0",
|
||||||
description="Interface name to sniff.",
|
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):
|
class SnifferStartResponse(BaseModel):
|
||||||
@@ -37,6 +44,7 @@ class SnifferStartResponse(BaseModel):
|
|||||||
session_id: str = Field(..., description="Unique session identifier.")
|
session_id: str = Field(..., description="Unique session identifier.")
|
||||||
target: str = Field(..., description="Started target name.")
|
target: str = Field(..., description="Started target name.")
|
||||||
target_type: str = Field(..., description="Either 'bridge' or 'interface'.")
|
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):
|
class SnifferStopRequest(BaseModel):
|
||||||
@@ -62,6 +70,7 @@ class InterfaceSnifferStatus(BaseModel):
|
|||||||
up: bool = Field(..., description="Whether the interface is operationally up.")
|
up: bool = Field(..., description="Whether the interface is operationally up.")
|
||||||
session_id: Optional[str] = Field(None, description="Owning capture session ID.")
|
session_id: Optional[str] = Field(None, description="Owning capture session ID.")
|
||||||
session_label: Optional[str] = Field(None, description="Human-readable session label.")
|
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):
|
class SnifferStatusResponse(BaseModel):
|
||||||
@@ -87,14 +96,23 @@ def sniffer_start(req: SnifferStartRequest) -> SnifferStartResponse:
|
|||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
target=req.interface,
|
target=req.interface,
|
||||||
target_type="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(
|
return SnifferStartResponse(
|
||||||
started=True,
|
started=True,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
target=req.bridge,
|
target=req.bridge,
|
||||||
target_type="bridge",
|
target_type="bridge",
|
||||||
|
capture_mode=effective_capture_mode,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to start sniffer: {exc}") from exc
|
raise HTTPException(status_code=500, detail=f"Failed to start sniffer: {exc}") from exc
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ sessions: Dict[str, Dict[str, Any]] = {}
|
|||||||
# "ports": List[str],
|
# "ports": List[str],
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
BRIDGE_CAPTURE_MODE_TC_EBPF = "tc_ebpf"
|
||||||
|
BRIDGE_CAPTURE_MODE_AF_PACKET = "af_packet"
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# PacketInfo typing
|
# PacketInfo typing
|
||||||
# -------------------------
|
# -------------------------
|
||||||
@@ -545,7 +548,7 @@ def _sync_bridge_telemetry() -> None:
|
|||||||
bridge_session_interfaces = {
|
bridge_session_interfaces = {
|
||||||
session_id: list(session.get("ports", []))
|
session_id: list(session.get("ports", []))
|
||||||
for session_id, session in sessions.items()
|
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:
|
try:
|
||||||
bridge_telemetry_manager.update_sessions(bridge_session_interfaces)
|
bridge_telemetry_manager.update_sessions(bridge_session_interfaces)
|
||||||
@@ -558,7 +561,11 @@ def _sync_bridge_telemetry() -> None:
|
|||||||
for session in sessions.values()
|
for session in sessions.values()
|
||||||
for iface in (
|
for iface in (
|
||||||
list(session.get("capture_ifaces", []))
|
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
|
if iface
|
||||||
}
|
}
|
||||||
@@ -677,20 +684,31 @@ def _session_reader_loop(session_id: str) -> None:
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
# Public API: start/stop/status
|
# 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.
|
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 == 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
|
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_id = str(uuid4())
|
||||||
session: Dict[str, Any] = {
|
session: Dict[str, Any] = {
|
||||||
"stop_event": threading.Event(),
|
"stop_event": threading.Event(),
|
||||||
"sockets": {},
|
"sockets": {},
|
||||||
"label": target,
|
"label": target,
|
||||||
"is_bridge": not target_is_interface,
|
"is_bridge": not target_is_interface,
|
||||||
|
"capture_mode": effective_capture_mode,
|
||||||
"ports": [],
|
"ports": [],
|
||||||
"capture_ifaces": [],
|
"capture_ifaces": [],
|
||||||
}
|
}
|
||||||
@@ -701,7 +719,7 @@ def start_capture_session(target: str, target_is_interface: bool = False) -> str
|
|||||||
capture_ifaces = [target]
|
capture_ifaces = [target]
|
||||||
else:
|
else:
|
||||||
ports = get_bridge_ports_once(target)
|
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["ports"] = ports
|
||||||
session["capture_ifaces"] = capture_ifaces
|
session["capture_ifaces"] = capture_ifaces
|
||||||
@@ -729,9 +747,10 @@ def start_capture_session(target: str, target_is_interface: bool = False) -> str
|
|||||||
session["thread"] = None
|
session["thread"] = None
|
||||||
_sync_bridge_telemetry()
|
_sync_bridge_telemetry()
|
||||||
logger.info(
|
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,
|
session_id,
|
||||||
target,
|
target,
|
||||||
|
effective_capture_mode,
|
||||||
ports,
|
ports,
|
||||||
capture_ifaces,
|
capture_ifaces,
|
||||||
)
|
)
|
||||||
@@ -839,6 +858,7 @@ def get_capture_session_status() -> Dict[str, Dict[str, object]]:
|
|||||||
"up": check_interface_up(iface),
|
"up": check_interface_up(iface),
|
||||||
"session_id": sid,
|
"session_id": sid,
|
||||||
"session_label": s.get("label"),
|
"session_label": s.get("label"),
|
||||||
|
"capture_mode": s.get("capture_mode"),
|
||||||
}
|
}
|
||||||
if not s.get("sockets") and s.get("is_bridge"):
|
if not s.get("sockets") and s.get("is_bridge"):
|
||||||
for iface in s.get("ports", []):
|
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),
|
"up": check_interface_up(iface),
|
||||||
"session_id": sid,
|
"session_id": sid,
|
||||||
"session_label": s.get("label"),
|
"session_label": s.get("label"),
|
||||||
|
"capture_mode": s.get("capture_mode"),
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -861,6 +882,7 @@ def get_internal_debug_state() -> dict:
|
|||||||
sid: {
|
sid: {
|
||||||
"label": s.get("label"),
|
"label": s.get("label"),
|
||||||
"is_bridge": s.get("is_bridge"),
|
"is_bridge": s.get("is_bridge"),
|
||||||
|
"capture_mode": s.get("capture_mode"),
|
||||||
"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()),
|
||||||
@@ -870,16 +892,35 @@ def get_internal_debug_state() -> dict:
|
|||||||
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",
|
"bridge_capture_mode": (
|
||||||
"telemetry_ports": sorted({iface for session in sessions.values() for iface in session.get("ports", [])}),
|
"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(),
|
"tshark": tshark_manager.get_debug_snapshot(),
|
||||||
"packet_tracker": packet_tracker.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()."""
|
"""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(
|
def stop_afpacket_sniffer(
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ interface SnifferManagerProps {
|
|||||||
export default function SnifferManager(props: SnifferManagerProps): ReactElement {
|
export default function SnifferManager(props: SnifferManagerProps): ReactElement {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [startMode, setStartMode] = useState<'interface' | 'bridge'>('interface');
|
const [startMode, setStartMode] = useState<'interface' | 'bridge'>('interface');
|
||||||
|
const [bridgeCaptureMode, setBridgeCaptureMode] = useState<'tc_ebpf' | 'af_packet'>('tc_ebpf');
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const statusEntries = useMemo(
|
const statusEntries = useMemo(
|
||||||
@@ -53,6 +54,7 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
const onOpenStartModal = () => {
|
const onOpenStartModal = () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
setStartMode('interface');
|
setStartMode('interface');
|
||||||
|
setBridgeCaptureMode('tc_ebpf');
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,27 +62,30 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStartSubmit = async (values: { target?: string }) => {
|
const handleStartSubmit = async (values: { target?: string; bridgeCaptureMode?: 'tc_ebpf' | 'af_packet' }) => {
|
||||||
const target = values.target;
|
const target = values.target;
|
||||||
if (!target) {
|
if (!target) {
|
||||||
notification.warning({ message: 'Warning', description: 'Please select a target to start sniffing on.' });
|
notification.warning({ message: 'Warning', description: 'Please select a target to start capture on.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = startMode === 'interface' ? { interface: target } : { bridge: target };
|
const payload =
|
||||||
|
startMode === 'interface'
|
||||||
|
? { interface: target }
|
||||||
|
: { bridge: target, bridge_capture_mode: values.bridgeCaptureMode ?? bridgeCaptureMode };
|
||||||
const result = await startSniffer(payload);
|
const result = await startSniffer(payload);
|
||||||
notification.success({
|
notification.success({
|
||||||
message: 'Sniffer started',
|
message: 'Capture started',
|
||||||
description: `Sniffer started on ${target} (session ${result.session_id})`,
|
description: `Capture started on ${target} via ${result.capture_mode} (session ${result.session_id})`,
|
||||||
});
|
});
|
||||||
await props.refreshAll();
|
await props.refreshAll();
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('startSniffer error', error);
|
console.error('startSniffer error', error);
|
||||||
notification.error({
|
notification.error({
|
||||||
message: 'Failed to start sniffer',
|
message: 'Failed to start capture',
|
||||||
description: error?.message ?? 'Failed to start sniffer',
|
description: error?.message ?? 'Failed to start capture',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -88,13 +93,13 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
const handleStopAll = async () => {
|
const handleStopAll = async () => {
|
||||||
try {
|
try {
|
||||||
await stopSniffer();
|
await stopSniffer();
|
||||||
notification.success({ message: 'All sniffers stopped' });
|
notification.success({ message: 'All capture sessions stopped' });
|
||||||
await props.refreshStatus();
|
await props.refreshStatus();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('stopSniffer error', error);
|
console.error('stopSniffer error', error);
|
||||||
notification.error({
|
notification.error({
|
||||||
message: 'Failed to stop sniffers',
|
message: 'Failed to stop capture sessions',
|
||||||
description: error?.message ?? 'Failed to stop sniffers',
|
description: error?.message ?? 'Failed to stop capture sessions',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -103,8 +108,8 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
try {
|
try {
|
||||||
await stopSnifferByInterface(ifaceName);
|
await stopSnifferByInterface(ifaceName);
|
||||||
notification.success({
|
notification.success({
|
||||||
message: 'Sniffer stopped',
|
message: 'Capture stopped',
|
||||||
description: `Sniffer stopped on interface ${ifaceName}`,
|
description: `Capture stopped on interface ${ifaceName}`,
|
||||||
});
|
});
|
||||||
await props.refreshStatus();
|
await props.refreshStatus();
|
||||||
return;
|
return;
|
||||||
@@ -116,8 +121,8 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
try {
|
try {
|
||||||
await stopSniffer({ session_id: sessionId });
|
await stopSniffer({ session_id: sessionId });
|
||||||
notification.success({
|
notification.success({
|
||||||
message: 'Sniffer stopped',
|
message: 'Capture stopped',
|
||||||
description: `Sniffer stopped on interface ${ifaceName} (session ${sessionId})`,
|
description: `Capture stopped on interface ${ifaceName} (session ${sessionId})`,
|
||||||
});
|
});
|
||||||
await props.refreshStatus();
|
await props.refreshStatus();
|
||||||
return;
|
return;
|
||||||
@@ -127,8 +132,8 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
}
|
}
|
||||||
|
|
||||||
notification.error({
|
notification.error({
|
||||||
message: 'Failed to stop sniffer',
|
message: 'Failed to stop capture',
|
||||||
description: 'Could not stop sniffer for this interface.',
|
description: 'Could not stop capture for this interface.',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,7 +150,7 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
<Tooltip title="Refresh status">
|
<Tooltip title="Refresh status">
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => props.refreshStatus()} />
|
<Button icon={<ReloadOutlined />} onClick={() => props.refreshStatus()} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Stop all sniffers">
|
<Tooltip title="Stop all capture sessions">
|
||||||
<Button danger icon={<StopOutlined />} onClick={handleStopAll} />
|
<Button danger icon={<StopOutlined />} onClick={handleStopAll} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -189,15 +194,15 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
try {
|
try {
|
||||||
const result = await startSniffer({ interface: name });
|
const result = await startSniffer({ interface: name });
|
||||||
notification.success({
|
notification.success({
|
||||||
message: 'Sniffer started',
|
message: 'Capture started',
|
||||||
description: `Sniffer started on ${name} (session ${result.session_id})`,
|
description: `Capture started on ${name} via ${result.capture_mode} (session ${result.session_id})`,
|
||||||
});
|
});
|
||||||
await props.refreshStatus();
|
await props.refreshStatus();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('startSniffer quick', error);
|
console.error('startSniffer quick', error);
|
||||||
notification.error({
|
notification.error({
|
||||||
message: 'Failed to start sniffer',
|
message: 'Failed to start capture',
|
||||||
description: error?.message ?? 'Failed to start sniffer',
|
description: error?.message ?? 'Failed to start capture',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -231,6 +236,11 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
</Text>
|
</Text>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
|
{status.capture_mode && (
|
||||||
|
<Tag color={status.capture_mode === 'af_packet' ? 'geekblue' : 'purple'}>
|
||||||
|
{status.capture_mode === 'af_packet' ? 'AF_PACKET' : 'tc/eBPF'}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
description={<Text type="secondary">interface: {name}</Text>}
|
description={<Text type="secondary">interface: {name}</Text>}
|
||||||
@@ -256,7 +266,8 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
value={startMode}
|
value={startMode}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setStartMode(event.target.value);
|
setStartMode(event.target.value);
|
||||||
form.setFieldsValue({ target: undefined });
|
setBridgeCaptureMode('tc_ebpf');
|
||||||
|
form.setFieldsValue({ target: undefined, bridgeCaptureMode: 'tc_ebpf' });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Radio value="interface">Interface</Radio>
|
<Radio value="interface">Interface</Radio>
|
||||||
@@ -290,6 +301,28 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{startMode === 'bridge' && (
|
||||||
|
<Form.Item
|
||||||
|
label="Bridge Capture Path"
|
||||||
|
name="bridgeCaptureMode"
|
||||||
|
initialValue="tc_ebpf"
|
||||||
|
extra="Choose between tc/eBPF bridge telemetry or direct AF_PACKET capture on the bridge member interfaces."
|
||||||
|
>
|
||||||
|
<Radio.Group
|
||||||
|
value={bridgeCaptureMode}
|
||||||
|
onChange={(event) => {
|
||||||
|
setBridgeCaptureMode(event.target.value);
|
||||||
|
form.setFieldsValue({ bridgeCaptureMode: event.target.value });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Radio value="tc_ebpf">tc/eBPF telemetry capture</Radio>
|
||||||
|
<Radio value="af_packet">AF_PACKET on bridge member interfaces</Radio>
|
||||||
|
</Space>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function Sniffing(): ReactElement {
|
|||||||
setStatusMap(status.interfaces ?? {});
|
setStatusMap(status.interfaces ?? {});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('fetchSnifferStatus error', error);
|
console.error('fetchSnifferStatus error', error);
|
||||||
message.error(error?.message ?? 'Failed to fetch sniffer status');
|
message.error(error?.message ?? 'Failed to fetch capture status');
|
||||||
} finally {
|
} finally {
|
||||||
setStatusLoading(false);
|
setStatusLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
export interface SnifferStartRequest {
|
export interface SnifferStartRequest {
|
||||||
bridge?: string;
|
bridge?: string;
|
||||||
interface?: string;
|
interface?: string;
|
||||||
|
bridge_capture_mode?: 'tc_ebpf' | 'af_packet';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,6 +16,7 @@ export interface SnifferStartResponse {
|
|||||||
session_id: string;
|
session_id: string;
|
||||||
target: string;
|
target: string;
|
||||||
target_type: 'bridge' | 'interface';
|
target_type: 'bridge' | 'interface';
|
||||||
|
capture_mode: 'tc_ebpf' | 'af_packet';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +28,7 @@ export interface SnifferStopRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response returned when stopping sniffer(s).
|
* Response returned when stopping capture session(s).
|
||||||
*/
|
*/
|
||||||
export interface SnifferStopResponse {
|
export interface SnifferStopResponse {
|
||||||
stopped: boolean;
|
stopped: boolean;
|
||||||
@@ -36,7 +38,7 @@ export interface SnifferStopResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-interface sniffer status (now includes owning session info).
|
* Per-interface capture status (now includes owning session info).
|
||||||
*/
|
*/
|
||||||
export interface InterfaceSnifferStatus {
|
export interface InterfaceSnifferStatus {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
@@ -44,6 +46,7 @@ export interface InterfaceSnifferStatus {
|
|||||||
up: boolean;
|
up: boolean;
|
||||||
session_id?: string | null;
|
session_id?: string | null;
|
||||||
session_label?: string | null;
|
session_label?: string | null;
|
||||||
|
capture_mode?: 'tc_ebpf' | 'af_packet' | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user