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:
parent
7e9006a69b
commit
3cb36bff4b
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue