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 <cursoragent@cursor.com>
This commit is contained in:
Robert McMahon 2026-02-13 14:58:39 -08:00
parent 7e9006a69b
commit 3cb36bff4b
2 changed files with 376 additions and 359 deletions

365
parse_tshark_pcap.py Executable file
View File

@ -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 <pcap_file> [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()

View File

@ -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/<TAB>/g' | head -5
echo ""
echo " Last 5 lines of raw output:"
tail -5 "$TEMP_TSHARK_OUTPUT" | sed 's/\t/<TAB>/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"