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.
272 lines
8.0 KiB
C
272 lines
8.0 KiB
C
/*
|
|
* 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 */
|