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:
Robert McMahon 2026-02-13 15:47:50 -08:00
parent e52cd355e7
commit fc9b8c7f00
1 changed files with 95 additions and 7 deletions

View File

@ -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