From 3cb36bff4b616d2f2c51d9c63d2ecfa10592e413 Mon Sep 17 00:00:00 2001 From: Robert McMahon Date: Fri, 13 Feb 2026 14:58:39 -0800 Subject: [PATCH] Replace bash/awk parsing with Python script for robust tshark output parsing - Created parse_tshark_pcap.py to handle tshark field extraction - Python script gracefully handles missing fields and filters error messages - Replaced complex bash/awk parsing logic in test_monitor_tshark.sh - Fixes regression where stderr redirection prevented packet parsing - Python approach is more maintainable and robust Co-authored-by: Cursor --- parse_tshark_pcap.py | 365 ++++++++++++++++++++++++++++++++++++++++ test_monitor_tshark.sh | 370 ++--------------------------------------- 2 files changed, 376 insertions(+), 359 deletions(-) create mode 100755 parse_tshark_pcap.py diff --git a/parse_tshark_pcap.py b/parse_tshark_pcap.py new file mode 100755 index 0000000..28e4dc5 --- /dev/null +++ b/parse_tshark_pcap.py @@ -0,0 +1,365 @@ +#!/usr/bin/env python3 +""" +Parse tshark pcap file and extract 802.11 frame information. +This script handles missing fields gracefully and provides detailed statistics. +""" + +import sys +import subprocess +import collections +from typing import List, Dict, Tuple, Optional + +# Field indices (0-based) +FRAME_NUMBER = 0 +FRAME_TIME = 1 +WLAN_RA = 2 +WLAN_TA = 3 +WLAN_FC_TYPE = 4 +WLAN_FC_SUBTYPE = 5 +WLAN_FC_TYPE_SUBTYPE = 6 +WLAN_FC_PROTECTED = 7 +WLAN_FC_RETRY = 8 +WLAN_DURATION = 9 +RADIOTAP_PRESENT = 10 +RADIOTAP_DATARATE = 11 +RADIOTAP_MCS_INDEX = 12 +WLAN_RADIO_DATA_RATE = 13 +WLAN_RADIO_MCS_INDEX = 14 + +def safe_get_field(fields: List[str], index: int, default: str = "N/A") -> str: + """Safely get a field value, handling missing or empty fields.""" + if index < len(fields): + value = fields[index].strip() + if value and value != "-" and value != "": + return value + return default + +def parse_tshark_output(pcap_file: str) -> List[List[str]]: + """Extract fields from pcap file using tshark.""" + fields = [ + "frame.number", + "frame.time", + "wlan.ra", + "wlan.ta", + "wlan.fc.type", + "wlan.fc.subtype", + "wlan.fc.type_subtype", + "wlan.fc.protected", + "wlan.fc.retry", + "wlan.duration", + "radiotap.present", + "radiotap.datarate", + "radiotap.mcs.index", + "wlan_radio.data_rate", + "wlan_radio.mcs.index", + ] + + cmd = [ + "tshark", "-q", "-r", pcap_file, "-n", "-T", "fields" + ] + for field in fields: + cmd.extend(["-e", field]) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False # Don't fail on errors + ) + + # Filter out error messages and status lines + lines = [] + for line in result.stdout.splitlines(): + line = line.strip() + # Skip empty lines and tshark status messages + if not line: + continue + if line.startswith("Running as") or line.startswith("Capturing"): + continue + if "tshark:" in line.lower() or "packets captured" in line.lower(): + continue + # Only process lines that start with a number (frame number) + if line and line[0].isdigit(): + fields = line.split("\t") + if len(fields) > 0: + lines.append(fields) + + return lines + + except Exception as e: + print(f"Error running tshark: {e}", file=sys.stderr) + return [] + +def count_packets(packets: List[List[str]]) -> int: + """Count total valid packets.""" + return len(packets) + +def count_plcp_headers(packets: List[List[str]]) -> int: + """Count packets with PLCP headers (radiotap present).""" + count = 0 + for packet in packets: + radiotap = safe_get_field(packet, RADIOTAP_PRESENT, "") + if radiotap and radiotap != "0" and radiotap != "N/A" and radiotap != "-": + count += 1 + return count + +def get_unique_ra_ta_pairs(packets: List[List[str]]) -> Dict[str, int]: + """Count unique RA/TA pairs.""" + pairs = collections.Counter() + for packet in packets: + ra = safe_get_field(packet, WLAN_RA, "N/A") + ta = safe_get_field(packet, WLAN_TA, "N/A") + if ra != "N/A" or ta != "N/A": + pair = f"{ra} -> {ta}" + pairs[pair] += 1 + return dict(pairs) + +def get_phy_rate(packet: List[str]) -> Optional[str]: + """Extract PHY rate from packet (try radiotap first, then wlan_radio).""" + rate = safe_get_field(packet, RADIOTAP_DATARATE, "") + if rate and rate != "0" and rate != "N/A" and rate != "-": + return rate + rate = safe_get_field(packet, WLAN_RADIO_DATA_RATE, "") + if rate and rate != "0" and rate != "N/A" and rate != "-": + return rate + return None + +def get_mcs_index(packet: List[str]) -> Optional[str]: + """Extract MCS index from packet (try radiotap first, then wlan_radio).""" + mcs = safe_get_field(packet, RADIOTAP_MCS_INDEX, "") + if mcs and mcs != "0" and mcs != "255" and mcs != "N/A" and mcs != "-": + return mcs + mcs = safe_get_field(packet, WLAN_RADIO_MCS_INDEX, "") + if mcs and mcs != "0" and mcs != "255" and mcs != "N/A" and mcs != "-": + return mcs + return None + +def get_histograms_per_pair(packets: List[List[str]]) -> Dict[str, Dict[str, Dict[str, int]]]: + """Generate PHY rate and MCS histograms per RA/TA pair.""" + histograms = {} # {pair: {"rate": {rate: count}, "mcs": {mcs: count}}} + + for packet in packets: + ra = safe_get_field(packet, WLAN_RA, "N/A") + ta = safe_get_field(packet, WLAN_TA, "N/A") + if ra == "N/A" and ta == "N/A": + continue + + pair = f"{ra} -> {ta}" + frame_type = safe_get_field(packet, WLAN_FC_TYPE, "") + + # Only process data frames (type 2) for histograms + if frame_type != "2": + continue + + if pair not in histograms: + histograms[pair] = {"rate": collections.Counter(), "mcs": collections.Counter()} + + # Get PHY rate + rate = get_phy_rate(packet) + if rate: + histograms[pair]["rate"][rate] += 1 + + # Get MCS index + mcs = get_mcs_index(packet) + if mcs: + histograms[pair]["mcs"][mcs] += 1 + + return histograms + +def get_frame_type_breakdown(packets: List[List[str]]) -> Dict[str, int]: + """Count frames by type.""" + breakdown = collections.Counter() + for packet in packets: + frame_type = safe_get_field(packet, WLAN_FC_TYPE, "unknown") + type_name = "Unknown" + if frame_type == "0": + type_name = "Management" + elif frame_type == "1": + type_name = "Control" + elif frame_type == "2": + type_name = "Data" + breakdown[type_name] += 1 + return dict(breakdown) + +def get_data_frame_analysis(packets: List[List[str]]) -> Tuple[int, int, int]: + """Analyze data frames (QoS Data frames, subtype 8).""" + data_frames = [] + for packet in packets: + frame_type = safe_get_field(packet, WLAN_FC_TYPE, "") + frame_subtype = safe_get_field(packet, WLAN_FC_SUBTYPE, "") + if frame_type == "2" and frame_subtype == "8": # QoS Data frames + data_frames.append(packet) + + encrypted = 0 + unencrypted = 0 + for packet in data_frames: + protected = safe_get_field(packet, WLAN_FC_PROTECTED, "") + if protected == "1" or protected == "1.0": + encrypted += 1 + elif protected and protected != "-" and protected != "N/A": + unencrypted += 1 + + return len(data_frames), encrypted, unencrypted + +def format_sample_packet(packet: List[str], index: int) -> str: + """Format a packet for display.""" + frame_num = safe_get_field(packet, FRAME_NUMBER, str(index + 1)) + ra = safe_get_field(packet, WLAN_RA, "N/A") + ta = safe_get_field(packet, WLAN_TA, "N/A") + frame_type = safe_get_field(packet, WLAN_FC_TYPE, "N/A") + frame_subtype = safe_get_field(packet, WLAN_FC_SUBTYPE, "N/A") + protected = safe_get_field(packet, WLAN_FC_PROTECTED, "") + retry = safe_get_field(packet, WLAN_FC_RETRY, "") + duration = safe_get_field(packet, WLAN_DURATION, "N/A") + radiotap = safe_get_field(packet, RADIOTAP_PRESENT, "") + + protected_str = "encrypted" if (protected == "1" or protected == "1.0") else "unencrypted" + retry_str = " [retry]" if (retry == "1" or retry == "1.0") else "" + plcp_str = "yes" if (radiotap == "1" or radiotap == "1.0") else ("no" if radiotap != "N/A" and radiotap != "-" else "N/A") + + return f" Frame {frame_num}: RA={ra}, TA={ta}, type={frame_type}/{frame_subtype}, {protected_str}, dur={duration}, PLCP={plcp_str}{retry_str}" + +def main(): + if len(sys.argv) < 2: + print("Usage: parse_tshark_pcap.py [duration_seconds] [raw_packet_count]", file=sys.stderr) + sys.exit(1) + + pcap_file = sys.argv[1] + duration = float(sys.argv[2]) if len(sys.argv) > 2 else 10.0 + raw_packet_count = int(sys.argv[3]) if len(sys.argv) > 3 else 0 + + print("=== Capture Statistics ===") + + # Parse packets + packets = parse_tshark_output(pcap_file) + final_count = count_packets(packets) + plcp_count = count_plcp_headers(packets) + + # Check for parsing issues + if final_count < raw_packet_count and raw_packet_count > 10: + print(f"Warning: Parsed {final_count} packets but pcap file contains {raw_packet_count} packets") + print(" This may indicate field extraction issues.") + print() + + # Display basic stats + print(f"Total packets captured: {final_count}") + print(f"PLCP headers: {plcp_count}") + if final_count > 0: + rate = final_count / duration + print(f"Packet rate: {rate:.1f} packets/second") + print() + + if final_count == 0: + print("(No packets captured)") + print() + print("=== Summary ===") + print("✗ No packets captured. Check:") + print(f" 1. Is there WiFi traffic on the channel?") + print(f" 2. Is the interface actually in monitor mode?") + print(f" 3. Try a different channel or longer duration") + return + + # Display sample packets + print("Sample packets (first 10):") + for i, packet in enumerate(packets[:10]): + print(format_sample_packet(packet, i)) + print() + + # Unique RA/TA pairs + print("Unique RA/TA pairs (with counts):") + pairs = get_unique_ra_ta_pairs(packets) + if pairs: + for pair, count in sorted(pairs.items(), key=lambda x: x[1], reverse=True): + print(f" {pair}: {count} frame(s)") + else: + print(" (no valid RA/TA pairs found)") + print() + + # PHY rate and MCS histograms per RA/TA pair + print("PHY Rate and MCS Histograms per RA/TA pair:") + histograms = get_histograms_per_pair(packets) + for pair in sorted(histograms.keys()): + print(f"\n {pair}:") + + # PHY Rate histogram + print(" PHY Rate (Mbps):") + rate_hist = histograms[pair]["rate"] + if rate_hist: + for rate in sorted(rate_hist.keys(), key=lambda x: float(x) if x.replace(".", "").isdigit() else 0): + print(f" {rate} Mbps: {rate_hist[rate]} frame(s)") + else: + print(" (no PHY rate data)") + + # MCS histogram + print(" MCS Index:") + mcs_hist = histograms[pair]["mcs"] + if mcs_hist: + for mcs in sorted(mcs_hist.keys(), key=lambda x: int(x) if x.isdigit() else 0): + print(f" MCS {mcs}: {mcs_hist[mcs]} frame(s)") + else: + print(" (no MCS data)") + print() + + # Frame type breakdown + print("Frame type breakdown:") + breakdown = get_frame_type_breakdown(packets) + for frame_type, count in sorted(breakdown.items(), key=lambda x: x[1], reverse=True): + print(f" {frame_type}: {count} frame(s)") + print() + + # Data frame analysis + print("Data frame analysis (iperf typically uses QoS Data frames, subtype 8):") + data_count, encrypted, unencrypted = get_data_frame_analysis(packets) + print(f" QoS Data frames (type 2, subtype 8): {data_count}") + print(f" Encrypted: {encrypted}") + print(f" Unencrypted: {unencrypted}") + + if data_count > 0: + print(" Sample QoS Data frames (likely iperf traffic):") + data_frames = [p for p in packets if safe_get_field(p, WLAN_FC_TYPE, "") == "2" and safe_get_field(p, WLAN_FC_SUBTYPE, "") == "8"] + for i, packet in enumerate(data_frames[:5]): + frame_num = safe_get_field(packet, FRAME_NUMBER, str(i + 1)) + ra = safe_get_field(packet, WLAN_RA, "N/A") + ta = safe_get_field(packet, WLAN_TA, "N/A") + protected = safe_get_field(packet, WLAN_FC_PROTECTED, "") + retry = safe_get_field(packet, WLAN_FC_RETRY, "") + duration = safe_get_field(packet, WLAN_DURATION, "N/A") + protected_str = "encrypted" if (protected == "1" or protected == "1.0") else "unencrypted" + retry_str = " [retry]" if (retry == "1" or retry == "1.0") else "" + print(f" Frame {frame_num}: RA={ra}, TA={ta}, {protected_str}, dur={duration}{retry_str}") + print() + + # Frames involving server MAC + server_mac = "80:84:89:93:c4:b6" + print(f"Frames involving server MAC ({server_mac}):") + server_frames = [] + for packet in packets: + ra = safe_get_field(packet, WLAN_RA, "") + ta = safe_get_field(packet, WLAN_TA, "") + if ra == server_mac or ta == server_mac: + server_frames.append(packet) + + server_count = len(server_frames) + print(f" Total frames with server MAC: {server_count}") + if server_count > 0: + server_breakdown = get_frame_type_breakdown(server_frames) + print(" Frame type breakdown:") + for frame_type, count in sorted(server_breakdown.items(), key=lambda x: x[1], reverse=True): + print(f" {frame_type}: {count} frame(s)") + print(" Sample frames:") + for i, packet in enumerate(server_frames[:5]): + print(format_sample_packet(packet, i)) + print() + + # Summary + print("=== Summary ===") + if final_count > 0: + print(f"✓ Monitor mode is working! Captured {final_count} packet(s)") + if plcp_count > 0: + print(f"✓ PLCP headers detected: {plcp_count} packet(s) with radiotap information") + else: + print("⚠ No PLCP headers detected (may be using DLT_IEEE802_11 instead of DLT_IEEE802_11_RADIO)") + +if __name__ == "__main__": + main() diff --git a/test_monitor_tshark.sh b/test_monitor_tshark.sh index 2056b41..b8eed77 100755 --- a/test_monitor_tshark.sh +++ b/test_monitor_tshark.sh @@ -193,77 +193,16 @@ else exit 1 fi -# Now parse the pcap file to extract fields -# Only extract 802.11 header fields - data payloads are encrypted -# Include PHY rate and MCS for histograms -# Extract to temp file first to avoid pipe issues, then filter -TEMP_TSHARK_OUTPUT=$(mktemp /tmp/tshark_output_XXXXXX.txt) +# Now parse the pcap file using Python script (more robust than bash parsing) +# The Python script handles missing fields gracefully and provides detailed statistics +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PARSE_SCRIPT="${SCRIPT_DIR}/parse_tshark_pcap.py" -# First, verify we can read the pcap with a simple field extraction -SIMPLE_TEST=$(tshark -r "$TEMP_PCAP" -n -T fields -e frame.number 2>/dev/null | head -5 || true) -if [ -z "$SIMPLE_TEST" ]; then - echo "Warning: Could not read frame.number from pcap file" - echo " This may indicate a corrupted pcap or unsupported format" - echo "" -fi - -# Extract all fields (suppress stderr to avoid "Some fields aren't valid" messages) -# Use -q flag to suppress packet count output that interferes with parsing -tshark -q -r "$TEMP_PCAP" -n -T fields \ - -e frame.number \ - -e frame.time \ - -e wlan.ra \ - -e wlan.ta \ - -e wlan.fc.type \ - -e wlan.fc.subtype \ - -e wlan.fc.type_subtype \ - -e wlan.fc.protected \ - -e wlan.fc.retry \ - -e wlan.duration \ - -e radiotap.present \ - -e radiotap.datarate \ - -e radiotap.mcs.index \ - -e wlan_radio.data_rate \ - -e wlan_radio.mcs.index \ - 2>/dev/null > "$TEMP_TSHARK_OUTPUT" || true - -# Debug: Check what we actually got -TEMP_LINE_COUNT=$(wc -l < "$TEMP_TSHARK_OUTPUT" 2>/dev/null || echo "0") -if [ "$TEMP_LINE_COUNT" -eq 0 ]; then - echo "Warning: tshark field extraction produced no output" - echo " This may indicate an issue with the pcap file or field extraction" - echo " Try: tshark -r $TEMP_PCAP -T fields -e frame.number | head -5" - echo "" -fi - -# Filter out non-packet lines (keep only lines starting with frame numbers) -# Use the same approach as test capture: grep for lines starting with numbers, exclude status messages -# Filter out empty lines and lines that are just whitespace -CAPTURE_OUTPUT=$(grep -E '^[0-9]+' "$TEMP_TSHARK_OUTPUT" 2>/dev/null | grep -v -E '^[[:space:]]*$' | grep -v -E '(packets captured|Capturing on|Running as|tshark:)' || true) - -# Verify we got output before deleting temp file -if [ -z "$CAPTURE_OUTPUT" ] || [ "$(echo "$CAPTURE_OUTPUT" | wc -l)" -eq 0 ]; then - # If still empty but file has content, show debug info - if [ "$TEMP_LINE_COUNT" -gt 0 ]; then - echo "Warning: tshark output has $TEMP_LINE_COUNT lines but no valid packet data found" - if [ -n "$KEEP_PCAP" ]; then - echo " First 5 lines of raw output:" - head -5 "$TEMP_TSHARK_OUTPUT" | sed 's/\t//g' | head -5 - echo "" - echo " Last 5 lines of raw output:" - tail -5 "$TEMP_TSHARK_OUTPUT" | sed 's/\t//g' | tail -5 - echo "" - fi - fi -fi - -# Clean up temp file (keep for debugging if KEEP_PCAP is set) -if [ -z "$KEEP_PCAP" ]; then - rm -f "$TEMP_TSHARK_OUTPUT" -else - echo "Debug: Keeping tshark output file: $TEMP_TSHARK_OUTPUT" - echo " (Use: cat $TEMP_TSHARK_OUTPUT | head -20 to inspect)" - echo "" +# Check if Python script exists +if [ ! -f "$PARSE_SCRIPT" ]; then + echo "Error: Python parser script not found: $PARSE_SCRIPT" + echo " Falling back to basic tshark output" + exit 1 fi # Clean up temp file (unless KEEP_PCAP is set) @@ -278,293 +217,6 @@ fi # Force output flush sync -# Show stats immediately after capture completes +# Parse pcap file using Python script (more robust than bash/awk parsing) echo "" -echo "=== Capture Statistics ===" - -# Show any warnings/errors (but not the "Running as root" or "Capturing" messages) -WARNINGS=$(echo "$CAPTURE_OUTPUT" | grep -E "(tshark:|Warning|Error)" | grep -v -E "(Running as|Capturing)" | head -5 || true) -if [ -n "$WARNINGS" ]; then - echo "$WARNINGS" - echo "" -fi - -# Count total packets captured (lines starting with a number followed by tab) -# Filter out tshark status messages like "100 packets captured" or "Capturing on..." -# Only count lines that look like actual packet data: number, tab, then more fields -# Also handle lines that start with just a number (frame.number field) -PACKET_LINES=$(echo "$CAPTURE_OUTPUT" | grep -E '^[0-9]+' | grep -v -E '(packets captured|Capturing on|Running as|tshark:)' || true) -FINAL_COUNT=$(echo "$PACKET_LINES" | wc -l || echo "0") - -# If we got very few packets but raw count shows many, there might be a parsing issue -if [ "$FINAL_COUNT" -lt "$RAW_PACKET_COUNT" ] && [ "$RAW_PACKET_COUNT" -gt 10 ]; then - echo "Warning: Parsed $FINAL_COUNT packets but pcap file contains $RAW_PACKET_COUNT packets" - echo " This may indicate field extraction issues. Check tshark output above." -fi - -# Count packets with PLCP headers (radiotap present) -# radiotap.present is field 11 (after frame.number, frame.time, wlan.ra, wlan.ta, wlan.fc.type, wlan.fc.subtype, wlan.fc.type_subtype, wlan.fc.protected, wlan.fc.retry, wlan.duration) -PLCP_COUNT=$(echo "$PACKET_LINES" | awk -F'\t' 'NF >= 11 && $1 != "" && $11 != "" && $11 != "0" && $11 != "-" {count++} END {print count+0}' || echo "0") - -# Display stats immediately - always show these -echo "Total packets captured: $FINAL_COUNT" -echo "PLCP headers: $PLCP_COUNT" -if [ "$FINAL_COUNT" -gt 0 ]; then - # Calculate rate - RATE=$(awk "BEGIN {printf \"%.1f\", $FINAL_COUNT / $DURATION}") - echo "Packet rate: $RATE packets/second" -fi -echo "" - -# Display sample packets with readable format -if [ -n "$PACKET_LINES" ] && [ "$FINAL_COUNT" -gt 0 ]; then - echo "Sample packets (first 10):" - echo "$PACKET_LINES" | head -10 | awk -F'\t' '{ - ra = ($3 != "" && $3 != "-") ? $3 : "N/A" - ta = ($4 != "" && $4 != "-") ? $4 : "N/A" - type = ($5 != "" && $5 != "-") ? $5 : "N/A" - subtype = ($6 != "" && $6 != "-") ? $6 : "N/A" - protected = ($8 == "1" || $8 == "1.0") ? "encrypted" : "unencrypted" - retry = ($9 == "1" || $9 == "1.0") ? "retry" : "" - duration = ($10 != "" && $10 != "-") ? $10 : "N/A" - radiotap = ($11 == "1" || $11 == "1.0") ? "yes" : (($11 != "" && $11 != "-") ? "no" : "N/A") - retry_str = (retry != "") ? sprintf(" [%s]", retry) : "" - printf " Frame %s: RA=%s, TA=%s, type=%s/%s, %s, dur=%s, PLCP=%s%s\n", - $1, ra, ta, type, subtype, protected, duration, radiotap, retry_str - }' - echo "" - - # Count unique RA/TA pairs - echo "Unique RA/TA pairs (with counts):" - UNIQUE_PAIRS=$(echo "$PACKET_LINES" | awk -F'\t' '{ - ra = ($3 != "" && $3 != "-") ? $3 : "N/A" - ta = ($4 != "" && $4 != "-") ? $4 : "N/A" - if (ra != "N/A" || ta != "N/A") { - pair = ra " -> " ta - count[pair]++ - } - } - END { - for (pair in count) { - printf "%d\t%s\n", count[pair], pair - } - }' | sort -rn) - - if [ -n "$UNIQUE_PAIRS" ]; then - echo "$UNIQUE_PAIRS" | awk -F'\t' '{ - printf " %s: %d frame(s)\n", $2, $1 - }' - else - echo " (no valid RA/TA pairs found)" - fi - echo "" - - # Generate PHY rate and MCS histograms per RA/TA pair - echo "PHY Rate and MCS Histograms per RA/TA pair:" - echo "$PACKET_LINES" | awk -F'\t' '{ - ra = ($3 != "" && $3 != "-") ? $3 : "N/A" - ta = ($4 != "" && $4 != "-") ? $4 : "N/A" - if (ra != "N/A" || ta != "N/A") { - pair = ra " -> " ta - - # Get PHY rate (try radiotap.datarate first, then wlan_radio.data_rate) - phy_rate = "" - if ($12 != "" && $12 != "-" && $12 != "0") { - phy_rate = $12 - } else if ($14 != "" && $14 != "-" && $14 != "0") { - phy_rate = $14 - } - - # Get MCS index (try radiotap.mcs.index first, then wlan_radio.mcs.index) - mcs = "" - if ($13 != "" && $13 != "-" && $13 != "0" && $13 != "255") { - mcs = $13 - } else if ($15 != "" && $15 != "-" && $15 != "0" && $15 != "255") { - mcs = $15 - } - - # Only count data frames (type 2) for histograms - type = ($5 != "" && $5 != "-") ? $5 : "" - if (type == "2") { - if (phy_rate != "") { - rate_hist[pair][phy_rate]++ - } - if (mcs != "") { - mcs_hist[pair][mcs]++ - } - } - } - } - END { - # Process each pair - for (pair in rate_hist) { - printf "\n %s:\n", pair - - # PHY Rate histogram - printf " PHY Rate (Mbps):\n" - # Collect rates and sort - n = 0 - for (rate in rate_hist[pair]) { - rates[n++] = rate + 0 - rate_map[rate + 0] = rate - } - # Sort rates - for (i = 0; i < n; i++) { - for (j = i + 1; j < n; j++) { - if (rates[i] > rates[j]) { - tmp = rates[i] - rates[i] = rates[j] - rates[j] = tmp - } - } - } - for (i = 0; i < n; i++) { - rate_val = rates[i] - rate_str = rate_map[rate_val] - printf " %s Mbps: %d frame(s)\n", rate_str, rate_hist[pair][rate_str] - } - - # MCS histogram - if (pair in mcs_hist) { - printf " MCS Index:\n" - m = 0 - for (mcs in mcs_hist[pair]) { - mcs_vals[m++] = mcs + 0 - mcs_map[mcs + 0] = mcs - } - # Sort MCS - for (i = 0; i < m; i++) { - for (j = i + 1; j < m; j++) { - if (mcs_vals[i] > mcs_vals[j]) { - tmp = mcs_vals[i] - mcs_vals[i] = mcs_vals[j] - mcs_vals[j] = tmp - } - } - } - for (i = 0; i < m; i++) { - mcs_val = mcs_vals[i] - mcs_str = mcs_map[mcs_val] - printf " MCS %s: %d frame(s)\n", mcs_str, mcs_hist[pair][mcs_str] - } - } else { - printf " MCS Index: (no MCS data)\n" - } - } - }' - echo "" - - # Frame type breakdown - echo "Frame type breakdown:" - echo "$PACKET_LINES" | awk -F'\t' '{ - type = ($5 != "" && $5 != "-") ? $5 : "unknown" - subtype = ($6 != "" && $6 != "-") ? $6 : "unknown" - type_name = "Unknown" - if (type == "0") type_name = "Management" - else if (type == "1") type_name = "Control" - else if (type == "2") type_name = "Data" - count[type_name]++ - } - END { - for (t in count) { - printf " %s: %d frame(s)\n", t, count[t] - } - }' | sort -rn - echo "" - - # Analyze data frames (iperf uses QoS Data frames, subtype 8) - echo "Data frame analysis (iperf typically uses QoS Data frames, subtype 8):" - DATA_FRAMES=$(echo "$PACKET_LINES" | awk -F'\t' '{ - type = ($5 != "" && $5 != "-") ? $5 : "" - subtype = ($6 != "" && $6 != "-") ? $6 : "" - if (type == "2" && subtype == "8") { # QoS Data frames - print $0 - } - }') - DATA_COUNT=$(echo "$DATA_FRAMES" | wc -l || echo "0") - echo " QoS Data frames (type 2, subtype 8): $DATA_COUNT" - - # Count encrypted vs unencrypted data frames - ENCRYPTED_DATA=$(echo "$DATA_FRAMES" | awk -F'\t' '$8 == "1" || $8 == "1.0" {count++} END {print count+0}') - UNENCRYPTED_DATA=$(echo "$DATA_FRAMES" | awk -F'\t' '$8 != "1" && $8 != "1.0" && $8 != "" && $8 != "-" {count++} END {print count+0}') - echo " Encrypted: $ENCRYPTED_DATA" - echo " Unencrypted: $UNENCRYPTED_DATA" - - if [ "$DATA_COUNT" -gt 0 ]; then - echo " Sample QoS Data frames (likely iperf traffic):" - echo "$DATA_FRAMES" | head -5 | awk -F'\t' '{ - ra = ($3 != "" && $3 != "-") ? $3 : "N/A" - ta = ($4 != "" && $4 != "-") ? $4 : "N/A" - protected = ($8 == "1" || $8 == "1.0") ? "encrypted" : "unencrypted" - retry = ($9 == "1" || $9 == "1.0") ? "retry" : "" - duration = ($10 != "" && $10 != "-") ? $10 : "N/A" - retry_str = (retry != "") ? sprintf(" [%s]", retry) : "" - printf " Frame %s: RA=%s, TA=%s, %s, dur=%s%s\n", - $1, ra, ta, protected, duration, retry_str - }' - fi - echo "" - - # Frames involving server MAC (80:84:89:93:c4:b6) - echo "Frames involving server MAC (80:84:89:93:c4:b6):" - SERVER_MAC="80:84:89:93:c4:b6" - SERVER_FRAMES=$(echo "$PACKET_LINES" | awk -F'\t' -v mac="$SERVER_MAC" '{ - ra = ($3 != "" && $3 != "-") ? $3 : "" - ta = ($4 != "" && $4 != "-") ? $4 : "" - if (ra == mac || ta == mac) { - print $0 - } - }') - SERVER_COUNT=$(echo "$SERVER_FRAMES" | wc -l || echo "0") - echo " Total frames with server MAC: $SERVER_COUNT" - if [ "$SERVER_COUNT" -gt 0 ]; then - echo " Frame type breakdown:" - echo "$SERVER_FRAMES" | awk -F'\t' '{ - type = ($5 != "" && $5 != "-") ? $5 : "unknown" - subtype = ($6 != "" && $6 != "-") ? $6 : "unknown" - type_name = "Unknown" - if (type == "0") type_name = "Management" - else if (type == "1") type_name = "Control" - else if (type == "2") type_name = "Data" - count[type_name]++ - } - END { - for (t in count) { - printf " %s: %d frame(s)\n", t, count[t] - } - }' | sort -rn - echo " Sample frames:" - echo "$SERVER_FRAMES" | head -5 | awk -F'\t' '{ - ra = ($3 != "" && $3 != "-") ? $3 : "N/A" - ta = ($4 != "" && $4 != "-") ? $4 : "N/A" - type = ($5 != "" && $5 != "-") ? $5 : "N/A" - subtype = ($6 != "" && $6 != "-") ? $6 : "N/A" - protected = ($8 == "1" || $8 == "1.0") ? "encrypted" : "unencrypted" - retry = ($9 == "1" || $9 == "1.0") ? "retry" : "" - duration = ($10 != "" && $10 != "-") ? $10 : "N/A" - retry_str = (retry != "") ? sprintf(" [%s]", retry) : "" - printf " Frame %s: RA=%s, TA=%s, type=%s/%s, %s, dur=%s%s\n", - $1, ra, ta, type, subtype, protected, duration, retry_str - }' - fi - echo "" -else - echo "(No packets captured)" - echo "" -fi - -echo "=== Summary ===" -if [ "$FINAL_COUNT" -gt 0 ]; then - echo "✓ Monitor mode is working! Captured $FINAL_COUNT packet(s)" - if [ "$PLCP_COUNT" -gt 0 ]; then - echo "✓ PLCP headers detected: $PLCP_COUNT packet(s) with radiotap information" - else - echo "⚠ No PLCP headers detected (may be using DLT_IEEE802_11 instead of DLT_IEEE802_11_RADIO)" - fi -else - echo "✗ No packets captured. Check:" - echo " 1. Is there WiFi traffic on channel $CHANNEL?" - echo " 2. Is the interface actually in monitor mode? (iw dev $INTERFACE info)" - echo " 3. Try a different channel (e.g., 1, 6, 11 for 2.4GHz)" - echo " 4. Try a longer duration: sudo ./test_monitor_tshark.sh $INTERFACE $CHANNEL 30" -fi +python3 "$PARSE_SCRIPT" "$TEMP_PCAP" "$DURATION" "$RAW_PACKET_COUNT"