From 0a02cbef6c5f256b70168889ea7f609aa3fcf3bc Mon Sep 17 00:00:00 2001 From: Yan Ke Date: Wed, 19 Aug 2026 15:08:24 +0800 Subject: [PATCH 01/12] Increase buffers for speed tests --- README.md | 14 +++++ sdkconfig.defaults | 5 ++ src/main.c | 123 ++++++++++++++++++++++++++++++++++++++++++-- src/transport_tcp.c | 11 ++-- 4 files changed, 146 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a9caaf5..cb26297 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,20 @@ IP (`apip`), VLESS profile (`vless` or `vless-uri`), DNS resolver (`dns`), multiplexing (`mux`), and portal password (`admin`). It never prints saved passwords or the VLESS UUID. +For throughput diagnosis, start a speed test and enter `diag` twice about one +second apart. It reports the payload upload/download rate, STA RSSI and channel, +AP client count, TCP window/buffer configuration, UDP/smux queue pressure, +dropped datagrams, and free internal/PSRAM. The firmware uses 32 KiB TCP +receive/send buffers, SACK, 32 Wi-Fi TX buffers, and disables STA modem sleep; +the previous 5,760-byte TCP windows could limit a VLESS TCP stream to roughly +1 Mbps at high RTT. Use `route direct` for a same-boot, +non-VLESS comparison against a China speed-test server, then `route vless` to +return to the tunnel. **Direct mode sends TCP and UDP to the upstream network +without VLESS protection** and resets to VLESS on reboot. + +If the USB console is unavailable, the authenticated local portal exposes the +same temporary route test: `POST /api/route` with `mode=direct` or `mode=vless`. + After flashing, join the setup AP and visit the AP gateway at `http://192.168.4.1` by default. Configure the router AP, downstream subnet by setting the ESP32 gateway IP, upstream Wi-Fi, and VLESS profile independently. Do not commit a configured `sdkconfig`, NVS dump, URI, UUID, or Wi-Fi password to a public repository. For browser-only flashing, every build creates a target-specific image such as `dist/esp32-vless-router-esp32s3_r8n16.bin` or `dist/esp32-vless-router-esp32s3_r8n8.bin`. Follow [Browser-based flashing](docs/WEB_FLASHING.md) to program the image matching your board at address `0x0` using esptool-js. This full image installs the target's OTA partition table and can clear saved configuration; record the router settings first. diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 80baf40..1a62d98 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -7,11 +7,16 @@ CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65536 CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10 CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=64 CONFIG_ESP_WIFI_RX_BA_WIN=6 +CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM=32 +CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM=64 CONFIG_LWIP_MAX_SOCKETS=120 CONFIG_LWIP_DHCPS_MAX_STATION_NUM=16 # CONFIG_LWIP_IPV6 is not set CONFIG_LWIP_MAX_ACTIVE_TCP=128 CONFIG_LWIP_MAX_LISTENING_TCP=64 +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=32768 +CONFIG_LWIP_TCP_WND_DEFAULT=32768 CONFIG_LWIP_TCP_RECVMBOX_SIZE=32 CONFIG_LWIP_TCP_ACCEPTMBOX_SIZE=32 CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4 +CONFIG_LWIP_TCP_SACK_OUT=y diff --git a/src/main.c b/src/main.c index fb355e2..17f22b3 100644 --- a/src/main.c +++ b/src/main.c @@ -161,6 +161,7 @@ static bool dns_name(const uint8_t *packet, int bytes, int *offset, char name[25 static void json_string(httpd_req_t *req, const char *text); static void apply_access_point_task(void *argument); static void reset_transparent_transport_state(void); +static void set_transparent_mode(transparent_mode_t mode); static bool text_changed(const char *left, const char *right) { @@ -226,6 +227,57 @@ static bandwidth_stats_t bandwidth_snapshot(void) return stats; } +/* Safe to call while a speed test is running. It reports aggregate payload + flow without enabling the high-volume packet logging switch. */ +static void serial_diagnostics(void) +{ + bandwidth_stats_t bandwidth = bandwidth_snapshot(); + wifi_ap_record_t upstream = {0}; + uint8_t primary = 0; + wifi_second_chan_t secondary = WIFI_SECOND_CHAN_NONE; + wifi_sta_list_t clients = {0}; + esp_err_t upstream_result = esp_wifi_sta_get_ap_info(&upstream); + esp_err_t client_result = esp_wifi_ap_get_sta_list(&clients); + uint32_t singmux_tcp_active = 0; + (void)esp_wifi_get_channel(&primary, &secondary); + for (size_t index = 0; index < SINGMUX_TCP_STREAM_MAX; ++index) + { + if (s_singmux_tcp_streams[index].in_use) + { + ++singmux_tcp_active; + } + } + + ESP_LOGI(TAG, + "DIAG route=%s upstream=%s Wi-Fi channel=%u secondary=%d AP-clients=%u; " + "TCP wnd=32768 snd=32768 recvmbox=32; payload up=%u B/s down=%u B/s " + "(%.2f/%.2f Mbps)", + transparent_mode_name(), s_has_upstream ? "up" : "down", primary, (int)secondary, + client_result == ESP_OK ? clients.num : 0, bandwidth.upload_bps, + bandwidth.download_bps, bandwidth.upload_bps * 8.0 / 1000000.0, + bandwidth.download_bps * 8.0 / 1000000.0); + if (upstream_result == ESP_OK) + { + ESP_LOGI(TAG, "DIAG uplink RSSI=%d dBm channel=%u auth=%d", upstream.rssi, + upstream.primary, upstream.authmode); + } + else + { + ESP_LOGW(TAG, "DIAG uplink association unavailable: %s", esp_err_to_name(upstream_result)); + } + ESP_LOGI(TAG, + "DIAG queues: UDP=%u/%u drops=%u rx_drops=%u tunnel_failures=%u; smux=%s " + "TCP-streams=%u/%u control=%u/%u; heap internal=%u PSRAM=%u", + s_udp_manager_queue ? (unsigned)uxQueueMessagesWaiting(s_udp_manager_queue) : 0, + UDP_MANAGER_QUEUE_DEPTH, (unsigned)s_udp_queue_drops, (unsigned)s_udp_rx_drops, + (unsigned)s_udp_tunnel_failures, s_singmux_tunnel >= 0 ? "connected" : "off", + (unsigned)singmux_tcp_active, SINGMUX_TCP_STREAM_MAX, + s_singmux_control_queue ? (unsigned)uxQueueMessagesWaiting(s_singmux_control_queue) : 0, + SINGMUX_CONTROL_QUEUE_DEPTH, + (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL), + (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); +} + static bool access_point_ip_info(esp_netif_ip_info_t *info) { ip4_addr_t ap_ip = {0}; @@ -1252,7 +1304,7 @@ static BaseType_t create_stream_task(TaskFunction_t task, const char *name, void { /* Wi-Fi and lwIP need internal RAM. Relay task stacks can safely live in PSRAM, so connection capacity is not constrained by that scarce heap. */ - return xTaskCreateWithCaps(task, name, 6144, argument, 5, NULL, + return xTaskCreateWithCaps(task, name, 8192, argument, 5, NULL, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); } @@ -2666,6 +2718,44 @@ static esp_err_t config_post(httpd_req_t *req) return send_json(req, "{\"message\":\"VLESS configuration saved.\"}"); } +/* Physical serial and the BOOT button already support a temporary direct + route. Expose the same recovery diagnostic through authenticated local + management so a held USB console cannot prevent an A/B throughput test. */ +static esp_err_t route_post(httpd_req_t *req) +{ + if (require_admin_auth(req) != ESP_OK) + { + return ESP_FAIL; + } + if (req->content_len == 0 || req->content_len > 32) + { + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Use mode=vless or mode=direct"); + } + char body[33] = {0}; + int total = 0; + while (total < req->content_len) + { + int received = httpd_req_recv(req, body + total, req->content_len - total); + if (received <= 0) + { + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Incomplete route form"); + } + total += received; + } + char mode[12] = {0}; + if (!form_value(body, "mode", mode, sizeof(mode)) || + (strcmp(mode, "vless") != 0 && strcmp(mode, "direct") != 0)) + { + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Use mode=vless or mode=direct"); + } + set_transparent_mode(strcmp(mode, "direct") == 0 ? TRANSPARENT_MODE_UPSTREAM + : TRANSPARENT_MODE_VLESS); + ESP_LOGW(TAG, "Authenticated local route test selected %s for this boot", mode); + return send_json(req, transparent_mode_uses_vless() + ? "{\"message\":\"VLESS route active for this boot.\"}" + : "{\"message\":\"DIRECT route active for this boot; traffic bypasses VLESS.\"}"); +} + static void json_string(httpd_req_t *req, const char *text) { httpd_resp_sendstr_chunk(req, "\""); @@ -2944,7 +3034,7 @@ static void start_http_server(void) httpd_handle_t server = NULL; httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.stack_size = 8192; - config.max_uri_handlers = 14; + config.max_uri_handlers = 15; config.lru_purge_enable = true; config.uri_match_fn = httpd_uri_match_wildcard; ESP_ERROR_CHECK(httpd_start(&server, &config)); @@ -2960,6 +3050,8 @@ static void start_http_server(void) .method = HTTP_POST, .handler = config_post, .user_ctx = (void *)CONFIG_SECTION_VLESS}; + const httpd_uri_t route_post_uri = {.uri = "/api/route", .method = HTTP_POST, + .handler = route_post}; const httpd_uri_t ap_post = {.uri = "/api/access-point", .method = HTTP_POST, .handler = config_post, @@ -2980,6 +3072,7 @@ static void start_http_server(void) ESP_ERROR_CHECK(httpd_register_uri_handler(server, &upstream_post)); ESP_ERROR_CHECK(httpd_register_uri_handler(server, &upstream_delete)); ESP_ERROR_CHECK(httpd_register_uri_handler(server, &vless_post)); + ESP_ERROR_CHECK(httpd_register_uri_handler(server, &route_post_uri)); ESP_ERROR_CHECK(httpd_register_uri_handler(server, &ap_post)); ESP_ERROR_CHECK(httpd_register_uri_handler(server, &admin_password_uri)); ESP_ERROR_CHECK(httpd_register_uri_handler(server, &vless_test)); @@ -3299,7 +3392,7 @@ static void serial_console_task(void *arg) (void)arg; char line[384]; ESP_LOGI(TAG, - "Serial commands: help; status; wifi; ap; apip; vless; vless-uri; dns; mux; admin"); + "Serial commands: help; status; diag; route; wifi; ap; apip; vless; vless-uri; dns; mux; admin"); for (;;) { if (!fgets(line, sizeof(line), stdin)) @@ -3322,7 +3415,7 @@ static void serial_console_task(void *arg) } if (strcmp(command, "help") == 0) { - ESP_LOGI(TAG, "status; wifi [password]; ap "); + ESP_LOGI(TAG, "status; diag; route ; wifi [password]; ap "); ESP_LOGI(TAG, "apip "); ESP_LOGI(TAG, "vless ; vless-uri ; dns "); ESP_LOGI(TAG, "mux ; admin "); @@ -3339,6 +3432,25 @@ static void serial_console_task(void *arg) s_config.singmux_enabled ? "enabled" : "disabled"); continue; } + if (strcmp(command, "diag") == 0) + { + serial_diagnostics(); + continue; + } + if (strcmp(command, "route") == 0) + { + if (!first || second || + (strcmp(first, "vless") != 0 && strcmp(first, "direct") != 0)) + { + ESP_LOGW(TAG, "Usage: route "); + continue; + } + set_transparent_mode(strcmp(first, "vless") == 0 ? TRANSPARENT_MODE_VLESS + : TRANSPARENT_MODE_UPSTREAM); + ESP_LOGW(TAG, "Route is %s for this boot; direct bypasses VLESS for TCP and UDP", + transparent_mode_uses_vless() ? "VLESS" : "DIRECT"); + continue; + } if (strcmp(command, "mux") == 0) { if (!first || (strcmp(first, "on") != 0 && strcmp(first, "off") != 0)) @@ -3671,6 +3783,9 @@ void app_main(void) ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); apply_access_point_config(); ESP_ERROR_CHECK(esp_wifi_start()); + /* STA modem sleep adds latency and can throttle AP+STA forwarding. A + mains/USB-powered router should prefer predictable forwarding latency. */ + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); xTaskCreate(captive_dns_task, "captive_dns", 6144, NULL, 4, NULL); xTaskCreate(factory_reset_button_task, "factory_reset", 2048, NULL, 4, NULL); xTaskCreate(serial_console_task, "serial_console", 4096, NULL, 4, NULL); diff --git a/src/transport_tcp.c b/src/transport_tcp.c index ba83a47..552a8b4 100644 --- a/src/transport_tcp.c +++ b/src/transport_tcp.c @@ -23,6 +23,11 @@ static const char *TAG = "transport_tcp"; +/* 1 KiB relay reads cause excessive task/socket scheduling on an AP+STA + router. This stays comfortably below lwIP's MSS aggregation limits while + reducing copy and select overhead on sustained transfers. */ +#define TRANSPORT_RELAY_BUFFER_SIZE 4096 + bool transport_tcp_uuid_to_bytes(const char *uuid, uint8_t bytes[16]) { int output = 0; @@ -269,7 +274,7 @@ void transport_tcp_relay_vless(int client, int tunnel, const char *label, int source = tunnel_ready && (!client_ready || prefer_tunnel) ? tunnel : client; prefer_tunnel = !prefer_tunnel; int target = source == client ? tunnel : client; - uint8_t buffer[1024]; + uint8_t buffer[TRANSPORT_RELAY_BUFFER_SIZE]; int count = recv(source, buffer, sizeof(buffer), 0); if (count <= 0) { @@ -351,7 +356,7 @@ void transport_tcp_relay_plain(int client, int upstream, const char *label, int source = upstream_ready && (!client_ready || prefer_upstream) ? upstream : client; prefer_upstream = !prefer_upstream; int target = source == client ? upstream : client; - uint8_t buffer[1024]; + uint8_t buffer[TRANSPORT_RELAY_BUFFER_SIZE]; int count = recv(source, buffer, sizeof(buffer), 0); if (count <= 0) { @@ -386,4 +391,4 @@ void transport_tcp_relay(transparent_mode_t mode, int client, int tunnel, const { transport_tcp_relay_plain(client, tunnel, label, upload, download); } -} \ No newline at end of file +} From a24e6805e3e8e7fc508ba0aae5c4a77352dd017d Mon Sep 17 00:00:00 2001 From: Yan Ke Date: Wed, 19 Aug 2026 18:23:33 +0800 Subject: [PATCH 02/12] Automatically select upstream networks --- src/main.c | 147 +++++++++++++++++++++++++++++++++++++++++++++- src/portal_page.c | 2 +- 2 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/main.c b/src/main.c index 17f22b3..cda844b 100644 --- a/src/main.c +++ b/src/main.c @@ -154,6 +154,12 @@ static bool s_configuration_mode; static wifi_ap_record_t s_scan_records[20]; static uint16_t s_scan_record_count; static bool s_scan_results_available; +/* The first configured upstream gets its normal connection attempt. If that + fails during boot, scan once for a saved alternative instead of retrying the + same unavailable AP forever. */ +static bool s_boot_upstream_scan_scheduled; +static bool s_boot_upstream_connected; +static char s_boot_failed_upstream_ssid[33]; static bool socket_send_all(int socket_fd, const uint8_t *data, size_t length); static bool socket_recv_all(int socket_fd, uint8_t *data, size_t length); @@ -162,6 +168,7 @@ static void json_string(httpd_req_t *req, const char *text); static void apply_access_point_task(void *argument); static void reset_transparent_transport_state(void); static void set_transparent_mode(transparent_mode_t mode); +static void schedule_boot_upstream_scan(void); static bool text_changed(const char *left, const char *right) { @@ -371,6 +378,122 @@ static void connect_upstream(void) s_config.upstream_password, s_config.upstream_password[0] ? "" : " (open network)"); } +/* Runs outside the Wi-Fi event loop because esp_wifi_scan_start(..., true) + blocks until the scan is complete. Scan all reported APs: limiting the + result to the web UI's 20 entries could otherwise miss a weaker, but saved, + upstream. */ +static void boot_upstream_scan_task(void *argument) +{ + (void)argument; + vTaskDelay(pdMS_TO_TICKS(250)); + + wifi_scan_config_t scan = { + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time.active = {.min = 100, .max = 300}, + }; + ESP_LOGI(TAG, "Boot upstream recovery: scanning for saved Wi-Fi networks"); + esp_err_t err = esp_wifi_scan_start(&scan, true); + if (err != ESP_OK) + { + ESP_LOGW(TAG, "Boot upstream recovery scan failed: %s", esp_err_to_name(err)); + vTaskDelete(NULL); + return; + } + + uint16_t count = 0; + err = esp_wifi_scan_get_ap_num(&count); + if (err != ESP_OK) + { + ESP_LOGW(TAG, "Could not read boot scan results: %s", esp_err_to_name(err)); + vTaskDelete(NULL); + return; + } + wifi_ap_record_t *records = count ? calloc(count, sizeof(*records)) : NULL; + if (count && !records) + { + ESP_LOGW(TAG, "Not enough memory to process %u boot scan results", (unsigned)count); + vTaskDelete(NULL); + return; + } + if (count) + { + err = esp_wifi_scan_get_ap_records(&count, records); + if (err != ESP_OK) + { + ESP_LOGW(TAG, "Could not retrieve boot scan results: %s", esp_err_to_name(err)); + free(records); + vTaskDelete(NULL); + return; + } + } + + int best_profile = -1; + int best_rssi = -127; + int best_alternative_profile = -1; + int best_alternative_rssi = -127; + for (uint16_t record = 0; record < count; ++record) + { + for (uint8_t profile = 0; + profile < s_config.upstream_network_count && profile < UPSTREAM_NETWORK_MAX; ++profile) + { + if (strcmp((const char *)records[record].ssid, s_config.upstream_networks[profile].ssid) != + 0) + { + continue; + } + if (best_profile < 0 || records[record].rssi > best_rssi) + { + best_profile = profile; + best_rssi = records[record].rssi; + } + if (strcmp(s_config.upstream_networks[profile].ssid, s_boot_failed_upstream_ssid) != 0 && + (best_alternative_profile < 0 || records[record].rssi > best_alternative_rssi)) + { + best_alternative_profile = profile; + best_alternative_rssi = records[record].rssi; + } + } + } + free(records); + + /* Do not immediately retry a profile which already failed at boot when a + different saved AP is available. */ + if (best_alternative_profile >= 0) + { + best_profile = best_alternative_profile; + best_rssi = best_alternative_rssi; + } + if (best_profile < 0) + { + ESP_LOGW(TAG, "Boot upstream recovery: no saved upstream was found in %u AP(s)", + (unsigned)count); + vTaskDelete(NULL); + return; + } + + const upstream_network_t *selected = &s_config.upstream_networks[best_profile]; + strlcpy(s_config.upstream_ssid, selected->ssid, sizeof(s_config.upstream_ssid)); + strlcpy(s_config.upstream_password, selected->password, sizeof(s_config.upstream_password)); + ESP_LOGI(TAG, "Boot upstream recovery: selected saved AP '%s' (%d dBm)", + s_config.upstream_ssid, best_rssi); + connect_upstream(); + vTaskDelete(NULL); +} + +static void schedule_boot_upstream_scan(void) +{ + if (s_boot_upstream_scan_scheduled || s_configuration_mode) + { + return; + } + s_boot_upstream_scan_scheduled = true; + if (xTaskCreate(boot_upstream_scan_task, "boot_wifi_scan", 4096, NULL, 4, NULL) != pdPASS) + { + ESP_LOGW(TAG, "Could not start boot upstream recovery scan task"); + } +} + static const char *wifi_disconnect_reason_text(uint8_t reason) { switch (reason) @@ -402,12 +525,21 @@ static void network_event(void *arg, esp_event_base_t base, int32_t event, void } if (base == WIFI_EVENT && event == WIFI_EVENT_STA_START) { - connect_upstream(); + if (s_config.upstream_ssid[0]) + { + connect_upstream(); + } + else + { + /* This still scans on a first boot with no profiles, making the + available networks discoverable in the boot log. */ + schedule_boot_upstream_scan(); + } } if (base == WIFI_EVENT && event == WIFI_EVENT_STA_DISCONNECTED && s_config.upstream_ssid[0]) { const wifi_event_sta_disconnected_t *disconnected = data; - ESP_LOGW(TAG, "Upstream '%s' disconnected: %s (reason=%u); retrying", + ESP_LOGW(TAG, "Upstream '%s' disconnected: %s (reason=%u)", s_config.upstream_ssid, wifi_disconnect_reason_text(disconnected->reason), (unsigned)disconnected->reason); s_has_upstream = false; @@ -419,7 +551,15 @@ static void network_event(void *arg, esp_event_base_t base, int32_t event, void return; } status_led_show_mode(s_transparent_mode, s_has_upstream); - esp_wifi_connect(); + if (s_boot_upstream_connected) + { + ESP_LOGI(TAG, "Retrying established upstream connection"); + esp_wifi_connect(); + return; + } + strlcpy(s_boot_failed_upstream_ssid, s_config.upstream_ssid, + sizeof(s_boot_failed_upstream_ssid)); + schedule_boot_upstream_scan(); } if (base == IP_EVENT && event == IP_EVENT_STA_GOT_IP) { @@ -433,6 +573,7 @@ static void network_event(void *arg, esp_event_base_t base, int32_t event, void return; } s_has_upstream = true; + s_boot_upstream_connected = true; refresh_transparent_interception_state(); status_led_show_mode(s_transparent_mode, s_has_upstream); ESP_LOGI(TAG, "Upstream '%s' connected with IP %s; %s", s_config.upstream_ssid, ip, diff --git a/src/portal_page.c b/src/portal_page.c index fffa3b2..483c10e 100644 --- a/src/portal_page.c +++ b/src/portal_page.c @@ -74,7 +74,7 @@ const char *portal_page_html(void) "