diff --git a/wifi_monitor.py b/wifi_monitor.py index 8317fa3..caabf4a 100755 --- a/wifi_monitor.py +++ b/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