test
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s

This commit is contained in:
2026-03-01 15:00:58 +01:00
parent e9ff417df1
commit 3fc5b6db48
2 changed files with 334 additions and 173 deletions

View File

@@ -8,6 +8,7 @@ import selectors
import errno
import struct
from typing import Dict, List, Optional, Any
from uuid import uuid4
# NOTE: ensure this path points to your shared runtime module
from src import shared_objects
@@ -36,14 +37,18 @@ from src.Models.ip_protocol import IPProtocolEnum, protocol_from_number
# ---- Logging ----------------------------------------------------------
logger = logging.getLogger("af_packet_sniffer")
# ---- Globals (kept minimal) -------------------------------------------
af_sockets: Dict[str, socket.socket] = {}
af_thread: Optional[threading.Thread] = None
af_stop_event: Optional[threading.Event] = None
fixed_bridge_ports: Dict[str, List[str]] = {}
current_bridge: Optional[str] = None
current_bridge_label_for_thread: Optional[str] = None
# ---- Session model -------------------------------------------
# sessions: session_id -> session dict
sessions: Dict[str, Dict[str, Any]] = {}
# session schema:
# {
# "thread": threading.Thread,
# "stop_event": threading.Event,
# "sockets": Dict[iface, socket.socket],
# "label": str,
# "is_bridge": bool,
# "ports": List[str],
# }
# small bounded buffer for packets produced before shared_objects is ready
_PACKET_BUFFER: List[Dict[str, Any]] = []
@@ -135,26 +140,142 @@ def parse_packet(pkt, bridge_label: str) -> None:
"dst_port": None,
}
# (packet parsing unchanged — omitted here for brevity in this snippet)
# --- copy the full packet parsing logic from your original file here ---
# For brevity in this repo patch, the original parse_packet body remains unchanged.
# Keep everything from "Ether in pkt" through the Raw fallback and DB scheduling exactly as you had.
# (In your codebase, ensure the same body is present, unchanged.)
# -----------------------------
# Ethernet layer
if Ether in pkt:
eth = pkt[Ether]
pkt_info["src_mac"] = _safe_get_attr(eth, "src")
pkt_info["dst_mac"] = _safe_get_attr(eth, "dst")
# Base ethertype
try:
eth_type_raw = int(eth.type)
except Exception:
eth_type_raw = None
# VLAN inner ethertype and vlan id if Dot1Q exists
if Dot1Q in pkt:
try:
inner = int(pkt[Dot1Q].type)
if inner:
eth_type_raw = inner
except Exception:
pass
try:
pkt_info["vlan_id"] = int(pkt[Dot1Q].vlan)
except Exception:
pkt_info["vlan_id"] = None
if eth_type_raw is not None:
pkt_info["eth_type_raw"] = eth_type_raw
try:
pkt_info["eth_type"] = ethertype_from_int(eth_type_raw)
except Exception:
pkt_info["eth_type"] = EtherTypeEnum.UNKNOWN
# ARP
if ARP in pkt:
arp = pkt[ARP]
pkt_info["protocol_name"] = "ARP"
pkt_info["src_ip"] = _safe_get_attr(arp, "psrc")
pkt_info["dst_ip"] = _safe_get_attr(arp, "pdst")
pkt_info["src_port"] = None
pkt_info["dst_port"] = None
# IPv4
if IP in pkt:
ip = pkt[IP]
pkt_info["src_ip"] = _safe_get_attr(ip, "src")
pkt_info["dst_ip"] = _safe_get_attr(ip, "dst")
try:
proto_num = int(_safe_get_attr(ip, "proto"))
except Exception:
proto_num = None
if proto_num is not None:
pkt_info["protocol_raw"] = proto_num
try:
pkt_info["protocol"] = protocol_from_number(proto_num)
except Exception:
pkt_info["protocol"] = IPProtocolEnum.UNKNOWN
if proto_num == 6 and TCP in pkt:
pkt_info["protocol_name"] = "TCP"
pkt_info["src_port"] = _safe_get_attr(pkt[TCP], "sport")
pkt_info["dst_port"] = _safe_get_attr(pkt[TCP], "dport")
elif proto_num == 17 and UDP in pkt:
pkt_info["protocol_name"] = "UDP"
pkt_info["src_port"] = _safe_get_attr(pkt[UDP], "sport")
pkt_info["dst_port"] = _safe_get_attr(pkt[UDP], "dport")
elif proto_num == 1 and ICMP in pkt:
pkt_info["protocol_name"] = "ICMP"
else:
if pkt_info["protocol_name"] is None:
pkt_info["protocol_name"] = f"IP_PROTO_{proto_num}" if proto_num is not None else None
# IPv6
if IPv6 in pkt:
ip6 = pkt[IPv6]
pkt_info["src_ip"] = _safe_get_attr(ip6, "src")
pkt_info["dst_ip"] = _safe_get_attr(ip6, "dst")
try:
nh = int(_safe_get_attr(ip6, "nh"))
except Exception:
nh = None
if nh is not None:
pkt_info["protocol_raw"] = nh
try:
pkt_info["protocol"] = protocol_from_number(nh)
except Exception:
pkt_info["protocol"] = IPProtocolEnum.UNKNOWN
if nh == 6 and TCP in pkt:
pkt_info["protocol_name"] = "TCP"
pkt_info["src_port"] = _safe_get_attr(pkt[TCP], "sport")
pkt_info["dst_port"] = _safe_get_attr(pkt[TCP], "dport")
elif nh == 17 and UDP in pkt:
pkt_info["protocol_name"] = "UDP"
pkt_info["src_port"] = _safe_get_attr(pkt[UDP], "sport")
pkt_info["dst_port"] = _safe_get_attr(pkt[UDP], "dport")
elif ICMPv6Unknown in pkt:
pkt_info["protocol_name"] = "ICMPv6"
else:
if pkt_info["protocol_name"] is None:
pkt_info["protocol_name"] = f"IPV6_PROTO_{nh}" if nh is not None else None
# Raw fallback label
if Raw in pkt and not pkt_info["protocol_name"]:
pkt_info["protocol_name"] = "RAW"
# Submit DB insert to shared web loop if available, otherwise buffer
try:
web_loop = getattr(shared_objects, "web_loop", None)
web_db = getattr(shared_objects, "db", None)
if web_db is not None and web_loop is not None:
asyncio.run_coroutine_threadsafe(web_db.insert_packet(pkt_info), web_loop)
try:
fut = asyncio.run_coroutine_threadsafe(web_db.insert_packet(pkt_info), web_loop)
# best-effort non-blocking check for immediate errors
try:
fut.result(timeout=0.005)
except Exception:
pass
logger.debug("Scheduled insert for packet on %s (len=%d)", pkt_info.get("iface"), pkt_info.get("length"))
except Exception as e:
logger.exception("Failed to schedule insert_packet for %s — buffering: %s", pkt_info.get("iface"), e)
_PACKET_BUFFER.append(pkt_info)
if len(_PACKET_BUFFER) > _BUFFER_CAPACITY:
_PACKET_BUFFER.pop(0)
else:
# buffer (bounded) until the web app initializes
_PACKET_BUFFER.append(pkt_info)
if len(_PACKET_BUFFER) == 1:
logger.info("Buffering packets; web DB not ready yet (first buffered packet on %s)", pkt_info.get("iface"))
if len(_PACKET_BUFFER) > _BUFFER_CAPACITY:
# drop oldest packet
_PACKET_BUFFER.pop(0)
logger.warning("Packet buffer full: dropped oldest packet")
except Exception:
logger.exception("Failed to schedule DB insert")
logger.exception("Failed to schedule DB insert (unexpected)")
# -------------------------
@@ -197,11 +318,12 @@ def _create_af_packet_socket(ifname: str, rx_buf: int = 4 * 1024 * 1024) -> Opti
return None
s.setblocking(False)
logger.info("AF_PACKET socket bound to %s (fd=%s)", ifname, getattr(s, "fileno", lambda: "?")())
return s
def _close_socket(ifname: str) -> None:
s = af_sockets.pop(ifname, None)
def _close_socket_in_dict(sockets: Dict[str, socket.socket], ifname: str) -> None:
s = sockets.pop(ifname, None)
if s:
try:
s.close()
@@ -209,52 +331,52 @@ def _close_socket(ifname: str) -> None:
pass
def _ensure_socket_for_iface(iface: str, bridge_label: str) -> None:
def _ensure_socket_for_session(sockets: Dict[str, socket.socket], iface: str, bridge_label: str) -> None:
"""
Ensure we have an AF_PACKET socket in af_sockets for iface.
Ensure we have an AF_PACKET socket in the given sockets dict for iface.
If socket doesn't exist, attempt to create it. bridge_label is used for logging/context.
"""
if iface in af_sockets:
if iface in sockets:
return
if not check_interface_exists(iface):
logger.warning("Interface %s does not exist; skipping socket creation", iface)
return
s = _create_af_packet_socket(iface)
if s:
af_sockets[iface] = s
sockets[iface] = s
logger.info("AF_PACKET socket created for %s (label=%s)", iface, bridge_label)
else:
logger.warning("Failed to create AF_PACKET socket for %s (label=%s)", iface, bridge_label)
# -------------------------
# Main reader thread
# Per-session reader loop
# -------------------------
def afpacket_reader_loop(bridge_label: str, stop_event: threading.Event) -> None:
"""
Multiplex AF_PACKET sockets using a selector and hand packets to parse_packet.
Uses the fixed_bridge_ports snapshot to decide which interfaces to open when bridge-mode.
If started in interface-mode, fixed_bridge_ports may contain a single entry mapping the
interface name to itself (or None), but af_sockets drives which sockets are active.
"""
logger.info("AF_PACKET reader starting (label=%s)", bridge_label)
def _session_reader_loop(session_id: str) -> None:
session = sessions.get(session_id)
if not session:
logger.warning("Session %s not found at reader start", session_id)
return
stop_event: threading.Event = session["stop_event"]
sockets: Dict[str, socket.socket] = session["sockets"]
label: str = session["label"]
logger.info("Session %s reader starting (label=%s)", session_id, label)
sel = selectors.DefaultSelector()
# If there is a snapshot for this label, create sockets for snapshot ports.
for iface in fixed_bridge_ports.get(bridge_label, []):
_ensure_socket_for_iface(iface, bridge_label)
# register existing sockets
for iface, s in list(af_sockets.items()):
for iface, s in list(sockets.items()):
try:
sel.register(s, selectors.EVENT_READ, data=iface)
except Exception:
logger.debug("Register failed for %s (continuing)", iface)
logger.debug("Register failed for %s in session %s (continuing)", iface, session_id)
last_drain_ts = time.time()
try:
while not stop_event.is_set():
# ensure newly created sockets are registered
for iface, s in list(af_sockets.items()):
for iface, s in list(sockets.items()):
try:
if not any(k.fileobj is s for k in sel.get_map().values()):
sel.register(s, selectors.EVENT_READ, data=iface)
@@ -264,11 +386,18 @@ def afpacket_reader_loop(bridge_label: str, stop_event: threading.Event) -> None
try:
events = sel.select(timeout=1.0)
except Exception:
logger.exception("Selector error")
logger.exception("Selector error in session %s", session_id)
time.sleep(0.1)
continue
if not events:
# occasionally attempt to drain buffer if DB becomes available
if time.time() - last_drain_ts > 5:
try:
drain_buffer_to_shared_db()
except Exception:
logger.exception("Periodic drain failed in session %s", session_id)
last_drain_ts = time.time()
continue
for key, _ in events:
@@ -282,28 +411,29 @@ def afpacket_reader_loop(bridge_label: str, stop_event: threading.Event) -> None
continue
except OSError as e:
if e.errno in (errno.ENODEV, errno.ENETDOWN, errno.EBADF):
logger.warning("Socket error on %s: %s — closing", iface, e)
logger.warning("Socket error on %s in session %s: %s — closing", iface, session_id, e)
try:
sel.unregister(sock)
except Exception:
pass
_close_socket(iface)
_close_socket_in_dict(sockets, iface)
continue
else:
logger.exception("Recv error on %s", iface)
logger.exception("Recv error on %s in session %s", iface, session_id)
continue
# parse with scapy
try:
pkt = Ether(raw)
pkt.sniffed_on = iface
parse_packet(pkt, bridge_label)
parse_packet(pkt, label)
logger.debug("Captured packet on %s in session %s (len=%d)", iface, session_id, len(raw))
except Exception:
logger.exception("Failed to parse/process packet from %s", iface)
logger.exception("Failed to parse/process packet from %s in session %s", iface, session_id)
continue
finally:
logger.info("AF_PACKET reader stopping; cleaning up sockets")
# unregister and close
logger.info("Session %s reader stopping; cleaning up sockets", session_id)
# unregister and close sockets
try:
for key in list(sel.get_map().values()):
try:
@@ -313,123 +443,106 @@ def afpacket_reader_loop(bridge_label: str, stop_event: threading.Event) -> None
except Exception:
pass
for iface in list(af_sockets.keys()):
_close_socket(iface)
for iface in list(sockets.keys()):
_close_socket_in_dict(sockets, iface)
try:
sel.close()
except Exception:
pass
logger.info("AF_PACKET reader stopped")
logger.info("Session %s reader stopped", session_id)
# -------------------------
# Public API: start/stop/status
# -------------------------
def start_afpacket_sniffer(target: str, target_is_interface: bool = False) -> None:
def start_afpacket_sniffer(target: str, target_is_interface: bool = False) -> str:
"""
Start the sniffer.
Start a sniffer session. Returns session_id string.
If target_is_interface == False: target is treated as bridge name; snapshot ports and start reader
for that bridge (existing behavior).
If target_is_interface == True: target is treated as interface name; create socket for interface
and start reader if not running.
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.
"""
global af_thread, af_stop_event, current_bridge, current_bridge_label_for_thread
if af_thread and af_thread.is_alive():
logger.info("Sniffer thread already running — adding sockets if needed")
session_id = str(uuid4())
session: Dict[str, Any] = {
"stop_event": threading.Event(),
"sockets": {},
"label": target,
"is_bridge": not target_is_interface,
"ports": [],
}
sessions[session_id] = session
# If thread already running and target is interface -> try to add socket only
if target_is_interface:
_ensure_socket_for_iface(target, target)
else:
# bridge-mode: snapshot ports and ensure sockets for those ports
ports = get_bridge_ports_once(target)
fixed_bridge_ports[target] = ports
logger.info("Fixed bridge ports updated for %s: %s", target, ports)
for p in ports:
_ensure_socket_for_iface(p, target)
return
# Thread not running — set up snapshot or single interface and start thread
# determine ports for this session
if target_is_interface:
# ensure socket for the single interface
_ensure_socket_for_iface(target, target)
current_bridge = target # use target as label
current_bridge_label_for_thread = target
fixed_bridge_ports[target] = [target] # optional snapshot entry
ports = [target]
else:
ports = get_bridge_ports_once(target)
fixed_bridge_ports[target] = ports
current_bridge = target
current_bridge_label_for_thread = target
logger.info("Fixed bridge ports for %s: %s", target, ports)
# create sockets now
for iface in ports:
_ensure_socket_for_iface(iface, target)
af_stop_event = threading.Event()
af_thread = threading.Thread(target=afpacket_reader_loop, args=(current_bridge_label_for_thread or target, af_stop_event), daemon=True)
af_thread.start()
logger.info("AF_PACKET sniffer started (label=%s)", current_bridge_label_for_thread or target)
session["ports"] = ports
# create sockets for this session only
for iface in ports:
if not check_interface_exists(iface):
logger.warning("Snapshot port %s missing for session %s, skipping", iface, session_id)
continue
s = _create_af_packet_socket(iface)
if s:
session["sockets"][iface] = s
# start dedicated thread
t = threading.Thread(target=_session_reader_loop, args=(session_id,), daemon=True)
session["thread"] = t
t.start()
logger.info("Started sniffer session %s label=%s ports=%s", session_id, target, ports)
return session_id
def stop_afpacket_sniffer(target: Optional[str] = None, target_is_interface: bool = False) -> None:
def stop_afpacket_sniffer(session_id: Optional[str] = None, target: Optional[str] = None, target_is_interface: bool = False) -> None:
"""
Stop the sniffer or a subset:
- If no target: stop the whole reader thread and clear snapshot (existing behavior).
- If target_is_interface True: close the socket for that interface (do not stop thread).
- If target provided and target_is_interface False: remove snapshot for the bridge and close sockets for ports in that snapshot.
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.
- Else if target provided: remove sockets associated with target from sessions (close sockets).
- Else if no args: stop all sessions (global stop).
"""
global af_thread, af_stop_event, current_bridge, current_bridge_label_for_thread
# Stop a single interface
if target is not None and target_is_interface:
_close_socket(target)
# Also remove from any fixed_bridge_ports entries where present
for br, ports in list(fixed_bridge_ports.items()):
if target in ports:
# Stop by session_id
if session_id:
s = sessions.pop(session_id, None)
if not s:
logger.warning("Requested stop for unknown session %s", session_id)
return
s["stop_event"].set()
t = s.get("thread")
if t and isinstance(t, threading.Thread):
t.join(timeout=2)
logger.info("Stopped session %s", session_id)
return
# Stop by target (interface or bridge)
if target:
# For interface-mode: close that iface in any session that has it
for sid, s in list(sessions.items()):
if target in s.get("ports", []):
# close only that iface socket in that session
sock = s["sockets"].pop(target, None)
if sock:
try:
sock.close()
except Exception:
pass
try:
fixed_bridge_ports[br].remove(target)
s["ports"].remove(target)
except Exception:
pass
logger.info("Stopped sniffer on interface %s (socket closed)", target)
logger.info("Removed target %s from session %s", target, sid)
return
# Stop a bridge snapshot: close sockets for snapshot ports and remove snapshot
if target is not None and not target_is_interface:
ports = fixed_bridge_ports.pop(target, [])
for p in ports:
_close_socket(p)
# If we stopped the bridge that the running thread was for, also clear current_bridge
if current_bridge == target:
current_bridge = None
current_bridge_label_for_thread = None
logger.info("Stopped sniffer for bridge %s (closed ports: %s)", target, ports)
return
# Global stop: stop all sessions
for sid in list(sessions.keys()):
stop_afpacket_sniffer(session_id=sid)
# Global stop (existing behavior)
if not af_thread:
return
if af_stop_event:
af_stop_event.set()
af_thread.join(timeout=2)
af_thread = None
af_stop_event = None
# clear all snapshots
try:
for br in list(fixed_bridge_ports.keys()):
fixed_bridge_ports.pop(br, None)
except Exception:
pass
current_bridge = None
current_bridge_label_for_thread = None
# close db pool if shared.web_loop is available; otherwise leave to main
# attempt to close db pool if shared.web_loop is available; otherwise leave to main
try:
web_loop = getattr(shared_objects, "web_loop", None)
web_db = getattr(shared_objects, "db", None)
@@ -438,19 +551,46 @@ def stop_afpacket_sniffer(target: Optional[str] = None, target_is_interface: boo
except Exception:
logger.exception("Failed to schedule DB pool close")
logger.info("AF_PACKET sniffer stopped")
logger.info("All sniffer sessions stopped")
def get_sniffer_status() -> Dict[str, Dict[str, object]]:
"""
Return simple status per managed interface.
Return simple status per managed interface, including which session owns it.
Output format:
{
"<iface>": {"running": True, "exists": True, "up": True, "session_id": "<uuid>", "session_label": "..."}
}
"""
running = af_thread.is_alive() if af_thread else False
out: Dict[str, Dict[str, object]] = {}
for iface in list(af_sockets.keys()):
out[iface] = {
"running": running,
"exists": check_interface_exists(iface),
"up": check_interface_up(iface),
}
return out
# build map iface -> session
for sid, s in sessions.items():
running = s.get("thread") is not None and s.get("thread").is_alive()
for iface in s.get("sockets", {}).keys():
out[iface] = {
"running": running,
"exists": check_interface_exists(iface),
"up": check_interface_up(iface),
"session_id": sid,
"session_label": s.get("label"),
}
return out
def get_internal_debug_state() -> dict:
"""
Return internal debug information.
"""
return {
"sessions": {
sid: {
"label": s.get("label"),
"is_bridge": s.get("is_bridge"),
"ports": list(s.get("ports", [])),
"sockets": list(s.get("sockets", {}).keys()),
"thread_alive": bool(s.get("thread") and s.get("thread").is_alive()),
}
for sid, s in sessions.items()
},
"buffer_len": len(_PACKET_BUFFER),
}