Add concurrent tcpdump counter for data frame comparison
- Run tcpdump concurrently with scapy to count data frames - Use BPF filter (wlan[0] & 0x0C == 0x08) to filter data frames only - Display comparison between scapy and tcpdump data frame counts - Show capture ratio to identify if scapy is missing packets - Add re import for regex parsing of tcpdump output Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e52cd355e7
commit
fc9b8c7f00
102
wifi_monitor.py
102
wifi_monitor.py
|
|
@ -18,6 +18,7 @@ Usage:
|
|||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
|
@ -419,12 +420,21 @@ class CaptureAnalyzer:
|
|||
return dt.strftime("%H:%M:%S.%f")[:-3] # Truncate to milliseconds
|
||||
return "N/A"
|
||||
|
||||
def analyze(self, packets, duration):
|
||||
def analyze(self, packets, duration, tcpdump_data_count=None):
|
||||
"""Analyze captured packets and generate statistics."""
|
||||
print("\n=== Capture Statistics ===")
|
||||
|
||||
total_count = len(packets)
|
||||
print(f"Total packets captured: {total_count}")
|
||||
print(f"Total packets captured (scapy): {total_count}")
|
||||
|
||||
if tcpdump_data_count is not None:
|
||||
print(f"Data frames captured (tcpdump): {tcpdump_data_count}")
|
||||
if total_count > 0:
|
||||
scapy_data_count = sum(1 for pkt in packets if pkt.haslayer(Dot11) and pkt[Dot11].type == 2)
|
||||
print(f"Data frames captured (scapy): {scapy_data_count}")
|
||||
if tcpdump_data_count > 0:
|
||||
ratio = scapy_data_count / tcpdump_data_count
|
||||
print(f"Scapy capture ratio: {ratio:.1%} ({scapy_data_count}/{tcpdump_data_count})")
|
||||
|
||||
if total_count == 0:
|
||||
self._print_no_packets_message()
|
||||
|
|
@ -443,7 +453,10 @@ class CaptureAnalyzer:
|
|||
self._print_phy_histograms(packets)
|
||||
self._print_frame_type_breakdown(packets)
|
||||
self._print_data_frame_analysis(packets)
|
||||
self._print_summary(total_count, plcp_count)
|
||||
|
||||
# Calculate scapy data frame count for summary
|
||||
scapy_data_count = sum(1 for pkt in packets if pkt.haslayer(Dot11) and pkt[Dot11].type == 2)
|
||||
self._print_summary(total_count, plcp_count, tcpdump_data_count, scapy_data_count)
|
||||
|
||||
def _print_no_packets_message(self):
|
||||
"""Print message when no packets are captured."""
|
||||
|
|
@ -643,11 +656,13 @@ class CaptureAnalyzer:
|
|||
return subtype_names.get(subtype, f"Unknown ({subtype})")
|
||||
|
||||
|
||||
def _print_summary(self, total_count, plcp_count):
|
||||
def _print_summary(self, total_count, plcp_count, tcpdump_data_count=None, scapy_data_count=0):
|
||||
"""Print summary statistics."""
|
||||
print("=== Summary ===")
|
||||
if total_count > 0:
|
||||
print(f"✓ Monitor mode is working! Captured {total_count} packet(s)")
|
||||
print(f"✓ Monitor mode is working! Captured {total_count} packet(s) (scapy)")
|
||||
if tcpdump_data_count is not None and tcpdump_data_count > 0:
|
||||
print(f"✓ Data frames: {scapy_data_count} (scapy) vs {tcpdump_data_count} (tcpdump)")
|
||||
if plcp_count > 0:
|
||||
print(f"✓ PLCP headers detected: {plcp_count} packet(s) with radiotap information")
|
||||
else:
|
||||
|
|
@ -675,7 +690,7 @@ class PacketCapture:
|
|||
print(f"File size: {file_size} bytes")
|
||||
print()
|
||||
|
||||
self.analyzer.analyze(packets, 1.0)
|
||||
self.analyzer.analyze(packets, 1.0, None)
|
||||
return 0
|
||||
except OSError as e:
|
||||
print(f"Error reading PCAP file: {e}")
|
||||
|
|
@ -760,6 +775,72 @@ class PacketCapture:
|
|||
|
||||
return True
|
||||
|
||||
async def _run_tcpdump_counter(self, interface, duration):
|
||||
"""Run tcpdump concurrently to count data frames."""
|
||||
try:
|
||||
# Check if tcpdump is available
|
||||
check_proc = await asyncio.create_subprocess_exec(
|
||||
"which", "tcpdump",
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL
|
||||
)
|
||||
await check_proc.wait()
|
||||
if check_proc.returncode != 0:
|
||||
return None
|
||||
|
||||
# Use tcpdump with BPF filter for data frames
|
||||
# wlan[0] & 0x0C extracts the type field (bits 2-3)
|
||||
# Type 2 (data) = 0x08, so we check wlan[0] & 0x0C == 0x08
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"tcpdump",
|
||||
"-i", interface,
|
||||
"-n", # Don't resolve addresses
|
||||
"-q", # Quiet mode (less verbose)
|
||||
"-c", "1000000", # Large count limit
|
||||
"wlan[0] & 0x0C == 0x08", # BPF filter: type == 2 (data frames)
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
|
||||
# Wait for duration, then terminate
|
||||
await asyncio.sleep(duration)
|
||||
|
||||
# Terminate tcpdump gracefully
|
||||
try:
|
||||
proc.terminate()
|
||||
await asyncio.wait_for(proc.wait(), timeout=2.0)
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
|
||||
# Read stderr (tcpdump outputs packet count to stderr)
|
||||
_, stderr = await proc.communicate()
|
||||
stderr_text = stderr.decode() if stderr else ""
|
||||
|
||||
# Parse tcpdump output for packet count
|
||||
# Format: "X packets captured" or "X packets received by filter"
|
||||
data_frame_count = 0
|
||||
|
||||
for line in stderr_text.splitlines():
|
||||
# Look for "X packets captured" or "X packets received by filter"
|
||||
match = re.search(r'(\d+)\s+packets?\s+(captured|received by filter)', line, re.IGNORECASE)
|
||||
if match:
|
||||
data_frame_count = int(match.group(1))
|
||||
break
|
||||
# Also try simpler pattern
|
||||
match = re.search(r'(\d+)\s+packets?', line, re.IGNORECASE)
|
||||
if match and "dropped" not in line.lower() and "packet" in line.lower():
|
||||
potential_count = int(match.group(1))
|
||||
if potential_count > data_frame_count:
|
||||
data_frame_count = potential_count
|
||||
|
||||
return data_frame_count if data_frame_count > 0 else None
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Warning: tcpdump counter failed: {e}")
|
||||
return None
|
||||
|
||||
async def _main_capture_async(self, interface, duration):
|
||||
"""Perform the main packet capture (async)."""
|
||||
print(f"\n=== Starting scapy capture ({duration} seconds) ===")
|
||||
|
|
@ -779,6 +860,10 @@ class PacketCapture:
|
|||
pcap_file.close()
|
||||
print(f"Capturing to file: {pcap_path}")
|
||||
|
||||
# Start tcpdump counter concurrently
|
||||
print("Starting concurrent tcpdump counter for data frames...")
|
||||
tcpdump_task = asyncio.create_task(self._run_tcpdump_counter(interface, duration))
|
||||
|
||||
capture_error = None
|
||||
try:
|
||||
# Run blocking sniff in executor
|
||||
|
|
@ -816,7 +901,10 @@ class PacketCapture:
|
|||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
self.analyzer.analyze(packets, duration)
|
||||
# Get tcpdump count
|
||||
tcpdump_data_count = await tcpdump_task
|
||||
|
||||
self.analyzer.analyze(packets, duration, tcpdump_data_count)
|
||||
|
||||
if capture_error:
|
||||
return 1
|
||||
|
|
|
|||
Loading…
Reference in New Issue