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 argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
@ -419,12 +420,21 @@ class CaptureAnalyzer:
|
||||||
return dt.strftime("%H:%M:%S.%f")[:-3] # Truncate to milliseconds
|
return dt.strftime("%H:%M:%S.%f")[:-3] # Truncate to milliseconds
|
||||||
return "N/A"
|
return "N/A"
|
||||||
|
|
||||||
def analyze(self, packets, duration):
|
def analyze(self, packets, duration, tcpdump_data_count=None):
|
||||||
"""Analyze captured packets and generate statistics."""
|
"""Analyze captured packets and generate statistics."""
|
||||||
print("\n=== Capture Statistics ===")
|
print("\n=== Capture Statistics ===")
|
||||||
|
|
||||||
total_count = len(packets)
|
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:
|
if total_count == 0:
|
||||||
self._print_no_packets_message()
|
self._print_no_packets_message()
|
||||||
|
|
@ -443,7 +453,10 @@ class CaptureAnalyzer:
|
||||||
self._print_phy_histograms(packets)
|
self._print_phy_histograms(packets)
|
||||||
self._print_frame_type_breakdown(packets)
|
self._print_frame_type_breakdown(packets)
|
||||||
self._print_data_frame_analysis(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):
|
def _print_no_packets_message(self):
|
||||||
"""Print message when no packets are captured."""
|
"""Print message when no packets are captured."""
|
||||||
|
|
@ -643,11 +656,13 @@ class CaptureAnalyzer:
|
||||||
return subtype_names.get(subtype, f"Unknown ({subtype})")
|
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 statistics."""
|
||||||
print("=== Summary ===")
|
print("=== Summary ===")
|
||||||
if total_count > 0:
|
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:
|
if plcp_count > 0:
|
||||||
print(f"✓ PLCP headers detected: {plcp_count} packet(s) with radiotap information")
|
print(f"✓ PLCP headers detected: {plcp_count} packet(s) with radiotap information")
|
||||||
else:
|
else:
|
||||||
|
|
@ -675,7 +690,7 @@ class PacketCapture:
|
||||||
print(f"File size: {file_size} bytes")
|
print(f"File size: {file_size} bytes")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
self.analyzer.analyze(packets, 1.0)
|
self.analyzer.analyze(packets, 1.0, None)
|
||||||
return 0
|
return 0
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(f"Error reading PCAP file: {e}")
|
print(f"Error reading PCAP file: {e}")
|
||||||
|
|
@ -760,6 +775,72 @@ class PacketCapture:
|
||||||
|
|
||||||
return True
|
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):
|
async def _main_capture_async(self, interface, duration):
|
||||||
"""Perform the main packet capture (async)."""
|
"""Perform the main packet capture (async)."""
|
||||||
print(f"\n=== Starting scapy capture ({duration} seconds) ===")
|
print(f"\n=== Starting scapy capture ({duration} seconds) ===")
|
||||||
|
|
@ -779,6 +860,10 @@ class PacketCapture:
|
||||||
pcap_file.close()
|
pcap_file.close()
|
||||||
print(f"Capturing to file: {pcap_path}")
|
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
|
capture_error = None
|
||||||
try:
|
try:
|
||||||
# Run blocking sniff in executor
|
# Run blocking sniff in executor
|
||||||
|
|
@ -816,7 +901,10 @@ class PacketCapture:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
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:
|
if capture_error:
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue