use shared objects like loops db access etc
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s
This commit is contained in:
401
backend/src/api/network_api.py
Normal file
401
backend/src/api/network_api.py
Normal file
@@ -0,0 +1,401 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from pyroute2 import IPRoute, NDB
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Globals for lazy initialization
|
||||
ip: IPRoute | None = None
|
||||
ndb: NDB | None = None
|
||||
|
||||
# ------------------------------
|
||||
# Pydantic models
|
||||
# ------------------------------
|
||||
|
||||
class InterfaceAddress(BaseModel):
|
||||
"""
|
||||
Represents an IP address assigned to a network interface.
|
||||
"""
|
||||
family: str = Field(..., description="IP family: 'ipv4' or 'ipv6'.")
|
||||
address: str = Field(..., description="The IP address assigned to the interface.")
|
||||
prefixlen: int = Field(..., description="Subnet prefix length (e.g., 24 for 255.255.255.0).")
|
||||
|
||||
|
||||
class InterfaceInfo(BaseModel):
|
||||
"""
|
||||
Represents a network interface with all its properties.
|
||||
"""
|
||||
ifindex: int = Field(..., description="Interface index (unique identifier assigned by the kernel).")
|
||||
name: str = Field(..., description="Interface name (e.g., 'eth0', 'enp38s0').")
|
||||
state: str = Field(..., description="Operational state (e.g., 'UP', 'DOWN', 'UNKNOWN').")
|
||||
mac: Optional[str] = Field(None, description="MAC address of the interface, if applicable.")
|
||||
mtu: int = Field(..., description="Maximum Transmission Unit for the interface.")
|
||||
flags: List[str] = Field(..., description="List of interface flags (e.g., ['BROADCAST', 'MULTICAST']).")
|
||||
addresses: List[InterfaceAddress] = Field(..., description="List of IP addresses assigned to the interface.")
|
||||
|
||||
class RouteInfo(BaseModel):
|
||||
"""
|
||||
Represents a single routing table entry.
|
||||
"""
|
||||
|
||||
dst: Optional[str] = Field(
|
||||
None, description="Destination network in CIDR notation (e.g., '192.168.1.0/24'). None means default route."
|
||||
)
|
||||
|
||||
gateway: Optional[str] = Field(
|
||||
None, description="Next-hop gateway IP address for this route. None if the route is directly connected."
|
||||
)
|
||||
|
||||
prefsrc: Optional[str] = Field(
|
||||
None, description="Preferred source IP to use when sending packets via this route."
|
||||
)
|
||||
|
||||
oif: Optional[int] = Field(
|
||||
None, description="Output interface index (ifindex) for this route. Can be used to look up the interface name."
|
||||
)
|
||||
|
||||
ifname: Optional[str] = Field(
|
||||
None, description="Name of the interface corresponding to `oif` (e.g., 'eth0')."
|
||||
)
|
||||
|
||||
table: int = Field(
|
||||
..., description="Routing table ID (e.g., 254 = main, 255 = local)."
|
||||
)
|
||||
|
||||
proto: Optional[int] = Field(
|
||||
None,
|
||||
description="Protocol of the route (numeric Linux codes, e.g., 2=kernel, 16=static)."
|
||||
)
|
||||
|
||||
scope: Optional[int] = Field(
|
||||
None,
|
||||
description="Scope of the route: 0=global, 253=link, 254=host, 255=nowhere."
|
||||
)
|
||||
|
||||
type: Optional[int] = Field(
|
||||
None,
|
||||
description="Type of the route (numeric code): 1=unicast, 2=local, 3=broadcast, 5=multicast."
|
||||
)
|
||||
|
||||
class BridgeInterfaceInfo(BaseModel):
|
||||
"""
|
||||
Represents a network interface which is a member of an bridge.
|
||||
"""
|
||||
ifindex: int = Field(..., description="Interface index of a bridge member")
|
||||
ifname: str = Field(..., description="Interface name of a bridge member")
|
||||
state: Optional[str] = Field(None, description="Operational state of the interface")
|
||||
mtu: Optional[int] = Field(None, description="MTU of the interface")
|
||||
|
||||
class BridgeInfo(BaseModel):
|
||||
"""
|
||||
Represents a network bridge interface with all its properties.
|
||||
"""
|
||||
ifindex: int = Field(..., description="Interface index of the bridge")
|
||||
ifname: str = Field(..., description="Bridge interface name")
|
||||
state: Optional[str] = Field(None, description="Operational state of the bridge")
|
||||
mtu: Optional[int] = Field(None, description="MTU of the bridge")
|
||||
stp_state: Optional[int] = Field(None, description="STP (Spanning Tree Protocol) state of the bridge")
|
||||
members: List[BridgeInterfaceInfo] = Field(default_factory=list, description="List of member interfaces of the bridge")
|
||||
|
||||
class BridgeCreateRequest(BaseModel):
|
||||
name: str
|
||||
interfaces: List[str]
|
||||
|
||||
|
||||
class BridgeRemoveRequest(BaseModel):
|
||||
name: str
|
||||
# ------------------------------
|
||||
# Lazy Init Functions
|
||||
# ------------------------------
|
||||
|
||||
def init_network_api():
|
||||
global ip, ndb
|
||||
if ip is None:
|
||||
ip = IPRoute()
|
||||
if ndb is None:
|
||||
ndb = NDB()
|
||||
|
||||
def shutdown_network_api():
|
||||
global ip, ndb
|
||||
if ip:
|
||||
ip.close()
|
||||
ip = None
|
||||
if ndb:
|
||||
ndb.close()
|
||||
ndb = None
|
||||
|
||||
def get_iproute():
|
||||
if ip is None:
|
||||
init_network_api()
|
||||
return ip
|
||||
|
||||
def get_ndb():
|
||||
if ndb is None:
|
||||
init_network_api()
|
||||
return ndb
|
||||
|
||||
# ------------------------------
|
||||
# Utility functions
|
||||
# ------------------------------
|
||||
|
||||
def parse_addresses(addrs):
|
||||
res = []
|
||||
for a in addrs:
|
||||
family = "ipv4" if a.get("family") == 2 else "ipv6"
|
||||
res.append(
|
||||
InterfaceAddress(
|
||||
family=family,
|
||||
address=a.get("address"),
|
||||
prefixlen=a.get("prefixlen"),
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
def parse_flags(flags_int: int) -> list[str]:
|
||||
"""
|
||||
Converts the integer flags from pyroute2 to human-readable list of strings.
|
||||
"""
|
||||
flags_map = {
|
||||
0x1: "UP",
|
||||
0x2: "BROADCAST",
|
||||
0x4: "DEBUG",
|
||||
0x8: "LOOPBACK",
|
||||
0x10: "POINTOPOINT",
|
||||
0x20: "NOTRAILERS",
|
||||
0x40: "RUNNING",
|
||||
0x80: "NOARP",
|
||||
0x100: "PROMISC",
|
||||
0x200: "ALLMULTI",
|
||||
0x400: "MASTER",
|
||||
0x800: "SLAVE",
|
||||
0x1000: "MULTICAST",
|
||||
0x2000: "PORTSEL",
|
||||
0x4000: "AUTOMEDIA",
|
||||
0x8000: "DYNAMIC",
|
||||
0x10000: "LOWER_UP",
|
||||
0x20000: "DORMANT",
|
||||
0x40000: "ECHO",
|
||||
}
|
||||
result = []
|
||||
for bit, name in flags_map.items():
|
||||
if flags_int & bit:
|
||||
result.append(name)
|
||||
return result
|
||||
|
||||
def iface_index(name: str, ip: IPRoute) -> int:
|
||||
idx = ip.link_lookup(ifname=name)
|
||||
if not idx:
|
||||
raise HTTPException(status_code=404, detail=f"Interface {name} not found")
|
||||
return idx[0]
|
||||
|
||||
|
||||
def bridge_exists(name: str, ip: IPRoute) -> bool:
|
||||
return bool(ip.link_lookup(ifname=name))
|
||||
|
||||
|
||||
# ------------------------------
|
||||
# Endpoints
|
||||
# ------------------------------
|
||||
|
||||
@router.get("/interfaces", response_model=List[InterfaceInfo])
|
||||
def get_interfaces(ip: IPRoute = Depends(get_iproute)):
|
||||
result = []
|
||||
links = ip.get_links()
|
||||
addresses = ip.get_addr()
|
||||
|
||||
addr_map = {}
|
||||
for a in addresses:
|
||||
ifindex = a.get("index")
|
||||
addr_map.setdefault(ifindex, []).append(a)
|
||||
|
||||
for link in links:
|
||||
attrs = dict(link["attrs"])
|
||||
ifindex = link["index"]
|
||||
addrs = addr_map.get(ifindex, [])
|
||||
|
||||
result.append(
|
||||
InterfaceInfo(
|
||||
ifindex=ifindex,
|
||||
name=attrs.get("IFLA_IFNAME"),
|
||||
state=attrs.get("IFLA_OPERSTATE", "unknown"),
|
||||
mac=attrs.get("IFLA_ADDRESS"),
|
||||
mtu=attrs.get("IFLA_MTU"),
|
||||
flags=parse_flags(link.get("flags", 0)),
|
||||
addresses=parse_addresses(addrs),
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
@router.get("/routes", response_model=List[RouteInfo])
|
||||
def get_routes(ip: IPRoute = Depends(get_iproute)):
|
||||
routes = []
|
||||
for r in ip.get_routes():
|
||||
attrs = dict(r["attrs"])
|
||||
dst = attrs.get("RTA_DST")
|
||||
gateway = attrs.get("RTA_GATEWAY")
|
||||
prefsrc = attrs.get("RTA_PREFSRC")
|
||||
oif = r.get("oif")
|
||||
ifname = None
|
||||
if oif is not None:
|
||||
# translate ifindex → name
|
||||
link = ip.get_links(oif)[0]
|
||||
ifname = dict(link["attrs"]).get("IFLA_IFNAME")
|
||||
|
||||
routes.append(
|
||||
RouteInfo(
|
||||
dst=f"{dst}/{r.get('dst_len')}" if dst else None,
|
||||
gateway=gateway,
|
||||
prefsrc=prefsrc,
|
||||
oif=oif,
|
||||
ifname=ifname,
|
||||
table=r.get("table", 254),
|
||||
proto=r.get("proto"),
|
||||
scope=r.get("scope"),
|
||||
type=r.get("type"),
|
||||
)
|
||||
)
|
||||
return routes
|
||||
|
||||
|
||||
@router.get("/links", response_model=List[InterfaceInfo])
|
||||
def get_raw_links(ip: IPRoute = Depends(get_iproute)):
|
||||
"""
|
||||
Returns all interfaces in a clean Pydantic format.
|
||||
This is similar to /interfaces but avoids additional processing if needed.
|
||||
"""
|
||||
result = []
|
||||
links = ip.get_links()
|
||||
addresses = ip.get_addr()
|
||||
|
||||
# group addresses by interface index
|
||||
addr_map = {}
|
||||
for a in addresses:
|
||||
ifindex = a.get("index")
|
||||
addr_map.setdefault(ifindex, []).append(a)
|
||||
|
||||
for link in links:
|
||||
attrs = dict(link.get("attrs", [])) # convert list of tuples to dict
|
||||
ifindex = link["index"]
|
||||
addrs = addr_map.get(ifindex, [])
|
||||
|
||||
result.append(
|
||||
InterfaceInfo(
|
||||
ifindex=ifindex,
|
||||
name=attrs.get("IFLA_IFNAME", "unknown"),
|
||||
state=attrs.get("IFLA_OPERSTATE", "unknown"),
|
||||
mac=attrs.get("IFLA_ADDRESS"),
|
||||
mtu=attrs.get("IFLA_MTU", 0),
|
||||
flags=[], # latest pyroute2 removed ifi_flags, leave empty
|
||||
addresses=parse_addresses(addrs),
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@router.get("/bridges", response_model=List[BridgeInfo])
|
||||
def get_bridges():
|
||||
"""
|
||||
Get all bridge interfaces on the system, including their member interfaces.
|
||||
Returns detailed information:
|
||||
- Bridge index, name, state, MTU
|
||||
- STP state
|
||||
- Member interfaces with index, name, state, and MTU
|
||||
"""
|
||||
bridges_list: List[BridgeInfo] = []
|
||||
|
||||
with NDB() as ndb:
|
||||
for br in ndb.interfaces:
|
||||
# Only bridges
|
||||
if getattr(br, "kind", None) == "bridge":
|
||||
members: List[BridgeInterfaceInfo] = []
|
||||
# Find member interfaces
|
||||
for iface in ndb.interfaces:
|
||||
if getattr(iface, "master", None) == br.index:
|
||||
members.append(
|
||||
BridgeInterfaceInfo(
|
||||
ifindex=iface.index,
|
||||
ifname=iface.ifname,
|
||||
state=getattr(iface, "operstate", None),
|
||||
mtu=getattr(iface, "mtu", None)
|
||||
)
|
||||
)
|
||||
|
||||
bridges_list.append(
|
||||
BridgeInfo(
|
||||
ifindex=br.index,
|
||||
ifname=br.ifname,
|
||||
state=getattr(br, "operstate", None),
|
||||
mtu=getattr(br, "mtu", None),
|
||||
stp_state=getattr(br, "stp_state", None),
|
||||
members=members
|
||||
)
|
||||
)
|
||||
|
||||
return bridges_list
|
||||
|
||||
@router.get("/full-state")
|
||||
def full_state(
|
||||
ip: IPRoute = Depends(get_iproute),
|
||||
):
|
||||
"""
|
||||
Returns the full network state:
|
||||
- Interfaces with IP addresses and flags
|
||||
- Routes
|
||||
- Bridges with member interfaces
|
||||
"""
|
||||
return {
|
||||
"interfaces": get_interfaces(ip),
|
||||
"routes": get_routes(ip),
|
||||
"bridges": get_bridges(), # uses NDB internally
|
||||
}
|
||||
|
||||
@router.post("/bridge/create")
|
||||
def create_bridge(req: BridgeCreateRequest, ip: IPRoute = Depends(get_iproute)):
|
||||
if bridge_exists(req.name, ip):
|
||||
raise HTTPException(400, f"Bridge {req.name} already exists")
|
||||
|
||||
# Bridge erzeugen
|
||||
ip.link("add", ifname=req.name, kind="bridge")
|
||||
br_idx = iface_index(req.name, ip)
|
||||
|
||||
# Bridge konfigurieren
|
||||
# TODO Parameter anpassen (STP, etc.)
|
||||
ip.link("set", index=br_idx, kind="bridge", br_stp_state=0)
|
||||
ip.link("set", index=br_idx, state="up")
|
||||
|
||||
# Interfaces hinzufügen + aktivieren
|
||||
for iface in req.interfaces:
|
||||
idx = iface_index(iface, ip)
|
||||
|
||||
# interface hochfahren
|
||||
ip.link("set", index=idx, state="down") # optional - sicherer
|
||||
ip.link("set", index=idx, state="up")
|
||||
|
||||
# interface in die bridge hängen
|
||||
ip.link("set", index=idx, master=br_idx)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"bridge": req.name,
|
||||
"interfaces": req.interfaces
|
||||
}
|
||||
|
||||
|
||||
@router.post("/bridge/remove")
|
||||
def remove_bridge(req: BridgeRemoveRequest, ip: IPRoute = Depends(get_iproute)):
|
||||
if not bridge_exists(req.name, ip):
|
||||
raise HTTPException(404, f"Bridge {req.name} not found")
|
||||
|
||||
br_idx = iface_index(req.name, ip)
|
||||
|
||||
# Bridge runterfahren
|
||||
ip.link("set", index=br_idx, state="down")
|
||||
|
||||
# Bridge löschen
|
||||
ip.link("del", index=br_idx)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"deleted": req.name
|
||||
}
|
||||
144
backend/src/api/packet_api.py
Normal file
144
backend/src/api/packet_api.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# src/routers/packets.py
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Any, Dict, List
|
||||
|
||||
from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
import src.shared_objects as shared
|
||||
|
||||
logger = logging.getLogger("packets_router")
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _serialize_row_for_json(row: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert DB row / pkt_info to a JSON-serializable dict.
|
||||
- If 'raw' is bytes, produce 'raw_b64' and drop 'raw'.
|
||||
- Fallback to str() for unknown/unserializable values.
|
||||
"""
|
||||
out: Dict[str, Any] = {}
|
||||
for k, v in row.items():
|
||||
if k == "raw" and isinstance(v, (bytes, bytearray)):
|
||||
out["raw_b64"] = base64.b64encode(v).decode("ascii")
|
||||
continue
|
||||
# try to JSON serialize the value directly
|
||||
try:
|
||||
json.dumps({k: v})
|
||||
out[k] = v
|
||||
except (TypeError, ValueError):
|
||||
out[k] = str(v)
|
||||
return out
|
||||
|
||||
|
||||
async def _serialize_rows(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
return [_serialize_row_for_json(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/packets")
|
||||
async def get_packets(limit: int = Query(100, ge=1, le=10000)):
|
||||
"""
|
||||
Return latest `limit` packets (newest first). The DB helper already converts
|
||||
`raw` to `raw_b64` in fetch_latest, but we defensively re-serialize here.
|
||||
"""
|
||||
db = shared.db
|
||||
if db is None:
|
||||
logger.warning("GET /packets called but DB is not available")
|
||||
raise HTTPException(status_code=503, detail="Database not available")
|
||||
|
||||
try:
|
||||
rows = await db.fetch_latest(limit)
|
||||
serial = await _serialize_rows(rows)
|
||||
return JSONResponse(content={"count": len(serial), "packets": serial})
|
||||
except Exception:
|
||||
logger.exception("Failed to fetch latest packets from DB")
|
||||
raise HTTPException(status_code=500, detail="Failed to fetch packets")
|
||||
|
||||
|
||||
@router.websocket("/ws/packets")
|
||||
async def websocket_packets(ws: WebSocket):
|
||||
"""
|
||||
WebSocket live feed endpoint.
|
||||
|
||||
Accepts optional query param `subscribe_recent` (e.g. ?subscribe_recent=20)
|
||||
which will deliver the last N packets immediately on connect.
|
||||
"""
|
||||
await ws.accept()
|
||||
logger.debug("WebSocket connection accepted: %s", ws.client)
|
||||
|
||||
db = shared.db
|
||||
broadcaster = shared.broadcaster
|
||||
|
||||
if db is None:
|
||||
await ws.send_json({"error": "database not available"})
|
||||
await ws.close()
|
||||
logger.warning("WebSocket closed: DB not available")
|
||||
return
|
||||
|
||||
if broadcaster is None:
|
||||
await ws.send_json({"error": "broadcaster not available"})
|
||||
await ws.close()
|
||||
logger.warning("WebSocket closed: broadcaster not available")
|
||||
return
|
||||
|
||||
# Parse subscribe_recent from query params (defensive)
|
||||
try:
|
||||
subscribe_recent_raw = ws.query_params.get("subscribe_recent", "0")
|
||||
subscribe_recent = int(subscribe_recent_raw)
|
||||
if subscribe_recent < 0:
|
||||
subscribe_recent = 0
|
||||
except Exception:
|
||||
subscribe_recent = 0
|
||||
|
||||
q: Optional[asyncio.Queue] = None
|
||||
try:
|
||||
# Optionally send recent history first
|
||||
if subscribe_recent > 0:
|
||||
recent = await db.fetch_latest(subscribe_recent)
|
||||
recent_serial = await _serialize_rows(recent)
|
||||
await ws.send_json({"type": "recent", "count": len(recent_serial), "packets": recent_serial})
|
||||
|
||||
# Subscribe to broadcaster to receive live packets
|
||||
q = await broadcaster.subscribe()
|
||||
logger.info("WebSocket subscribed client %s (queue maxsize=%d)", ws.client, q.maxsize)
|
||||
|
||||
# Simple heartbeat: periodically ensure client is responsive (optional)
|
||||
# We'll implement by awaiting q.get() which blocks until a message is published.
|
||||
while True:
|
||||
msg = await q.get()
|
||||
# Normalize message to JSON-able dict
|
||||
if isinstance(msg, dict):
|
||||
payload = _serialize_row_for_json(msg)
|
||||
else:
|
||||
# not a dict — try to json-serialize directly
|
||||
try:
|
||||
json.dumps(msg)
|
||||
payload = msg
|
||||
except Exception:
|
||||
payload = {"data": str(msg)}
|
||||
|
||||
try:
|
||||
await ws.send_json(payload)
|
||||
except Exception:
|
||||
# sending failed (client disconnected or write error)
|
||||
logger.info("WebSocket send failed for client %s — unsubscribing", ws.client)
|
||||
break
|
||||
except WebSocketDisconnect:
|
||||
logger.info("WebSocket client disconnected: %s", ws.client)
|
||||
except Exception:
|
||||
logger.exception("Unexpected error in websocket_packets")
|
||||
finally:
|
||||
# Clean up subscriber queue
|
||||
if q is not None:
|
||||
try:
|
||||
await broadcaster.unsubscribe(q)
|
||||
except Exception:
|
||||
logger.exception("Failed to unsubscribe websocket queue")
|
||||
try:
|
||||
await ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("WebSocket connection closed and cleaned up for client %s", ws.client)
|
||||
99
backend/src/api/sniffer_api.py
Normal file
99
backend/src/api/sniffer_api.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from src.network_sniffer import (
|
||||
get_sniffer_status,
|
||||
start_afpacket_sniffer,
|
||||
stop_afpacket_sniffer,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ------------------------------
|
||||
# Pydantic Models
|
||||
# ------------------------------
|
||||
|
||||
class SnifferStartRequest(BaseModel):
|
||||
"""
|
||||
Request model for starting the sniffer on a specific bridge.
|
||||
"""
|
||||
bridge: str = Field(..., example="br0", description="Name of the Linux bridge to sniff on")
|
||||
|
||||
|
||||
class SnifferStartResponse(BaseModel):
|
||||
"""
|
||||
Response model returned when sniffer starts successfully.
|
||||
"""
|
||||
started: bool = Field(..., description="Whether the sniffer was started successfully")
|
||||
bridge: str = Field(..., description="Bridge where the sniffer was started")
|
||||
|
||||
|
||||
class SnifferStopResponse(BaseModel):
|
||||
"""
|
||||
Response model returned when the sniffer stops successfully.
|
||||
"""
|
||||
stopped: bool = Field(..., description="Whether the sniffer was stopped successfully")
|
||||
|
||||
|
||||
class InterfaceSnifferStatus(BaseModel):
|
||||
"""
|
||||
Status of an individual interface monitored by the AF_PACKET sniffer.
|
||||
"""
|
||||
running: bool = Field(..., description="Whether the sniffer thread is active")
|
||||
exists: bool = Field(..., description="Whether the interface exists in /sys/class/net")
|
||||
up: bool = Field(..., description="Whether the interface is operationally UP")
|
||||
|
||||
|
||||
class SnifferStatusResponse(BaseModel):
|
||||
"""
|
||||
Response model for the sniffer status endpoint.
|
||||
"""
|
||||
interfaces: Dict[str, InterfaceSnifferStatus] = Field(
|
||||
..., description="Map of interface names to their sniffer status"
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------
|
||||
# Endpoints
|
||||
# ------------------------------
|
||||
|
||||
@router.post("/sniffer/start", response_model=SnifferStartResponse)
|
||||
def sniffer_start(req: SnifferStartRequest):
|
||||
"""
|
||||
Start the AF_PACKET sniffer for the given bridge.
|
||||
"""
|
||||
try:
|
||||
start_afpacket_sniffer(req.bridge)
|
||||
return SnifferStartResponse(started=True, bridge=req.bridge)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to start sniffer: {exc}")
|
||||
|
||||
|
||||
@router.post("/sniffer/stop", response_model=SnifferStopResponse)
|
||||
def sniffer_stop():
|
||||
"""
|
||||
Stop the AF_PACKET sniffer (if running).
|
||||
"""
|
||||
try:
|
||||
stop_afpacket_sniffer()
|
||||
return SnifferStopResponse(stopped=True)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to stop sniffer: {exc}")
|
||||
|
||||
|
||||
@router.get("/sniffer/status", response_model=SnifferStatusResponse)
|
||||
def sniffer_status():
|
||||
"""
|
||||
Return the sniffer status information.
|
||||
"""
|
||||
try:
|
||||
raw = get_sniffer_status()
|
||||
# Convert raw dict → typed model
|
||||
typed = {
|
||||
k: InterfaceSnifferStatus(**v)
|
||||
for k, v in raw.items()
|
||||
}
|
||||
return SnifferStatusResponse(interfaces=typed)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to query sniffer status: {exc}")
|
||||
Reference in New Issue
Block a user