Initial commit: Linux wireless monitor for ESP32 verification

A C program using GNU Automake for capturing and parsing 802.11 WiFi
frame headers in monitor mode. Designed to verify ESP32 monitor code
by comparing Linux vs ESP32 frame parsing.

Features:
- Monitor mode setup using libnl3
- Packet capture using libpcap
- 802.11 frame parsing (RA, TA, duration, retry flag)
- MAC address filtering (matches ESP32 filter behavior)
- Output format matches ESP32 debug logs for easy comparison

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Robert McMahon 2026-02-12 13:26:17 -08:00
commit 38fbce9061
19 changed files with 1561 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# Autotools generated files
Makefile
Makefile.in
aclocal.m4
autom4te.cache/
config.h
config.h.in
config.log
config.status
configure
stamp-h1
.deps/
.dirstamp
# Compiled files
*.o
*.lo
*.la
*.a
*.so
*.so.*
# Executables
wireless_monitor
src/wireless_monitor
# Distribution files
*.tar.gz
*.tar.bz2
*.zip
# Editor files
*~
*.swp
*.swo
.vscode/
.idea/
# Build directories
build/
*.dSYM/

103
BUILD_PI5.md Normal file
View File

@ -0,0 +1,103 @@
# Building on Raspberry Pi 5
This guide covers building the wireless monitor tool on Raspberry Pi 5 running Raspberry Pi OS (Debian-based).
## Prerequisites
### Install Build Tools and Dependencies
```bash
# Update package list
sudo apt-get update
# Install build essentials and autotools
sudo apt-get install -y \
build-essential \
autoconf \
automake \
libtool \
pkg-config
# Install WiFi monitoring libraries
sudo apt-get install -y \
libpcap-dev \
libnl-genl-3-dev \
libnl-3-dev
```
## Building
```bash
cd wireless-monitor-template
# Generate configure script
./autogen.sh
# Configure build
./configure
# Build
make
# Test (as root - required for monitor mode)
sudo ./src/wireless_monitor wlan0 11
```
## Installation (Optional)
```bash
# Install system-wide
sudo make install
# Then run from anywhere
sudo wireless_monitor wlan0 11
```
## Troubleshooting
### Missing autotools
If `autogen.sh` fails:
```bash
sudo apt-get install autoconf automake libtool
```
### Missing pkg-config
If configure fails to find libraries:
```bash
sudo apt-get install pkg-config
```
### Library Not Found
If you get linker errors:
```bash
# Verify libraries are installed
pkg-config --exists libpcap && echo "libpcap OK" || echo "libpcap missing"
pkg-config --exists libnl-genl-3.0 && echo "libnl-genl OK" || echo "libnl-genl missing"
pkg-config --exists libnl-3.0 && echo "libnl-3 OK" || echo "libnl-3 missing"
```
### Monitor Mode Permission Denied
Must run as root:
```bash
sudo ./src/wireless_monitor wlan0 11
```
## Cross-Compilation (from Linux PC)
If you want to cross-compile from a Linux PC:
```bash
# Install cross-compiler
sudo apt-get install gcc-aarch64-linux-gnu
# Configure for cross-compilation
./autogen.sh
./configure --host=aarch64-linux-gnu \
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
make
```

5
Makefile.am Normal file
View File

@ -0,0 +1,5 @@
SUBDIRS = src
dist_doc_DATA = README.md
EXTRA_DIST = autogen.sh

125
PUSH_TO_UMBER.md Normal file
View File

@ -0,0 +1,125 @@
# Push to Umber Git Repository
## Current Status
✅ Git repository initialized
✅ All files staged
✅ Ready to commit and push
## Steps to Push
### 1. Create Repository on Umber Git Server
First, create a new repository on your Umber Git server (Gitea/GitLab/etc.):
- Repository name: `wireless-monitor` (or your preferred name)
- Visibility: Private or Public (your choice)
- **Do NOT** initialize with README (we already have one)
### 2. Commit Locally
```bash
cd /home/rjmcmahon/Code/esp32/esp32-iperf-shell/wireless-monitor-template
# Create initial commit
git commit -m "Initial commit: Linux wireless monitor for ESP32 verification
A C program using GNU Automake for capturing and parsing 802.11 WiFi
frame headers in monitor mode. Designed to verify ESP32 monitor code
by comparing Linux vs ESP32 frame parsing.
Features:
- Monitor mode setup using libnl3
- Packet capture using libpcap
- 802.11 frame parsing (RA, TA, duration, retry flag)
- MAC address filtering (matches ESP32 filter behavior)
- Output format matches ESP32 debug logs for easy comparison"
```
### 3. Add Remote Repository
Replace `YOUR_REPO_URL` with your actual Umber Git repository URL:
```bash
# Example URLs:
# https://git.umber.com/rjmcmahon/wireless-monitor.git
# git@git.umber.com:rjmcmahon/wireless-monitor.git
git remote add origin YOUR_REPO_URL
# Verify remote
git remote -v
```
### 4. Push to Repository
```bash
# Set default branch name (if needed)
git branch -M main
# Push to repository
git push -u origin main
# If your default branch is 'master' instead:
# git branch -M master
# git push -u origin master
```
## Building on Raspberry Pi 5 After Clone
Once pushed, you can clone and build on Raspberry Pi 5:
```bash
# Clone repository
git clone https://git.umber.com/rjmcmahon/wireless-monitor.git
cd wireless-monitor
# Build (automated)
./build_pi5.sh
# Or manually
./autogen.sh
./configure
make
# Run
sudo ./src/wireless_monitor wlan0 11
```
## Troubleshooting
### Authentication Issues
If you get authentication errors:
```bash
# For HTTPS (will prompt for username/password)
git remote set-url origin https://git.umber.com/rjmcmahon/wireless-monitor.git
# For SSH (requires SSH key setup)
git remote set-url origin git@git.umber.com:rjmcmahon/wireless-monitor.git
```
### Branch Name Mismatch
If you get "branch name mismatch" error:
```bash
# Check what branch you're on
git branch
# Rename if needed
git branch -M main # or 'master'
```
### Push Rejected
If push is rejected because remote has content:
```bash
# Pull first (if remote has initial commit)
git pull origin main --allow-unrelated-histories
# Then push
git push -u origin main
```

61
QUICK_START_PI5.md Normal file
View File

@ -0,0 +1,61 @@
# Quick Start: Raspberry Pi 5
## 1. Clone Repository
```bash
# On Raspberry Pi 5
git clone https://git.umber.com/rjmcmahon/wireless-monitor.git
cd wireless-monitor
```
## 2. Build
**Option A: Use automated script**
```bash
./build_pi5.sh
```
**Option B: Manual build**
```bash
# Install dependencies
sudo apt-get update
sudo apt-get install -y build-essential autoconf automake libtool \
pkg-config libpcap-dev libnl-genl-3-dev libnl-3-dev
# Build
./autogen.sh
./configure
make
```
## 3. Run
```bash
# Run as root (required for monitor mode)
sudo ./src/wireless_monitor wlan0 11
# With MAC filter (to match ESP32)
sudo ./src/wireless_monitor wlan0 11 80:84:89:93:c4:b6
```
## 4. Compare with ESP32
Run both simultaneously on the same channel and compare outputs:
**ESP32:**
```
monitor start -c 11
monitor debug on
monitor filter 80:84:89:93:c4:b6
```
**Raspberry Pi 5:**
```bash
sudo ./src/wireless_monitor wlan0 11 80:84:89:93:c4:b6
```
Compare:
- Same TA/RA addresses?
- Same frame counts?
- Same durations?
- Same retry flags?

99
README.md Normal file
View File

@ -0,0 +1,99 @@
# Wireless Monitor
A Linux C program for capturing and parsing 802.11 WiFi frame headers in monitor mode. This tool is designed to **verify and cross-check** the ESP32 monitor code by comparing what a Linux machine sees vs what the ESP32 sees when monitoring the same WiFi traffic.
## Purpose
This program captures 802.11 frames and displays:
- **RA (Receiver Address)** and **TA (Transmitter Address)** - same fields ESP32 extracts
- Frame type, size, duration (NAV)
- RSSI, MCS, spatial streams (when available from radiotap)
- Retry flag
This allows you to verify that the ESP32's frame parsing matches what Linux sees, helping debug issues like:
- Missing frames
- Incorrect MAC address extraction
- Duration/NAV mismatches
- Frame type classification
## Requirements
- Linux kernel with nl80211 support
- libpcap development files
- libnl3 development files
- GNU autotools (autoconf, automake, libtool)
### Install Dependencies
**Ubuntu/Debian:**
```bash
sudo apt-get install build-essential autoconf automake libtool \
libpcap-dev libnl-genl-3-dev libnl-3-dev pkg-config
```
**Fedora/RHEL:**
```bash
sudo dnf install gcc autoconf automake libtool \
libpcap-devel libnl3-devel pkgconfig
```
## Building
```bash
# Generate configure script
./autogen.sh
# Configure build
./configure
# Build
make
# Install (optional)
sudo make install
```
## Usage
```bash
# Run as root (required for monitor mode)
sudo ./src/wireless_monitor wlan0 11
# Or after installation
sudo wireless_monitor wlan1 36
# Example output (comparable to ESP32 debug logs):
# [1770775602.813] DATA: TA=80:84:89:93:c4:b6, RA=e0:46:ee:07:df:e1, Size=228 bytes, Dur=25038 us, RSSI=-94 dBm, Retry=YES
```
## Comparison with ESP32
The output format is designed to match ESP32's debug output format:
- **TA** = Transmitter Address (same as ESP32's `addr2`)
- **RA** = Receiver Address (same as ESP32's `addr1`)
- Frame type, size, duration, RSSI, retry flag
You can run both simultaneously on the same channel and compare:
1. Are the same frames seen?
2. Do RA/TA match?
3. Do durations match?
4. Are retry flags consistent?
## Project Structure
```
wireless-monitor/
├── configure.ac # Autoconf configuration
├── Makefile.am # Top-level automake file
├── autogen.sh # Script to generate configure
├── README.md
└── src/
├── Makefile.am # Source automake file
├── main.c # Main program
├── monitor.c # Monitor mode setup (libnl3)
├── monitor.h
├── capture.c # Packet capture (libpcap)
├── capture.h
├── frame_parser.c # 802.11 frame parsing
└── frame_parser.h # Frame structures (matches ESP32)
```

140
SETUP_GIT.md Normal file
View File

@ -0,0 +1,140 @@
# Setting Up Git Repository
This guide helps you set up this project in the Umber git repository.
## Option 1: Create New Repository
### On Your Git Server (Gitea/GitHub/etc.)
1. Create a new repository named `wireless-monitor` (or your preferred name)
2. Note the repository URL (e.g., `https://git.umber.com/rjmcmahon/wireless-monitor.git`)
### On Raspberry Pi 5
```bash
cd wireless-monitor-template
# Initialize git (if not already done)
git init
# Add all files
git add -A
# Create initial commit
git commit -m "Initial commit: Linux wireless monitor for ESP32 verification"
# Add remote repository
git remote add origin https://git.umber.com/rjmcmahon/wireless-monitor.git
# Push to repository
git push -u origin main
# Or if your default branch is 'master':
# git push -u origin master
```
## Option 2: Add to Existing Repository
If you want to add this as a subdirectory to an existing repository:
```bash
# From the parent repository
cd /path/to/parent/repo
# Copy the template
cp -r wireless-monitor-template wireless-monitor
# Add to git
cd wireless-monitor
git init
git add -A
git commit -m "Add wireless monitor tool for ESP32 verification"
# Add as submodule (if parent repo uses submodules)
# Or just add files directly to parent repo
```
## Option 3: Standalone Repository
If you want this as a completely separate repository:
```bash
cd wireless-monitor-template
# Initialize git
git init
# Create .gitignore (already included)
# Add all files
git add -A
# Initial commit
git commit -m "Initial commit: Linux wireless monitor
A C program using GNU Automake for capturing and parsing 802.11 WiFi
frame headers in monitor mode. Designed to verify ESP32 monitor code
by comparing Linux vs ESP32 frame parsing."
# Add remote (replace with your actual repository URL)
git remote add origin https://git.umber.com/rjmcmahon/wireless-monitor.git
# Push
git branch -M main # Or 'master' if that's your default
git push -u origin main
```
## Building on Raspberry Pi 5
After cloning the repository:
```bash
# Clone repository
git clone https://git.umber.com/rjmcmahon/wireless-monitor.git
cd wireless-monitor
# Run build script
./build_pi5.sh
# Or manually:
./autogen.sh
./configure
make
```
## Repository Structure
```
wireless-monitor/
├── .gitignore
├── configure.ac
├── Makefile.am
├── autogen.sh
├── build_pi5.sh # Automated build script for Pi 5
├── README.md
├── BUILD_PI5.md # Build instructions
├── VERIFICATION_GUIDE.md # ESP32 verification guide
└── src/
├── Makefile.am
├── main.c
├── monitor.c
├── monitor.h
├── capture.c
├── capture.h
├── frame_parser.c
└── frame_parser.h
```
## Git Workflow
```bash
# Make changes
# ... edit files ...
# Stage changes
git add -A
# Commit
git commit -m "Description of changes"
# Push
git push origin main
```

101
VERIFICATION_GUIDE.md Normal file
View File

@ -0,0 +1,101 @@
# ESP32 Monitor Verification Guide
This Linux C program captures 802.11 frames and displays them in a format that matches ESP32's debug output, allowing you to verify that the ESP32 monitor code is correctly parsing frames.
## Quick Start
```bash
# Build
cd wireless-monitor-template
./autogen.sh
./configure
make
# Run (as root)
sudo ./src/wireless_monitor wlan0 11
# With MAC filter (to match ESP32 filter)
sudo ./src/wireless_monitor wlan0 11 80:84:89:93:c4:b6
```
## Comparison Workflow
### 1. Setup ESP32 Monitor
```bash
# On ESP32 console
monitor start -c 11
monitor debug on
monitor filter 80:84:89:93:c4:b6
```
### 2. Setup Linux Monitor (Same Channel)
```bash
# On Linux machine (same channel!)
sudo ./src/wireless_monitor wlan0 11 80:84:89:93:c4:b6
```
### 3. Generate Test Traffic
```bash
# On another device (e.g., Raspberry Pi)
iperf -c <server_ip> -u -b 10M -t 60
```
### 4. Compare Outputs
**ESP32 Output:**
```
[1770775602.813] I MONITOR: DATA: DATA, TA=80:84:89:93:c4:b6, Size=228 bytes, Rate=54 Mbps, MCS=0, SS=1, BW=20 MHz, RSSI=-94 dBm, Retry:YES
```
**Linux Output:**
```
[1770775602.813] DATA: TA=80:84:89:93:c4:b6, RA=e0:46:ee:07:df:e1, Size=228 bytes, Dur=25038 us, RSSI=-94 dBm, Retry=YES
```
## What to Verify
1. **Same Frames Seen**: Do both see the same number of frames?
2. **TA Matches**: Transmitter Address should be identical
3. **RA Present**: Linux shows RA, ESP32 extracts it as `addr1`
4. **Duration/NAV**: Duration field should match (for collapse detection)
5. **Retry Flag**: Should be consistent
6. **Frame Types**: Both should classify frames the same way
## Troubleshooting
### No Frames on Linux but ESP32 Sees Them
- **Channel Mismatch**: Ensure both are on the same channel
- **Interface Issue**: Check `iw dev wlan0 info` shows monitor mode
- **Permissions**: Must run as root
### Different TA/RA Values
- **Address Order**: ESP32 uses `addr2`=TA, `addr1`=RA (correct)
- **To DS/From DS**: Address meanings change based on these bits
- **Frame Direction**: Client→AP vs AP→Client have different addressing
### Duration Mismatches
- **NAV Field**: Both should read the same Duration/ID field
- **Expected vs Actual**: ESP32 calculates expected duration, Linux shows actual
- **Collapse Detection**: Large mismatches indicate potential collisions
## Example: Debugging Missing Frames
If ESP32 shows `Filter: 80:84:89:93:c4:b6 (0 frames, 0.0 fps)` but Linux sees frames:
1. **Check Channel**: `monitor status` on ESP32 vs `iw dev wlan0 info` on Linux
2. **Check MAC**: Verify the MAC address is correct
3. **Check Filter Logic**: ESP32 checks both TA and RA, Linux does the same
4. **Check Frame Types**: ESP32 might be filtering by frame type
## Notes
- Linux output format matches ESP32 debug logs for easy comparison
- Both tools filter on TA **and** RA to maximize matches
- Radiotap header parsing (RSSI, MCS) is simplified - full parsing would require radiotap library
- For accurate PHY info, consider using `tcpdump` or `wireshark` with radiotap support

15
autogen.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/sh
# Run this script to generate the configure script and Makefile.in files
set -e
echo "Running aclocal..."
aclocal
echo "Running autoconf..."
autoconf
echo "Running automake..."
automake --add-missing --copy
echo "Done. Now run ./configure && make"

64
build_pi5.sh Executable file
View File

@ -0,0 +1,64 @@
#!/bin/bash
# Build script for Raspberry Pi 5
# This script installs dependencies and builds the wireless monitor tool
set -e
echo "=== Wireless Monitor - Raspberry Pi 5 Build ==="
echo ""
# Check if running on Raspberry Pi (optional check)
if [ -f /proc/device-tree/model ] && grep -q "Raspberry Pi" /proc/device-tree/model 2>/dev/null; then
echo "Detected Raspberry Pi"
cat /proc/device-tree/model
echo ""
fi
# Check for root (needed for some package checks, but not for building)
if [ "$EUID" -eq 0 ]; then
echo "Warning: Running as root. Building as regular user is recommended."
echo ""
fi
# Install dependencies
echo "=== Installing Dependencies ==="
sudo apt-get update
sudo apt-get install -y \
build-essential \
autoconf \
automake \
libtool \
pkg-config \
libpcap-dev \
libnl-genl-3-dev \
libnl-3-dev
echo ""
echo "=== Building ==="
# Generate configure script
if [ ! -f configure ]; then
echo "Running autogen.sh..."
./autogen.sh
fi
# Configure
if [ ! -f Makefile ]; then
echo "Running configure..."
./configure
fi
# Build
echo "Running make..."
make
echo ""
echo "=== Build Complete ==="
echo ""
echo "Binary location: ./src/wireless_monitor"
echo ""
echo "To test (requires root for monitor mode):"
echo " sudo ./src/wireless_monitor wlan0 11"
echo ""
echo "To install system-wide:"
echo " sudo make install"

31
configure.ac Normal file
View File

@ -0,0 +1,31 @@
AC_PREREQ([2.69])
AC_INIT([wireless-monitor], [1.0.0], [your-email@example.com])
AM_INIT_AUTOMAKE([-Wall -Werror foreign subdir-objects])
AC_CONFIG_SRCDIR([src/main.c])
AC_CONFIG_HEADERS([config.h])
# Checks for programs
AC_PROG_CC
AC_PROG_CC_C99
AM_PROG_AR
# Checks for libraries
PKG_CHECK_MODULES([LIBPCAP], [libpcap])
PKG_CHECK_MODULES([LIBNL], [libnl-genl-3.0 libnl-3.0])
# Checks for header files
AC_CHECK_HEADERS([sys/socket.h netinet/in.h linux/nl80211.h])
# Checks for typedefs, structures, and compiler characteristics
AC_TYPE_UINT8_T
AC_TYPE_UINT16_T
AC_TYPE_UINT32_T
# Checks for library functions
AC_CHECK_FUNCS([memset memcpy strncpy])
AC_CONFIG_FILES([
Makefile
src/Makefile
])
AC_OUTPUT

24
src/Makefile.am Normal file
View File

@ -0,0 +1,24 @@
bin_PROGRAMS = wireless_monitor
wireless_monitor_SOURCES = \
main.c \
monitor.c \
capture.c \
frame_parser.c
wireless_monitor_HEADERS = \
monitor.h \
capture.h \
frame_parser.h
wireless_monitor_CFLAGS = \
$(LIBPCAP_CFLAGS) \
$(LIBNL_CFLAGS) \
-I$(top_srcdir)/src
wireless_monitor_LDADD = \
$(LIBPCAP_LIBS) \
$(LIBNL_LIBS)
# Include headers in distribution
include_HEADERS = monitor.h capture.h frame_parser.h

71
src/capture.c Normal file
View File

@ -0,0 +1,71 @@
#include "config.h"
#include "capture.h"
#include "frame_parser.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pcap/pcap.h>
#include <signal.h>
#include <errno.h>
#include <time.h>
static pcap_t *pcap_handle = NULL;
static volatile int capture_running = 0;
static void packet_handler(u_char *user, const struct pcap_pkthdr *h, const u_char *bytes) {
packet_callback_t callback = (packet_callback_t)user;
if (callback && h->caplen >= 24) { // Minimum 802.11 header size
callback(bytes, h->caplen, NULL);
}
}
int start_capture(const char *interface, packet_callback_t callback, void *user_data) {
char errbuf[PCAP_ERRBUF_SIZE];
if (capture_running) {
fprintf(stderr, "Capture already running\n");
return -1;
}
// Open interface for capture
pcap_handle = pcap_open_live(interface, 65535, 1, 1000, errbuf);
if (!pcap_handle) {
fprintf(stderr, "Failed to open interface %s: %s\n", interface, errbuf);
return -1;
}
// Set non-blocking mode
if (pcap_setnonblock(pcap_handle, 0, errbuf) < 0) {
fprintf(stderr, "Warning: Failed to set blocking mode: %s\n", errbuf);
}
capture_running = 1;
// Start capture loop
printf("Starting capture loop...\n");
while (capture_running) {
int ret = pcap_dispatch(pcap_handle, 1, packet_handler, (u_char *)callback);
if (ret < 0) {
if (ret == PCAP_ERROR_BREAK) {
// Normal break
break;
}
fprintf(stderr, "Capture error: %s\n", pcap_geterr(pcap_handle));
break;
} else if (ret == 0) {
// Timeout - continue
continue;
}
}
return 0;
}
void stop_capture(void) {
capture_running = 0;
if (pcap_handle) {
pcap_breakloop(pcap_handle);
pcap_close(pcap_handle);
pcap_handle = NULL;
}
}

29
src/capture.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef CAPTURE_H
#define CAPTURE_H
#include <stdint.h>
#include <stddef.h>
/**
* @brief Packet callback function type
* @param packet Pointer to packet data
* @param len Packet length
* @param user_data User-provided data pointer
*/
typedef void (*packet_callback_t)(const uint8_t *packet, size_t len, void *user_data);
/**
* @brief Start packet capture on interface
* @param interface Interface name
* @param callback Callback function for each packet
* @param user_data User data passed to callback
* @return 0 on success, -1 on error
*/
int start_capture(const char *interface, packet_callback_t callback, void *user_data);
/**
* @brief Stop packet capture
*/
void stop_capture(void);
#endif /* CAPTURE_H */

161
src/frame_parser.c Normal file
View File

@ -0,0 +1,161 @@
#include "config.h"
#include "frame_parser.h"
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
// Radiotap header parsing (simplified)
static int skip_radiotap_header(const uint8_t *packet, size_t len, size_t *offset) {
if (len < 8) return -1;
// Check for radiotap header (starts with version 0)
if (packet[0] == 0 && packet[1] == 0) {
// Radiotap header present
uint16_t rt_len = *(uint16_t *)&packet[2];
if (rt_len > len || rt_len < 8) return -1;
// Extract RSSI and rate from radiotap (simplified - full parsing is complex)
// For now, just skip the header
*offset = rt_len;
return 0;
}
// No radiotap header
*offset = 0;
return 0;
}
int parse_80211_frame(const uint8_t *packet, size_t len, wifi_frame_info_t *frame_info) {
size_t offset = 0;
// Skip radiotap header if present
if (skip_radiotap_header(packet, len, &offset) < 0) {
return -1;
}
if (len < offset + 24) {
return -1; // Too short for 802.11 header
}
const uint8_t *frame = packet + offset;
// Parse Frame Control (bytes 0-1)
frame_info->frame_control = frame[0] | (frame[1] << 8);
frame_info->type = (frame[0] >> 2) & 0x03;
frame_info->subtype = (frame[0] >> 4) & 0x0F;
frame_info->to_ds = (frame[1] >> 0) & 0x01;
frame_info->from_ds = (frame[1] >> 1) & 0x01;
frame_info->retry = (frame[1] >> 3) & 0x01;
// Parse Duration/ID (bytes 2-3)
frame_info->duration_id = frame[2] | (frame[3] << 8);
// Parse Addresses based on To DS / From DS bits
// addr1 = RA (Receiver Address)
// addr2 = TA (Transmitter Address)
// addr3 = BSSID/SA/DA
memcpy(frame_info->addr1, &frame[4], 6); // RA
memcpy(frame_info->addr2, &frame[10], 6); // TA
memcpy(frame_info->addr3, &frame[16], 6); // BSSID/SA/DA
// Check for Address 4 (only if To DS and From DS both set)
frame_info->has_addr4 = frame_info->to_ds && frame_info->from_ds;
if (frame_info->has_addr4) {
if (len < offset + 30) return -1;
memcpy(frame_info->addr4, &frame[22], 6);
}
// Parse Sequence Control (bytes 22-23 or 28-29)
uint16_t seq_offset = frame_info->has_addr4 ? 28 : 22;
if (len < offset + seq_offset + 2) return -1;
frame_info->seq_ctrl = frame[seq_offset] | (frame[seq_offset + 1] << 8);
frame_info->fragment_num = frame_info->seq_ctrl & 0x0F;
frame_info->sequence_num = (frame_info->seq_ctrl >> 4) & 0x0FFF;
// Initialize PHY info (would need radiotap parsing for accurate values)
frame_info->rssi = -100; // Default
frame_info->mcs = 0;
frame_info->spatial_streams = 1;
frame_info->bandwidth = 0;
frame_info->sgi = false;
frame_info->phy_rate_kbps = 0;
frame_info->frame_len = len - offset;
return 0;
}
const char *get_frame_type_name(uint8_t type, uint8_t subtype) {
switch (type) {
case FRAME_TYPE_MANAGEMENT:
switch (subtype) {
case 0: return "ASSOC_REQ";
case 1: return "ASSOC_RESP";
case 2: return "REASSOC_REQ";
case 3: return "REASSOC_RESP";
case 4: return "PROBE_REQ";
case 5: return "PROBE_RESP";
case 8: return "BEACON";
case 10: return "DISASSOC";
case 11: return "AUTH";
case 12: return "DEAUTH";
default: return "MGMT_UNKNOWN";
}
case FRAME_TYPE_CONTROL:
switch (subtype) {
case 11: return "RTS";
case 12: return "CTS";
case 13: return "ACK";
default: return "CTRL_UNKNOWN";
}
case FRAME_TYPE_DATA:
switch (subtype) {
case 0: return "DATA";
case 1: return "DATA_CF_ACK";
case 2: return "DATA_CF_POLL";
case 3: return "DATA_CF_ACK_POLL";
case 4: return "NULL";
case 8: return "QOS_DATA";
case 12: return "QOS_NULL";
default: return "DATA_UNKNOWN";
}
default:
return "UNKNOWN";
}
}
void print_frame_info(const wifi_frame_info_t *frame_info, uint64_t timestamp_us) {
const char *type_name = get_frame_type_name(frame_info->type, frame_info->subtype);
printf("[%llu.%03llu] %s: ",
timestamp_us / 1000000, (timestamp_us / 1000) % 1000);
printf("TA=%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]);
printf("RA=%02x:%02x:%02x:%02x:%02x:%02x, ",
frame_info->addr1[0], frame_info->addr1[1], frame_info->addr1[2],
frame_info->addr1[3], frame_info->addr1[4], frame_info->addr1[5]);
printf("Size=%u bytes, ", frame_info->frame_len);
printf("Dur=%u us, ", frame_info->duration_id);
printf("RSSI=%d dBm", frame_info->rssi);
if (frame_info->retry) {
printf(", Retry=YES");
} else {
printf(", Retry=no");
}
if (frame_info->mcs > 0) {
printf(", MCS=%u", frame_info->mcs);
}
if (frame_info->spatial_streams > 1) {
printf(", SS=%u", frame_info->spatial_streams);
}
printf("\n");
}

76
src/frame_parser.h Normal file
View File

@ -0,0 +1,76 @@
#ifndef FRAME_PARSER_H
#define FRAME_PARSER_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
/**
* @brief 802.11 Frame types
*/
typedef enum {
FRAME_TYPE_MANAGEMENT = 0,
FRAME_TYPE_CONTROL = 1,
FRAME_TYPE_DATA = 2,
FRAME_TYPE_RESERVED = 3
} wifi_frame_type_t;
/**
* @brief Parsed 802.11 frame information (similar to ESP32 wifi_frame_info_t)
*/
typedef struct {
// Frame Control
uint16_t frame_control;
uint8_t type;
uint8_t subtype;
bool to_ds;
bool from_ds;
bool retry;
// Duration/ID (NAV)
uint16_t duration_id;
// MAC Addresses
uint8_t addr1[6]; // Receiver Address (RA)
uint8_t addr2[6]; // Transmitter Address (TA)
uint8_t addr3[6]; // BSSID/SA/DA
uint8_t addr4[6]; // Optional Address 4
bool has_addr4;
// Sequence Control
uint16_t seq_ctrl;
uint16_t fragment_num;
uint16_t sequence_num;
// PHY info (from radiotap header if available)
int8_t rssi;
uint8_t mcs;
uint8_t spatial_streams; // NSS
uint8_t bandwidth; // 0=20MHz, 1=40MHz, 2=80MHz, 3=160MHz
bool sgi; // Short Guard Interval
uint32_t phy_rate_kbps;
// Frame size
uint16_t frame_len;
} wifi_frame_info_t;
/**
* @brief Parse 802.11 frame header
* @param packet Raw packet data (including radiotap header if present)
* @param len Packet length
* @param frame_info Output: parsed frame information
* @return 0 on success, -1 on error
*/
int parse_80211_frame(const uint8_t *packet, size_t len, wifi_frame_info_t *frame_info);
/**
* @brief Get human-readable frame type name
*/
const char *get_frame_type_name(uint8_t type, uint8_t subtype);
/**
* @brief Print frame information (for verification/comparison with ESP32)
*/
void print_frame_info(const wifi_frame_info_t *frame_info, uint64_t timestamp_us);
#endif /* FRAME_PARSER_H */

148
src/main.c Normal file
View File

@ -0,0 +1,148 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include "config.h"
#include "monitor.h"
#include "capture.h"
#include "frame_parser.h"
static volatile int running = 1;
static uint64_t packet_count = 0;
static uint64_t data_frame_count = 0;
static uint8_t filter_mac[6] = {0};
static bool filter_enabled = false;
static void signal_handler(int sig) {
(void)sig;
running = 0;
stop_capture();
}
static uint64_t get_timestamp_us(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return (uint64_t)tv.tv_sec * 1000000ULL + (uint64_t)tv.tv_usec;
}
static bool mac_match(const uint8_t *mac1, const uint8_t *mac2) {
return memcmp(mac1, mac2, 6) == 0;
}
static void packet_callback(const uint8_t *packet, size_t len, void *user_data) {
(void)user_data;
wifi_frame_info_t frame_info;
uint64_t timestamp_us = get_timestamp_us();
packet_count++;
// Parse 802.11 frame
if (parse_80211_frame(packet, len, &frame_info) == 0) {
// Only print data frames (for comparison with ESP32)
if (frame_info.type == FRAME_TYPE_DATA) {
// Apply MAC filter if enabled (check both TA and RA like ESP32)
bool should_print = true;
if (filter_enabled) {
should_print = mac_match(frame_info.addr2, filter_mac) ||
mac_match(frame_info.addr1, filter_mac);
}
if (should_print) {
data_frame_count++;
print_frame_info(&frame_info, timestamp_us);
}
}
}
}
static int parse_mac(const char *mac_str, uint8_t *mac_out) {
int values[6];
if (sscanf(mac_str, "%x:%x:%x:%x:%x:%x",
&values[0], &values[1], &values[2],
&values[3], &values[4], &values[5]) != 6) {
return -1;
}
for (int i = 0; i < 6; i++) {
if (values[i] < 0 || values[i] > 255) return -1;
mac_out[i] = (uint8_t)values[i];
}
return 0;
}
int main(int argc, char **argv) {
if (argc < 3) {
fprintf(stderr, "Usage: %s <interface> <channel> [filter_mac]\n", argv[0]);
fprintf(stderr, "Example: %s wlan0 11\n", argv[0]);
fprintf(stderr, "Example: %s wlan0 11 80:84:89:93:c4:b6\n", argv[0]);
fprintf(stderr, "\n");
fprintf(stderr, "This tool captures 802.11 frames to verify ESP32 monitor code.\n");
fprintf(stderr, "Output format matches ESP32 debug logs for easy comparison.\n");
return 1;
}
const char *interface = argv[1];
int channel = atoi(argv[2]);
if (channel < 1 || channel > 165) {
fprintf(stderr, "Invalid channel: %d (must be 1-165)\n", channel);
return 1;
}
// Parse optional MAC filter
if (argc >= 4) {
if (parse_mac(argv[3], filter_mac) == 0) {
filter_enabled = true;
printf("Filter enabled: %02x:%02x:%02x:%02x:%02x:%02x\n",
filter_mac[0], filter_mac[1], filter_mac[2],
filter_mac[3], filter_mac[4], filter_mac[5]);
} else {
fprintf(stderr, "Invalid MAC address format: %s\n", argv[3]);
fprintf(stderr, "Expected format: XX:XX:XX:XX:XX:XX\n");
return 1;
}
}
// Setup signal handlers
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
printf("=== Wireless Monitor ===\n");
printf("Interface: %s\n", interface);
printf("Channel: %d\n", channel);
printf("\n");
// Set monitor mode
if (set_monitor_mode(interface, channel) != 0) {
fprintf(stderr, "Failed to set monitor mode\n");
return 1;
}
printf("Monitor mode activated\n");
printf("Capturing 802.11 frames... (Ctrl+C to stop)\n");
printf("This output can be compared with ESP32 monitor debug logs\n");
if (filter_enabled) {
printf("Filter: %02x:%02x:%02x:%02x:%02x:%02x (showing frames with matching TA or RA)\n",
filter_mac[0], filter_mac[1], filter_mac[2],
filter_mac[3], filter_mac[4], filter_mac[5]);
}
printf("\n");
// Start packet capture (blocks until stopped)
if (start_capture(interface, packet_callback, NULL) != 0) {
fprintf(stderr, "Failed to start capture\n");
restore_managed_mode(interface);
return 1;
}
printf("\n=== Capture Statistics ===\n");
printf("Total packets: %llu\n", (unsigned long long)packet_count);
printf("Data frames: %llu\n", (unsigned long long)data_frame_count);
restore_managed_mode(interface);
printf("Done\n");
return 0;
}

237
src/monitor.c Normal file
View File

@ -0,0 +1,237 @@
#include "config.h"
#include "monitor.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/nl80211.h>
#include <netlink/genl/genl.h>
#include <netlink/genl/ctrl.h>
#include <netlink/netlink.h>
#include <net/if.h>
#include <errno.h>
static struct nl_sock *nl_sock = NULL;
static int nl80211_id = -1;
static int nl80211_init(void) {
nl_sock = nl_socket_alloc();
if (!nl_sock) {
fprintf(stderr, "Failed to allocate netlink socket\n");
return -1;
}
if (genl_connect(nl_sock) < 0) {
fprintf(stderr, "Failed to connect to netlink\n");
nl_socket_free(nl_sock);
nl_sock = NULL;
return -1;
}
nl80211_id = genl_ctrl_resolve(nl_sock, "nl80211");
if (nl80211_id < 0) {
fprintf(stderr, "nl80211 not found\n");
nl_close(nl_sock);
nl_socket_free(nl_sock);
nl_sock = NULL;
return -1;
}
return 0;
}
static void nl80211_cleanup(void) {
if (nl_sock) {
nl_close(nl_sock);
nl_socket_free(nl_sock);
nl_sock = NULL;
}
}
int set_monitor_mode(const char *interface, int channel) {
struct nl_msg *msg;
struct nl_cb *cb;
int ifindex;
int err = 0;
if (nl80211_init() < 0) {
return -1;
}
ifindex = if_nametoindex(interface);
if (ifindex == 0) {
fprintf(stderr, "Interface %s not found\n", interface);
nl80211_cleanup();
return -1;
}
// Create message to set monitor mode
msg = nlmsg_alloc();
if (!msg) {
fprintf(stderr, "Failed to allocate netlink message\n");
nl80211_cleanup();
return -1;
}
genlmsg_put(msg, NL_AUTO_PID, NL_AUTO_SEQ, nl80211_id, 0,
NLM_F_REQUEST, NL80211_CMD_SET_INTERFACE, 0);
NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, ifindex);
NLA_PUT_U32(msg, NL80211_ATTR_IFTYPE, NL80211_IFTYPE_MONITOR);
// Send message
cb = nl_cb_alloc(NL_CB_DEFAULT);
if (!cb) {
fprintf(stderr, "Failed to allocate netlink callback\n");
nlmsg_free(msg);
nl80211_cleanup();
return -1;
}
err = nl_send_auto_complete(nl_sock, msg);
if (err < 0) {
fprintf(stderr, "Failed to send netlink message: %s\n", nl_geterror(err));
nl_cb_put(cb);
nlmsg_free(msg);
nl80211_cleanup();
return -1;
}
nl_cb_put(cb);
nlmsg_free(msg);
// Set channel
if (set_channel(interface, channel) < 0) {
nl80211_cleanup();
return -1;
}
nl80211_cleanup();
return 0;
nla_put_failure:
fprintf(stderr, "Failed to add netlink attribute\n");
nlmsg_free(msg);
nl80211_cleanup();
return -1;
}
int restore_managed_mode(const char *interface) {
struct nl_msg *msg;
int ifindex;
int err = 0;
if (nl80211_init() < 0) {
return -1;
}
ifindex = if_nametoindex(interface);
if (ifindex == 0) {
fprintf(stderr, "Interface %s not found\n", interface);
nl80211_cleanup();
return -1;
}
msg = nlmsg_alloc();
if (!msg) {
fprintf(stderr, "Failed to allocate netlink message\n");
nl80211_cleanup();
return -1;
}
genlmsg_put(msg, NL_AUTO_PID, NL_AUTO_SEQ, nl80211_id, 0,
NLM_F_REQUEST, NL80211_CMD_SET_INTERFACE, 0);
NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, ifindex);
NLA_PUT_U32(msg, NL80211_ATTR_IFTYPE, NL80211_IFTYPE_STATION);
err = nl_send_auto_complete(nl_sock, msg);
if (err < 0) {
fprintf(stderr, "Failed to send netlink message: %s\n", nl_geterror(err));
nlmsg_free(msg);
nl80211_cleanup();
return -1;
}
nlmsg_free(msg);
nl80211_cleanup();
return 0;
nla_put_failure:
fprintf(stderr, "Failed to add netlink attribute\n");
nlmsg_free(msg);
nl80211_cleanup();
return -1;
}
// Convert WiFi channel number to frequency in MHz
static uint32_t channel_to_freq(int channel) {
if (channel >= 1 && channel <= 14) {
// 2.4 GHz: 2412 + (channel - 1) * 5
return 2412 + (channel - 1) * 5;
} else if (channel >= 36 && channel <= 165) {
// 5 GHz: 5000 + channel * 5
return 5000 + channel * 5;
} else {
// Invalid channel
return 0;
}
}
int set_channel(const char *interface, int channel) {
struct nl_msg *msg;
int ifindex;
uint32_t freq_mhz;
int err = 0;
// Convert channel to frequency
freq_mhz = channel_to_freq(channel);
if (freq_mhz == 0) {
fprintf(stderr, "Invalid channel: %d\n", channel);
return -1;
}
if (nl80211_init() < 0) {
return -1;
}
ifindex = if_nametoindex(interface);
if (ifindex == 0) {
fprintf(stderr, "Interface %s not found\n", interface);
nl80211_cleanup();
return -1;
}
msg = nlmsg_alloc();
if (!msg) {
fprintf(stderr, "Failed to allocate netlink message\n");
nl80211_cleanup();
return -1;
}
genlmsg_put(msg, NL_AUTO_PID, NL_AUTO_SEQ, nl80211_id, 0,
NLM_F_REQUEST, NL80211_CMD_SET_CHANNEL, 0);
NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, ifindex);
NLA_PUT_U32(msg, NL80211_ATTR_WIPHY_FREQ, freq_mhz);
err = nl_send_auto_complete(nl_sock, msg);
if (err < 0) {
fprintf(stderr, "Failed to set channel %d (freq %u MHz): %s\n",
channel, freq_mhz, nl_geterror(err));
nlmsg_free(msg);
nl80211_cleanup();
return -1;
}
nlmsg_free(msg);
nl80211_cleanup();
return 0;
nla_put_failure:
fprintf(stderr, "Failed to add netlink attribute\n");
nlmsg_free(msg);
nl80211_cleanup();
return -1;
}

30
src/monitor.h Normal file
View File

@ -0,0 +1,30 @@
#ifndef MONITOR_H
#define MONITOR_H
#include <stdint.h>
#include <stdbool.h>
/**
* @brief Set WiFi interface to monitor mode
* @param interface Interface name (e.g., "wlan0")
* @param channel Channel number (1-165)
* @return 0 on success, -1 on error
*/
int set_monitor_mode(const char *interface, int channel);
/**
* @brief Restore WiFi interface to managed mode
* @param interface Interface name
* @return 0 on success, -1 on error
*/
int restore_managed_mode(const char *interface);
/**
* @brief Set channel for monitor interface
* @param interface Interface name
* @param channel Channel number (1-165)
* @return 0 on success, -1 on error
*/
int set_channel(const char *interface, int channel);
#endif /* MONITOR_H */