874 lines
34 KiB
C
874 lines
34 KiB
C
/*
|
|
* wifi_monitor.c
|
|
*
|
|
* Copyright (c) 2025 Umber Networks & Robert McMahon
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright notice,
|
|
* this list of conditions and the following disclaimer.
|
|
*
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* 3. Neither the name of the copyright holder nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "wifi_monitor.h"
|
|
#include "esp_log.h"
|
|
#include "esp_wifi.h"
|
|
#include "esp_timer.h"
|
|
#include "string.h"
|
|
|
|
static const char *TAG = "WiFi_Monitor";
|
|
|
|
// Module state
|
|
static bool monitor_running = false;
|
|
static wifi_monitor_cb_t user_callback = NULL;
|
|
static wifi_collapse_stats_t stats = {0};
|
|
|
|
// Tunable thresholds (accessible via GDB for runtime adjustment)
|
|
uint32_t threshold_high_nav_us = 5000; // NAV threshold for "high" classification
|
|
uint32_t threshold_duration_mismatch_us = 10000; // Log mismatches when NAV exceeds this
|
|
uint32_t threshold_phy_rate_fallback_mbps = 100; // PHY rate below this = fallback
|
|
float threshold_retry_rate_percent = 20.0f; // Retry rate for collapse detection
|
|
uint32_t threshold_avg_nav_collapse_us = 10000; // Avg NAV threshold for collapse
|
|
float threshold_collision_percent = 10.0f; // Collision event percentage
|
|
float threshold_mismatch_percent = 5.0f; // Duration mismatch percentage
|
|
uint32_t threshold_duration_multiplier = 2; // NAV > expected * this = mismatch
|
|
|
|
// Logging control
|
|
uint32_t log_every_n_mismatches = 1; // Log every Nth mismatch (1 = all, 10 = every 10th)
|
|
static uint32_t s_mismatch_log_counter = 0;
|
|
static bool s_monitor_debug = false; // Debug mode: enable serial logging
|
|
static uint8_t s_monitor_debug_filter_mac[6] = {0}; // MAC address filter (all zeros = no filter)
|
|
static bool s_monitor_debug_filter_enabled = false; // Whether MAC filtering is enabled
|
|
|
|
// Forward declarations
|
|
static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type);
|
|
static bool wifi_monitor_debug_filter_match(const uint8_t *mac);
|
|
|
|
/**
|
|
* @brief Parse HT/VHT/HE headers to extract PHY parameters
|
|
* @param payload Raw frame payload
|
|
* @param len Total frame length
|
|
* @param mac_hdr_len Length of MAC header (24 or 30 bytes)
|
|
* @param frame_info Frame info structure to populate
|
|
*/
|
|
static void wifi_parse_phy_headers(const uint8_t *payload, uint16_t len, uint16_t mac_hdr_len, wifi_frame_info_t *frame_info) {
|
|
if (len < mac_hdr_len + 4) {
|
|
return; // Not enough data for any PHY headers
|
|
}
|
|
|
|
uint16_t offset = mac_hdr_len;
|
|
|
|
// Check for QoS Control field (4 bytes) - present in QoS data frames
|
|
bool has_qos = (frame_info->type == FRAME_TYPE_DATA &&
|
|
(frame_info->subtype == DATA_QOS_DATA ||
|
|
frame_info->subtype == DATA_QOS_DATA_CF_ACK ||
|
|
frame_info->subtype == DATA_QOS_DATA_CF_POLL ||
|
|
frame_info->subtype == DATA_QOS_DATA_CF_ACK_POLL ||
|
|
frame_info->subtype == DATA_QOS_NULL ||
|
|
frame_info->subtype == DATA_QOS_CF_POLL ||
|
|
frame_info->subtype == DATA_QOS_CF_ACK_POLL));
|
|
|
|
if (has_qos && len >= offset + 4) {
|
|
offset += 4; // Skip QoS Control field
|
|
}
|
|
|
|
// Check for HT Control field (4 bytes) - present if Order bit set
|
|
// HT Control field format (IEEE 802.11-2016):
|
|
// Bit 0-3: Control ID (0=HT, 1-3=VHT, 4+=HE)
|
|
// For VHT/HE: Additional fields for MCS, bandwidth, SGI, NSS
|
|
bool has_ht_ctrl = frame_info->order;
|
|
if (has_ht_ctrl && len >= offset + 4) {
|
|
// HT Control field is little-endian
|
|
uint32_t ht_ctrl = payload[offset] | (payload[offset + 1] << 8) |
|
|
(payload[offset + 2] << 16) | (payload[offset + 3] << 24);
|
|
|
|
// Control ID is in bits 0-3
|
|
uint8_t ctrl_id = ht_ctrl & 0x0F;
|
|
|
|
if (ctrl_id == 0) {
|
|
// HT Control (802.11n) - MCS info is typically in PLCP header (HT-SIG)
|
|
// which ESP-IDF provides in rx_ctrl->rate, not in HT Control field
|
|
frame_info->sig_mode = 1; // HT
|
|
// Note: MCS will be extracted from rx_ctrl->rate in the callback
|
|
// For HT, spatial streams can be inferred from MCS index (MCS 0-7 = 1 SS, 8-15 = 2 SS, etc.)
|
|
} else if (ctrl_id >= 1 && ctrl_id <= 3) {
|
|
// VHT Control (802.11ac) - bits layout varies by variant
|
|
frame_info->sig_mode = 3; // VHT
|
|
|
|
// VHT Control variant 1: MCS in bits 4-7, bandwidth in 8-9, SGI in 10, NSS in 12-14
|
|
if (ctrl_id == 1) {
|
|
frame_info->mcs = (ht_ctrl >> 4) & 0x0F;
|
|
frame_info->bandwidth = (ht_ctrl >> 8) & 0x03;
|
|
frame_info->sgi = ((ht_ctrl >> 10) & 0x01) != 0;
|
|
uint8_t nss = (ht_ctrl >> 12) & 0x07;
|
|
frame_info->spatial_streams = (nss == 0) ? 1 : nss; // NSS encoding varies
|
|
}
|
|
} else if (ctrl_id >= 4) {
|
|
// HE Control (802.11ax)
|
|
frame_info->sig_mode = 4; // HE
|
|
|
|
// HE Control: Similar structure to VHT
|
|
frame_info->mcs = (ht_ctrl >> 4) & 0x0F;
|
|
frame_info->bandwidth = (ht_ctrl >> 8) & 0x03;
|
|
frame_info->sgi = ((ht_ctrl >> 10) & 0x01) != 0;
|
|
uint8_t nss = (ht_ctrl >> 12) & 0x07;
|
|
frame_info->spatial_streams = (nss == 0) ? 1 : nss;
|
|
}
|
|
}
|
|
|
|
// For HT frames without HT Control, try to infer from rate/MCS
|
|
// MCS index encoding: MCS 0-7 = 1 SS, 8-15 = 2 SS, 16-23 = 3 SS, 24-31 = 4 SS
|
|
if (frame_info->sig_mode == 1 && frame_info->mcs > 0) {
|
|
if (frame_info->mcs < 8) {
|
|
frame_info->spatial_streams = 1;
|
|
} else if (frame_info->mcs < 16) {
|
|
frame_info->spatial_streams = 2;
|
|
} else if (frame_info->mcs < 24) {
|
|
frame_info->spatial_streams = 3;
|
|
} else {
|
|
frame_info->spatial_streams = 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Parse A-MPDU aggregation count from frame payload
|
|
* @param payload Raw frame payload
|
|
* @param len Total frame length
|
|
* @param mac_hdr_len Length of MAC header (24 or 30 bytes)
|
|
* @return Number of aggregated MPDUs (1 if not aggregated)
|
|
*/
|
|
static uint8_t wifi_parse_ampdu_count(const uint8_t *payload, uint16_t len, uint16_t mac_hdr_len) {
|
|
uint8_t count = 1; // Default: not aggregated
|
|
uint16_t offset = mac_hdr_len;
|
|
|
|
// Check if this is a QoS data frame (required for A-MPDU)
|
|
uint8_t frame_type = (payload[0] >> 2) & 0x03;
|
|
uint8_t frame_subtype = (payload[0] >> 4) & 0x0F;
|
|
|
|
if (frame_type != FRAME_TYPE_DATA) {
|
|
return count; // Not a data frame
|
|
}
|
|
|
|
// Check if it's a QoS subtype
|
|
bool is_qos = (frame_subtype == DATA_QOS_DATA ||
|
|
frame_subtype == DATA_QOS_DATA_CF_ACK ||
|
|
frame_subtype == DATA_QOS_DATA_CF_POLL ||
|
|
frame_subtype == DATA_QOS_DATA_CF_ACK_POLL ||
|
|
frame_subtype == DATA_QOS_NULL ||
|
|
frame_subtype == DATA_QOS_CF_POLL ||
|
|
frame_subtype == DATA_QOS_CF_ACK_POLL);
|
|
|
|
if (!is_qos || len < offset + 4) {
|
|
return count; // Not QoS or too short
|
|
}
|
|
|
|
// Skip QoS Control field (4 bytes)
|
|
offset += 4;
|
|
|
|
// Check for HT Control field (4 bytes) - present if Order bit set
|
|
// ORDER bit is 0x8000 (bit 15), which is in payload[1], bit 7
|
|
bool has_ht_ctrl = (payload[1] & 0x80) != 0;
|
|
if (has_ht_ctrl && len >= offset + 4) {
|
|
offset += 4; // Skip HT Control field
|
|
}
|
|
|
|
// A-MPDU delimiter format (IEEE 802.11-2016):
|
|
// Each delimiter is 4 bytes: [Reserved(4) | MPDU Length(12) | CRC(8) | Delimiter Signature(8)]
|
|
// Delimiter signature is 0x4E (ASCII 'N')
|
|
// We count delimiters until we find the end delimiter (length = 0) or run out of data
|
|
|
|
uint16_t remaining = len - offset;
|
|
if (remaining < 4) {
|
|
return count; // Not enough data for even one delimiter
|
|
}
|
|
|
|
// Count A-MPDU subframes by parsing delimiters
|
|
uint16_t pos = offset;
|
|
uint16_t delimiter_count = 0; // Use uint16_t to support up to 256 delimiters
|
|
|
|
while (pos + 4 <= len && delimiter_count < 256) { // Support up to 256 for HE (though typically 64)
|
|
// Read delimiter (little-endian)
|
|
uint16_t delimiter = payload[pos] | (payload[pos + 1] << 8);
|
|
uint8_t sig = payload[pos + 3];
|
|
|
|
// Check delimiter signature (should be 0x4E)
|
|
if (sig != 0x4E) {
|
|
break; // Not a valid delimiter
|
|
}
|
|
|
|
// Extract MPDU length (bits 4-15)
|
|
uint16_t mpdu_len = (delimiter >> 4) & 0x0FFF;
|
|
|
|
if (mpdu_len == 0) {
|
|
break; // End delimiter
|
|
}
|
|
|
|
delimiter_count++;
|
|
|
|
// Move to next delimiter (skip this MPDU)
|
|
pos += 4 + mpdu_len; // Delimiter (4) + MPDU length
|
|
|
|
// Align to 4-byte boundary
|
|
pos = (pos + 3) & ~3;
|
|
|
|
if (pos >= len) {
|
|
break; // Reached end of frame
|
|
}
|
|
}
|
|
|
|
// If we found multiple delimiters, this is an A-MPDU
|
|
// Cap count at 255 (max value for uint8_t return type)
|
|
if (delimiter_count > 1) {
|
|
count = (delimiter_count > 255) ? 255 : (uint8_t)delimiter_count;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* @brief Parse 802.11 MAC header
|
|
*/
|
|
esp_err_t wifi_parse_frame(const uint8_t *payload, uint16_t len, wifi_frame_info_t *frame_info) {
|
|
if (!payload || !frame_info || len < 24) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
memset(frame_info, 0, sizeof(wifi_frame_info_t));
|
|
|
|
// Parse Frame Control (bytes 0-1)
|
|
frame_info->frame_control = (payload[1] << 8) | payload[0];
|
|
frame_info->protocol_version = frame_info->frame_control & 0x03;
|
|
frame_info->type = (frame_info->frame_control >> 2) & 0x03;
|
|
frame_info->subtype = (frame_info->frame_control >> 4) & 0x0F;
|
|
frame_info->to_ds = (frame_info->frame_control & FRAME_CTRL_TO_DS) != 0;
|
|
frame_info->from_ds = (frame_info->frame_control & FRAME_CTRL_FROM_DS) != 0;
|
|
frame_info->more_frag = (frame_info->frame_control & FRAME_CTRL_MORE_FRAG) != 0;
|
|
frame_info->retry = (frame_info->frame_control & FRAME_CTRL_RETRY) != 0;
|
|
frame_info->pwr_mgmt = (frame_info->frame_control & FRAME_CTRL_PWR_MGMT) != 0;
|
|
frame_info->more_data = (frame_info->frame_control & FRAME_CTRL_MORE_DATA) != 0;
|
|
frame_info->protected_frame = (frame_info->frame_control & FRAME_CTRL_PROTECTED) != 0;
|
|
frame_info->order = (frame_info->frame_control & FRAME_CTRL_ORDER) != 0;
|
|
|
|
// Parse Duration/ID (bytes 2-3) - THIS IS THE NAV FIELD!
|
|
frame_info->duration_id = (payload[3] << 8) | payload[2];
|
|
|
|
// Parse Address 1 (Receiver) (bytes 4-9)
|
|
memcpy(frame_info->addr1, &payload[4], 6);
|
|
|
|
// Parse Address 2 (Transmitter) (bytes 10-15)
|
|
memcpy(frame_info->addr2, &payload[10], 6);
|
|
|
|
// Parse Address 3 (BSSID/SA/DA) (bytes 16-21)
|
|
memcpy(frame_info->addr3, &payload[16], 6);
|
|
|
|
// Parse Sequence Control (bytes 22-23)
|
|
frame_info->seq_ctrl = (payload[23] << 8) | payload[22];
|
|
frame_info->fragment_num = frame_info->seq_ctrl & 0x0F;
|
|
frame_info->sequence_num = (frame_info->seq_ctrl >> 4) & 0x0FFF;
|
|
|
|
// Check for Address 4 (only present if To DS and From DS both set)
|
|
frame_info->has_addr4 = frame_info->to_ds && frame_info->from_ds;
|
|
uint16_t mac_hdr_len = frame_info->has_addr4 ? 30 : 24;
|
|
if (frame_info->has_addr4 && len >= 30) {
|
|
memcpy(frame_info->addr4, &payload[24], 6);
|
|
}
|
|
|
|
// Initialize PHY parameters
|
|
frame_info->spatial_streams = 1; // Default to 1 SS
|
|
frame_info->mcs = 0;
|
|
frame_info->sig_mode = 0;
|
|
frame_info->sgi = false;
|
|
frame_info->bandwidth = 0;
|
|
|
|
// Parse HT/VHT/HE headers to extract PHY parameters
|
|
wifi_parse_phy_headers(payload, len, mac_hdr_len, frame_info);
|
|
|
|
// Parse A-MPDU aggregation count
|
|
frame_info->ampdu_count = wifi_parse_ampdu_count(payload, len, mac_hdr_len);
|
|
|
|
frame_info->frame_len = len;
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Promiscuous mode RX callback
|
|
*/
|
|
static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type) {
|
|
if (!buf) return;
|
|
|
|
wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
|
|
wifi_pkt_rx_ctrl_t *rx_ctrl = &pkt->rx_ctrl;
|
|
const uint8_t *payload = pkt->payload;
|
|
uint16_t len = rx_ctrl->sig_len;
|
|
|
|
// Parse frame header
|
|
wifi_frame_info_t frame_info;
|
|
if (wifi_parse_frame(payload, len, &frame_info) != ESP_OK) {
|
|
return;
|
|
}
|
|
|
|
// Add RX metadata
|
|
frame_info.rssi = rx_ctrl->rssi;
|
|
frame_info.channel = rx_ctrl->channel;
|
|
frame_info.timestamp = rx_ctrl->timestamp;
|
|
|
|
// Extract PHY rate info from RX control
|
|
frame_info.rate = rx_ctrl->rate; // This is the rate index
|
|
|
|
// If MCS wasn't parsed from headers but we have HT/VHT/HE mode, try to extract from rate
|
|
// ESP-IDF encoding: rate >= 128 encodes MCS for HT/VHT/HE frames
|
|
// HT: MCS 0-31, VHT: MCS 0-9, HE: MCS 0-11
|
|
if (frame_info.sig_mode > 0 && frame_info.mcs == 0 && rx_ctrl->rate >= 128) {
|
|
uint8_t extracted_mcs = rx_ctrl->rate - 128;
|
|
|
|
// Cap MCS based on signal mode
|
|
if (frame_info.sig_mode == 1) {
|
|
// HT (802.11n): MCS 0-31
|
|
if (extracted_mcs > 31) extracted_mcs = 31;
|
|
frame_info.mcs = extracted_mcs;
|
|
} else if (frame_info.sig_mode == 3) {
|
|
// VHT (802.11ac): MCS 0-9
|
|
if (extracted_mcs > 9) extracted_mcs = 9;
|
|
frame_info.mcs = extracted_mcs;
|
|
} else if (frame_info.sig_mode == 4) {
|
|
// HE (802.11ax): MCS 0-11
|
|
if (extracted_mcs > 11) extracted_mcs = 11;
|
|
frame_info.mcs = extracted_mcs;
|
|
} else {
|
|
// Unknown mode, use extracted value but cap at 31 (max HT)
|
|
if (extracted_mcs > 31) extracted_mcs = 31;
|
|
frame_info.mcs = extracted_mcs;
|
|
}
|
|
}
|
|
|
|
// Calculate PHY rate using parsed MCS/spatial streams if available
|
|
// Otherwise fall back to rate table estimation
|
|
if (frame_info.sig_mode > 0 && frame_info.mcs > 0 && frame_info.spatial_streams > 0) {
|
|
// Use parsed MCS/SS info - rate will be calculated accurately in mcs_telemetry
|
|
// For monitor stats, keep the parsed value (may be 0 if not calculated yet)
|
|
if (frame_info.phy_rate_kbps == 0) {
|
|
// Fallback estimate if not set by parsing
|
|
frame_info.phy_rate_kbps = 100000; // Default estimate
|
|
}
|
|
} else {
|
|
// Estimate PHY rate from rate index (rough approximation for legacy frames)
|
|
static const uint32_t rate_table[] = {
|
|
1000, 2000, 5500, 11000, // 1, 2, 5.5, 11 Mbps (DSSS)
|
|
6000, 9000, 12000, 18000, 24000, 36000, 48000, 54000, // OFDM rates
|
|
65000, 130000, 195000, 260000 // Rough HT estimates
|
|
};
|
|
|
|
if (rx_ctrl->rate < sizeof(rate_table) / sizeof(rate_table[0])) {
|
|
frame_info.phy_rate_kbps = rate_table[rx_ctrl->rate];
|
|
} else {
|
|
frame_info.phy_rate_kbps = 100000; // Assume 100 Mbps default
|
|
}
|
|
}
|
|
|
|
// Update statistics
|
|
stats.total_frames++;
|
|
|
|
if (frame_info.retry) {
|
|
stats.retry_frames++;
|
|
}
|
|
|
|
if (frame_info.duration_id > threshold_high_nav_us) {
|
|
stats.high_nav_frames++;
|
|
}
|
|
|
|
if (frame_info.duration_id > stats.max_nav) {
|
|
stats.max_nav = frame_info.duration_id;
|
|
}
|
|
|
|
// Track PHY rate statistics
|
|
uint16_t phy_rate_mbps = frame_info.phy_rate_kbps / 1000;
|
|
if (phy_rate_mbps > 0) {
|
|
if (stats.min_phy_rate_mbps == 0 || phy_rate_mbps < stats.min_phy_rate_mbps) {
|
|
stats.min_phy_rate_mbps = phy_rate_mbps;
|
|
}
|
|
if (phy_rate_mbps > stats.max_phy_rate_mbps) {
|
|
stats.max_phy_rate_mbps = phy_rate_mbps;
|
|
}
|
|
|
|
stats.avg_phy_rate_mbps = (stats.avg_phy_rate_mbps * (stats.total_frames - 1) + phy_rate_mbps) / stats.total_frames;
|
|
|
|
if (frame_info.channel >= 36 && phy_rate_mbps < threshold_phy_rate_fallback_mbps) {
|
|
stats.rate_fallback_frames++;
|
|
}
|
|
|
|
// Calculate expected duration
|
|
uint32_t tx_time_us = (frame_info.frame_len * 8000) / frame_info.phy_rate_kbps;
|
|
uint32_t overhead_us = 44;
|
|
if (frame_info.sig_mode == 0) {
|
|
overhead_us = 24;
|
|
} else if (frame_info.bandwidth == 2) {
|
|
overhead_us = 52;
|
|
}
|
|
uint32_t expected_duration = tx_time_us + overhead_us;
|
|
|
|
// ---------------------------------------------------------
|
|
// DURATION MISMATCH CHECK (Attacker Identification Added)
|
|
// ---------------------------------------------------------
|
|
if (frame_info.duration_id > expected_duration * threshold_duration_multiplier) {
|
|
stats.duration_mismatch_frames++;
|
|
|
|
if (frame_info.duration_id > threshold_duration_mismatch_us) {
|
|
s_mismatch_log_counter++;
|
|
if (s_monitor_debug && (s_mismatch_log_counter % log_every_n_mismatches) == 0) {
|
|
/* Check MAC filter before logging */
|
|
if (wifi_monitor_debug_filter_match(frame_info.addr2)) {
|
|
ESP_LOGW("MONITOR", "Duration mismatch: %s frame, %u bytes @ %u Mbps",
|
|
wifi_frame_type_str(frame_info.type, frame_info.subtype),
|
|
frame_info.frame_len, phy_rate_mbps);
|
|
|
|
// NEW: Log the Source MAC (Addr2)
|
|
ESP_LOGW("MONITOR", " Source MAC: %02x:%02x:%02x:%02x:%02x:%02x",
|
|
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
|
|
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5]);
|
|
|
|
ESP_LOGW("MONITOR", " Expected: %lu us, Actual NAV: %u us (+%ld us)",
|
|
expected_duration, frame_info.duration_id,
|
|
frame_info.duration_id - expected_duration);
|
|
ESP_LOGW("MONITOR", " Retry: %s, RSSI: %d dBm",
|
|
frame_info.retry ? "YES" : "no", frame_info.rssi);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// COLLISION CHECK (Attacker Identification Added)
|
|
// ---------------------------------------------------------
|
|
if (frame_info.retry && frame_info.duration_id > threshold_high_nav_us &&
|
|
phy_rate_mbps < threshold_phy_rate_fallback_mbps) {
|
|
if (s_monitor_debug) {
|
|
/* Check MAC filter before logging */
|
|
if (wifi_monitor_debug_filter_match(frame_info.addr2)) {
|
|
ESP_LOGW("MONITOR", "⚠⚠⚠ COLLISION DETECTED!");
|
|
|
|
// NEW: Log the Attacker MAC
|
|
ESP_LOGW("MONITOR", " Attacker MAC: %02x:%02x:%02x:%02x:%02x:%02x",
|
|
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
|
|
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5]);
|
|
|
|
ESP_LOGW("MONITOR", " Type: %s, Size: %u bytes, Rate: %u Mbps",
|
|
wifi_frame_type_str(frame_info.type, frame_info.subtype),
|
|
frame_info.frame_len, phy_rate_mbps);
|
|
ESP_LOGW("MONITOR", " NAV: %u us (expected %lu us), Retry: YES",
|
|
frame_info.duration_id, expected_duration);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Count frame types
|
|
switch (frame_info.type) {
|
|
case FRAME_TYPE_MANAGEMENT:
|
|
stats.mgmt_frames++;
|
|
break;
|
|
|
|
case FRAME_TYPE_CONTROL:
|
|
if (frame_info.subtype == CTRL_RTS) {
|
|
stats.rts_frames++;
|
|
} else if (frame_info.subtype == CTRL_CTS) {
|
|
stats.cts_frames++;
|
|
} else if (frame_info.subtype == CTRL_ACK) {
|
|
stats.ack_frames++;
|
|
}
|
|
break;
|
|
|
|
case FRAME_TYPE_DATA:
|
|
stats.data_frames++;
|
|
|
|
/* Periodic diagnostic summary (even when debug is off) */
|
|
static uint32_t data_frame_diag_counter = 0;
|
|
static uint64_t last_diag_log_ms = 0;
|
|
data_frame_diag_counter++;
|
|
uint64_t now_ms = esp_timer_get_time() / 1000;
|
|
if (now_ms - last_diag_log_ms > 10000) { /* Log every 10 seconds */
|
|
ESP_LOGI("MONITOR", "Data frames: %lu total, last TA=%02x:%02x:%02x:%02x:%02x:%02x, "
|
|
"debug=%s, filter=%s",
|
|
(unsigned long)stats.data_frames,
|
|
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
|
|
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5],
|
|
s_monitor_debug ? "on" : "off",
|
|
s_monitor_debug_filter_enabled ? "on" : "off");
|
|
data_frame_diag_counter = 0;
|
|
last_diag_log_ms = now_ms;
|
|
}
|
|
|
|
/* Update histograms for data frames */
|
|
// AMPDU histogram: [1, 2-4, 5-8, 9-16, 17-32, 33-48, 49-64, 65+]
|
|
uint8_t ampdu_count = frame_info.ampdu_count;
|
|
if (ampdu_count == 1) {
|
|
stats.ampdu_hist[0]++;
|
|
} else if (ampdu_count >= 2 && ampdu_count <= 4) {
|
|
stats.ampdu_hist[1]++;
|
|
} else if (ampdu_count >= 5 && ampdu_count <= 8) {
|
|
stats.ampdu_hist[2]++;
|
|
} else if (ampdu_count >= 9 && ampdu_count <= 16) {
|
|
stats.ampdu_hist[3]++;
|
|
} else if (ampdu_count >= 17 && ampdu_count <= 32) {
|
|
stats.ampdu_hist[4]++;
|
|
} else if (ampdu_count >= 33 && ampdu_count <= 48) {
|
|
stats.ampdu_hist[5]++;
|
|
} else if (ampdu_count >= 49 && ampdu_count <= 64) {
|
|
stats.ampdu_hist[6]++;
|
|
} else if (ampdu_count >= 65) {
|
|
stats.ampdu_hist[7]++;
|
|
}
|
|
|
|
// MCS histogram: [0-31]
|
|
if (frame_info.mcs < 32) {
|
|
stats.mcs_hist[frame_info.mcs]++;
|
|
}
|
|
|
|
// Spatial streams histogram: [1-8]
|
|
uint8_t ss = frame_info.spatial_streams;
|
|
if (ss >= 1 && ss <= 8) {
|
|
stats.ss_hist[ss - 1]++; // Convert to 0-indexed
|
|
}
|
|
|
|
/* Debug logging for data frames (especially QoS data used by iperf) */
|
|
if (s_monitor_debug) {
|
|
bool filter_match = wifi_monitor_debug_filter_match(frame_info.addr2);
|
|
uint16_t phy_rate_mbps = frame_info.phy_rate_kbps / 1000;
|
|
const char *frame_type_name = wifi_frame_type_str(frame_info.type, frame_info.subtype);
|
|
|
|
if (filter_match) {
|
|
/* Log all data frames (QoS and non-QoS) when filter matches or filter is disabled */
|
|
ESP_LOGI("MONITOR", "DATA: %s, TA=%02x:%02x:%02x:%02x:%02x:%02x, RA=%02x:%02x:%02x:%02x:%02x:%02x, "
|
|
"Size=%u bytes, Rate=%u Mbps, MCS=%u, SS=%u, BW=%u MHz, RSSI=%d dBm, Retry=%s",
|
|
frame_type_name,
|
|
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
|
|
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5],
|
|
frame_info.addr1[0], frame_info.addr1[1], frame_info.addr1[2],
|
|
frame_info.addr1[3], frame_info.addr1[4], frame_info.addr1[5],
|
|
frame_info.frame_len, phy_rate_mbps, frame_info.mcs,
|
|
frame_info.spatial_streams,
|
|
(frame_info.bandwidth == 0) ? 20 : (frame_info.bandwidth == 1) ? 40 : 80,
|
|
frame_info.rssi, frame_info.retry ? "YES" : "no");
|
|
} else {
|
|
/* Diagnostic: log when data frames are filtered out (throttled, but more visible) */
|
|
static uint32_t filtered_data_count = 0;
|
|
static uint64_t last_filtered_log_ms = 0;
|
|
static uint8_t last_filtered_ta[6] = {0};
|
|
filtered_data_count++;
|
|
uint64_t now_ms = esp_timer_get_time() / 1000;
|
|
memcpy(last_filtered_ta, frame_info.addr2, 6);
|
|
if (now_ms - last_filtered_log_ms > 5000) { /* Log every 5 seconds */
|
|
ESP_LOGW("MONITOR", "Filtered out %lu data frames (last TA=%02x:%02x:%02x:%02x:%02x:%02x, filter=%s)",
|
|
(unsigned long)filtered_data_count,
|
|
last_filtered_ta[0], last_filtered_ta[1], last_filtered_ta[2],
|
|
last_filtered_ta[3], last_filtered_ta[4], last_filtered_ta[5],
|
|
s_monitor_debug_filter_enabled ? "enabled" : "disabled");
|
|
filtered_data_count = 0;
|
|
last_filtered_log_ms = now_ms;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (frame_info.retry && frame_info.duration_id > threshold_high_nav_us) {
|
|
stats.collision_events++;
|
|
}
|
|
|
|
if (stats.total_frames > 0) {
|
|
stats.retry_rate = (float)stats.retry_frames / stats.total_frames * 100.0f;
|
|
stats.avg_nav = (stats.avg_nav * (stats.total_frames - 1) + frame_info.duration_id) / stats.total_frames;
|
|
}
|
|
|
|
if (user_callback) {
|
|
user_callback(&frame_info, payload, len);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Initialize WiFi monitor mode
|
|
*/
|
|
esp_err_t wifi_monitor_init(uint8_t channel, wifi_monitor_cb_t callback) {
|
|
ESP_LOGI(TAG, "Initializing WiFi monitor mode on channel %d", channel);
|
|
|
|
user_callback = callback;
|
|
monitor_running = false;
|
|
|
|
// Initialize WiFi in NULL mode (no STA or AP)
|
|
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
|
esp_err_t ret = esp_wifi_init(&cfg);
|
|
if (ret != ESP_OK && ret != ESP_ERR_WIFI_NOT_INIT) {
|
|
ESP_LOGE(TAG, "WiFi init failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
// Set WiFi mode to NULL (required for promiscuous mode)
|
|
ret = esp_wifi_set_mode(WIFI_MODE_NULL);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Set mode failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
// Start WiFi
|
|
ret = esp_wifi_start();
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "WiFi start failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
// Set channel
|
|
ret = wifi_monitor_set_channel(channel);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Set channel failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
// Set promiscuous filter to capture all frame types
|
|
wifi_promiscuous_filter_t filter = {
|
|
.filter_mask = WIFI_PROMIS_FILTER_MASK_ALL
|
|
};
|
|
ret = esp_wifi_set_promiscuous_filter(&filter);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Set filter failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
// Register promiscuous callback
|
|
ret = esp_wifi_set_promiscuous_rx_cb(wifi_promiscuous_rx_cb);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Set callback failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "WiFi monitor initialized successfully");
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Start WiFi monitoring
|
|
*/
|
|
esp_err_t wifi_monitor_start(void) {
|
|
ESP_LOGI(TAG, "Starting WiFi monitor mode");
|
|
|
|
esp_err_t ret = esp_wifi_set_promiscuous(true);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Enable promiscuous failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
monitor_running = true;
|
|
wifi_monitor_reset_stats();
|
|
|
|
ESP_LOGI(TAG, "WiFi monitor started - capturing all 802.11 frames");
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Stop WiFi monitoring
|
|
*/
|
|
esp_err_t wifi_monitor_stop(void) {
|
|
ESP_LOGI(TAG, "Stopping WiFi monitor mode");
|
|
|
|
esp_err_t ret = esp_wifi_set_promiscuous(false);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Disable promiscuous failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
monitor_running = false;
|
|
|
|
ESP_LOGI(TAG, "WiFi monitor stopped");
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Set WiFi channel for monitoring
|
|
*/
|
|
esp_err_t wifi_monitor_set_channel(uint8_t channel) {
|
|
ESP_LOGI(TAG, "Setting channel to %d", channel);
|
|
|
|
esp_err_t ret = esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
|
|
if (ret != ESP_OK) {
|
|
ESP_LOGE(TAG, "Set channel failed: %s", esp_err_to_name(ret));
|
|
return ret;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Get WiFi collapse detection statistics
|
|
*/
|
|
esp_err_t wifi_monitor_get_stats(wifi_collapse_stats_t *out_stats) {
|
|
if (!out_stats) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
memcpy(out_stats, &stats, sizeof(wifi_collapse_stats_t));
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Reset WiFi collapse detection statistics
|
|
*/
|
|
void wifi_monitor_reset_stats(void) {
|
|
memset(&stats, 0, sizeof(wifi_collapse_stats_t));
|
|
ESP_LOGI(TAG, "Statistics reset");
|
|
}
|
|
|
|
/**
|
|
* @brief Check if current conditions indicate WiFi collapse
|
|
*/
|
|
bool wifi_monitor_is_collapsed(void) {
|
|
// Need minimum sample size
|
|
if (stats.total_frames < 100) {
|
|
return false;
|
|
}
|
|
|
|
bool high_retry = stats.retry_rate > threshold_retry_rate_percent;
|
|
bool high_nav = stats.avg_nav > threshold_avg_nav_collapse_us;
|
|
bool high_collision = (float)stats.collision_events / stats.total_frames > (threshold_collision_percent / 100.0f);
|
|
bool duration_issues = (float)stats.duration_mismatch_frames / stats.total_frames > (threshold_mismatch_percent / 100.0f);
|
|
bool rate_fallback = stats.avg_phy_rate_mbps < threshold_phy_rate_fallback_mbps;
|
|
|
|
// Collapse detected if 3 out of 5 indicators are true
|
|
int indicators = (high_retry ? 1 : 0) +
|
|
(high_nav ? 1 : 0) +
|
|
(high_collision ? 1 : 0) +
|
|
(duration_issues ? 1 : 0) +
|
|
(rate_fallback ? 1 : 0);
|
|
|
|
return indicators >= 3;
|
|
}
|
|
|
|
/**
|
|
* @brief Get string representation of frame type
|
|
*/
|
|
const char* wifi_frame_type_str(uint8_t type, uint8_t subtype) {
|
|
if (type == FRAME_TYPE_MANAGEMENT) {
|
|
switch (subtype) {
|
|
case MGMT_ASSOC_REQ: return "ASSOC_REQ";
|
|
case MGMT_ASSOC_RESP: return "ASSOC_RESP";
|
|
case MGMT_REASSOC_REQ: return "REASSOC_REQ";
|
|
case MGMT_REASSOC_RESP: return "REASSOC_RESP";
|
|
case MGMT_PROBE_REQ: return "PROBE_REQ";
|
|
case MGMT_PROBE_RESP: return "PROBE_RESP";
|
|
case MGMT_BEACON: return "BEACON";
|
|
case MGMT_ATIM: return "ATIM";
|
|
case MGMT_DISASSOC: return "DISASSOC";
|
|
case MGMT_AUTH: return "AUTH";
|
|
case MGMT_DEAUTH: return "DEAUTH";
|
|
case MGMT_ACTION: return "ACTION";
|
|
default: return "MGMT_UNKNOWN";
|
|
}
|
|
} else if (type == FRAME_TYPE_CONTROL) {
|
|
switch (subtype) {
|
|
case CTRL_BLOCK_ACK_REQ: return "BLOCK_ACK_REQ";
|
|
case CTRL_BLOCK_ACK: return "BLOCK_ACK";
|
|
case CTRL_PS_POLL: return "PS_POLL";
|
|
case CTRL_RTS: return "RTS";
|
|
case CTRL_CTS: return "CTS";
|
|
case CTRL_ACK: return "ACK";
|
|
case CTRL_CF_END: return "CF_END";
|
|
case CTRL_CF_END_ACK: return "CF_END_ACK";
|
|
default: return "CTRL_UNKNOWN";
|
|
}
|
|
} else if (type == FRAME_TYPE_DATA) {
|
|
switch (subtype) {
|
|
case DATA_DATA: return "DATA";
|
|
case DATA_NULL: return "NULL";
|
|
case DATA_QOS_DATA: return "QOS_DATA";
|
|
case DATA_QOS_NULL: return "QOS_NULL";
|
|
default: return "DATA_UNKNOWN";
|
|
}
|
|
}
|
|
|
|
return "UNKNOWN";
|
|
}
|
|
|
|
/**
|
|
* @brief Enable/disable monitor debug mode (serial logging)
|
|
* @param enable true to enable debug logging, false to disable
|
|
*/
|
|
void wifi_monitor_set_debug(bool enable) {
|
|
s_monitor_debug = enable;
|
|
}
|
|
|
|
/**
|
|
* @brief Get monitor debug mode status
|
|
* @return true if debug mode enabled, false otherwise
|
|
*/
|
|
bool wifi_monitor_get_debug(void) {
|
|
return s_monitor_debug;
|
|
}
|
|
|
|
/**
|
|
* @brief Check if a MAC address matches the debug filter
|
|
* @param mac MAC address to check (6 bytes)
|
|
* @return true if filter is disabled or MAC matches filter, false otherwise
|
|
*/
|
|
static bool wifi_monitor_debug_filter_match(const uint8_t *mac) {
|
|
if (!s_monitor_debug_filter_enabled || mac == NULL) {
|
|
return true; /* No filter or invalid MAC - allow all */
|
|
}
|
|
/* Compare MAC addresses byte by byte */
|
|
for (int i = 0; i < 6; i++) {
|
|
if (mac[i] != s_monitor_debug_filter_mac[i]) {
|
|
return false; /* MAC doesn't match filter */
|
|
}
|
|
}
|
|
return true; /* MAC matches filter */
|
|
}
|
|
|
|
/**
|
|
* @brief Set MAC address filter for debug logging
|
|
* @param mac MAC address to filter on (6 bytes), or NULL to disable filter
|
|
* @return ESP_OK on success, ESP_ERR_INVALID_ARG if mac is invalid
|
|
*/
|
|
esp_err_t wifi_monitor_set_debug_filter(const uint8_t *mac) {
|
|
if (mac == NULL) {
|
|
/* Disable filter */
|
|
s_monitor_debug_filter_enabled = false;
|
|
memset(s_monitor_debug_filter_mac, 0, 6);
|
|
return ESP_OK;
|
|
}
|
|
/* Enable filter and copy MAC address */
|
|
memcpy(s_monitor_debug_filter_mac, mac, 6);
|
|
s_monitor_debug_filter_enabled = true;
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Get current MAC address filter for debug logging
|
|
* @param mac_out Buffer to store MAC address (6 bytes), or NULL to just check if filter is enabled
|
|
* @return true if filter is enabled, false otherwise
|
|
*/
|
|
bool wifi_monitor_get_debug_filter(uint8_t *mac_out) {
|
|
if (mac_out != NULL && s_monitor_debug_filter_enabled) {
|
|
memcpy(mac_out, s_monitor_debug_filter_mac, 6);
|
|
}
|
|
return s_monitor_debug_filter_enabled;
|
|
}
|