diff --git a/.gitlab/ci/host-test.yml b/.gitlab/ci/host-test.yml index 9ac10721a7..535a8d7054 100644 --- a/.gitlab/ci/host-test.yml +++ b/.gitlab/ci/host-test.yml @@ -296,6 +296,13 @@ test_mqtt_on_host: - idf.py build - LSAN_OPTIONS=verbosity=1:log_threads=1 build/host_mqtt_client_test.elf +test_transport_on_host: + extends: .host_test_template + script: + - cd ${IDF_PATH}/components/tcp_transport/host_test + - idf.py build + - LSAN_OPTIONS=verbosity=1:log_threads=1 build/host_tcp_transport_test.elf + test_sockets_on_host: extends: .host_test_template script: diff --git a/components/tcp_transport/CMakeLists.txt b/components/tcp_transport/CMakeLists.txt index 8cbf1ec94d..f487c2e950 100644 --- a/components/tcp_transport/CMakeLists.txt +++ b/components/tcp_transport/CMakeLists.txt @@ -3,6 +3,11 @@ set(srcs "transport_ssl.c" "transport_internal.c") +if(CONFIG_LWIP_IPV4) +list(APPEND srcs + "transport_socks_proxy.c") +endif() + if(CONFIG_WS_TRANSPORT) list(APPEND srcs "transport_ws.c") @@ -10,7 +15,7 @@ endif() set(req esp-tls) if(NOT ${IDF_TARGET} STREQUAL "linux") - list(APPEND req lwip) + list(APPEND req lwip esp_timer) endif() idf_component_register(SRCS "${srcs}" diff --git a/components/tcp_transport/host_test/CMakeLists.txt b/components/tcp_transport/host_test/CMakeLists.txt new file mode 100644 index 0000000000..9041a793a0 --- /dev/null +++ b/components/tcp_transport/host_test/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.16) + +set(COMPONENTS esp_timer lwip tcp_transport main) + +list(APPEND EXTRA_COMPONENT_DIRS + "$ENV{IDF_PATH}/tools/mocks/lwip/" + "$ENV{IDF_PATH}/tools/mocks/freertos/" + "$ENV{IDF_PATH}/tools/mocks/esp_timer/" + "$ENV{IDF_PATH}/tools/mocks/esp-tls/" + ) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +project(host_tcp_transport_test) diff --git a/components/tcp_transport/host_test/README.md b/components/tcp_transport/host_test/README.md new file mode 100644 index 0000000000..fee1d99a08 --- /dev/null +++ b/components/tcp_transport/host_test/README.md @@ -0,0 +1,29 @@ +| Supported Targets | Linux | +| ----------------- | ----- | + +# Description + +This directory contains test code for `tcp_transport` that runs on host. + +Tests are written using [Catch2](https://github.com/catchorg/Catch2) test framework + +# Build + +Tests build regularly like an idf project. + +``` +idf.py build +``` + +# Run + +The build produces an executable in the build folder. + +Just run: + +``` +./build/host_tcp_transport_test.elf +``` + +The test executable have some options provided by the test framework. + diff --git a/components/tcp_transport/host_test/components/mocked_transport/CMakeLists.txt b/components/tcp_transport/host_test/components/mocked_transport/CMakeLists.txt new file mode 100644 index 0000000000..674480d59c --- /dev/null +++ b/components/tcp_transport/host_test/components/mocked_transport/CMakeLists.txt @@ -0,0 +1,6 @@ +# NOTE: This kind of mocking currently works on Linux targets only. +# On Espressif chips, too many dependencies are missing at the moment. + +idf_component_mock(INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} + REQUIRES tcp_transport + MOCK_HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/mock_transport.h) diff --git a/components/tcp_transport/host_test/components/mocked_transport/mock/mock_config.yaml b/components/tcp_transport/host_test/components/mocked_transport/mock/mock_config.yaml new file mode 100644 index 0000000000..596255b0ce --- /dev/null +++ b/components/tcp_transport/host_test/components/mocked_transport/mock/mock_config.yaml @@ -0,0 +1,9 @@ + :cmock: + :plugins: + - expect + - expect_any_args + - return_thru_ptr + - array + - ignore + - ignore_arg + - callback diff --git a/components/tcp_transport/host_test/components/mocked_transport/mock_transport.h b/components/tcp_transport/host_test/components/mocked_transport/mock_transport.h new file mode 100644 index 0000000000..02771e6c80 --- /dev/null +++ b/components/tcp_transport/host_test/components/mocked_transport/mock_transport.h @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "esp_transport.h" + + int mock_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms); + + int mock_close(esp_transport_handle_t t); + + int mock_write(esp_transport_handle_t t, const char *buffer, int len, int timeout_ms); + + int mock_read(esp_transport_handle_t t, char *buffer, int len, int timeout_ms); + + int mock_poll_read(esp_transport_handle_t t, int timeout_ms); + + int mock_poll_write(esp_transport_handle_t t, int timeout_ms); + + esp_err_t mock_destroy(esp_transport_handle_t t); diff --git a/components/tcp_transport/host_test/main/CMakeLists.txt b/components/tcp_transport/host_test/main/CMakeLists.txt new file mode 100644 index 0000000000..acceea9548 --- /dev/null +++ b/components/tcp_transport/host_test/main/CMakeLists.txt @@ -0,0 +1,12 @@ +idf_component_register(SRCS "test_socks_transport.cpp" "catch_main.cpp" + REQUIRES tcp_transport mocked_transport + INCLUDE_DIRS "$ENV{IDF_PATH}/tools" + WHOLE_ARCHIVE) + +idf_component_get_property(lwip_component lwip COMPONENT_LIB) +idf_component_get_property(esp_timer_component esp_timer COMPONENT_LIB) +idf_component_get_property(tcp_transport_component tcp_transport COMPONENT_LIB) +target_link_libraries(${tcp_transport_component} PUBLIC ${lwip_component} ${esp_timer_component}) +target_compile_options(${COMPONENT_LIB} PUBLIC -fsanitize=address -fconcepts) +target_link_options(${COMPONENT_LIB} PUBLIC -fsanitize=address) +set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 20) diff --git a/components/tcp_transport/host_test/main/catch_main.cpp b/components/tcp_transport/host_test/main/catch_main.cpp new file mode 100644 index 0000000000..eda0c3afe6 --- /dev/null +++ b/components/tcp_transport/host_test/main/catch_main.cpp @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#define CATCH_CONFIG_MAIN +#include "catch/catch.hpp" diff --git a/components/tcp_transport/host_test/main/idf_component.yml b/components/tcp_transport/host_test/main/idf_component.yml new file mode 100644 index 0000000000..7bad6ab6b4 --- /dev/null +++ b/components/tcp_transport/host_test/main/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + espressif/fmt: "^9.1.0" + idf: + version: ">=4.1.0" diff --git a/components/tcp_transport/host_test/main/test_socks_transport.cpp b/components/tcp_transport/host_test/main/test_socks_transport.cpp new file mode 100644 index 0000000000..fbd7f15128 --- /dev/null +++ b/components/tcp_transport/host_test/main/test_socks_transport.cpp @@ -0,0 +1,185 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "fmt/core.h" +#include "fmt/ranges.h" +#include "catch/catch.hpp" +#include "esp_transport.h" +#include "esp_transport_socks_proxy.h" + +extern "C" { +#include "Mockmock_transport.h" +#include "Mocknetdb.h" +#include "Mockesp_timer.h" + + uint16_t lwip_htons(uint16_t n) + { + return __builtin_bswap16(n); + } +} + +using unique_transport = std::unique_ptr, decltype(&esp_transport_destroy)>; +using namespace std::literals; + +namespace { + +/* + * Makes possible to pass a capturing lambda as a callback + */ +decltype(auto) capture_lambda(auto callable) +{ + // make a static copy of the lambda to extend it's lifetime and avoid the capture. + [[maybe_unused]]static auto call = callable; + return [](Args... args) { + return call(args...); + }; +} + +auto make_response(socks_transport_error_t response) +{ + return std::array({0x00, static_cast(response), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); +} +} + +TEST_CASE("Initial", "[Initialization]") +{ + esp_transport_socks_proxy_config_t config{ .version = SOCKS4, + .address = "test_socks4_proxy", + .port = 1080}; + + mock_destroy_IgnoreAndReturn(ESP_OK); + unique_transport test_parent{esp_transport_init(), esp_transport_destroy}; + esp_transport_set_func(test_parent.get(), mock_connect, mock_read, mock_write, mock_close, mock_poll_read, mock_poll_write, mock_destroy); + + SECTION("Initialize with invalid parent transport") { + esp_transport_handle_t parent_handle = nullptr; + unique_transport socks_transport{esp_transport_socks_proxy_init(parent_handle, &config), esp_transport_destroy}; + REQUIRE(socks_transport == nullptr); + } + + SECTION("Initialize with NULL config") { + auto *socks_transport = esp_transport_socks_proxy_init(test_parent.get(), nullptr); + REQUIRE(socks_transport == nullptr); + } + + SECTION("Initialize with NULL address config") { + config.address = nullptr; + auto *socks_transport = esp_transport_socks_proxy_init(test_parent.get(), &config); + REQUIRE(socks_transport == nullptr); + } + + SECTION("Successful Initialization") { + auto *socks_transport = esp_transport_socks_proxy_init(test_parent.get(), &config); + REQUIRE(socks_transport != nullptr); + esp_transport_destroy(socks_transport); + } +} + +TEST_CASE("Requests to Proxy", "[Requests]") +{ + constexpr auto timeout = 50; + esp_transport_socks_proxy_config_t config{ .version = SOCKS4, + .address = "test_socks4_proxy", + .port = 1080}; + + auto test_target = "test_target"sv; + auto target_port = 80; + unique_transport test_parent{esp_transport_init(), esp_transport_destroy}; + REQUIRE(test_parent); + esp_transport_set_func(test_parent.get(), mock_connect, mock_read, mock_write, mock_close, mock_poll_read, mock_poll_write, mock_destroy); + unique_transport socks_transport{esp_transport_socks_proxy_init(test_parent.get(), &config), esp_transport_destroy}; + + mock_destroy_IgnoreAndReturn(ESP_OK); + esp_timer_get_time_IgnoreAndReturn(0); + + SECTION("Failure to connect to proxy") { + mock_connect_ExpectAndReturn(test_parent.get(), config.address, config.port, timeout, -1); + REQUIRE(esp_transport_connect(socks_transport.get(), test_target.data(), target_port, timeout) == -1); + } + + GIVEN("Proxy accepted the connection") { + + mock_connect_ExpectAndReturn(test_parent.get(), config.address, config.port, timeout, 0); + auto expect_addr_info = [](std::string_view test_target, int return_value) { + lwip_getaddrinfo_ExpectAndReturn(test_target.data(), nullptr, nullptr, nullptr, return_value); + lwip_getaddrinfo_IgnoreArg_hints(); + lwip_getaddrinfo_IgnoreArg_res(); + struct addrinfo addr_info = {}; + struct sockaddr_in sockaddr = {}; + sockaddr.sin_addr.s_addr = 0x5a5a5a5a; + lwip_freeaddrinfo_Ignore(); + return std::tuple{addr_info, sockaddr}; + }; + + SECTION("Failure to resolve target") { + auto [addr_info, sockaddr] = expect_addr_info(test_target, EAI_NONAME); + addr_info.ai_addr = reinterpret_cast(&sockaddr); + auto *p_addr_info = &addr_info; + lwip_getaddrinfo_ReturnThruPtr_res(&p_addr_info); + REQUIRE(esp_transport_connect(socks_transport.get(), "test_target", 8080, timeout) == -1); + REQUIRE(errno == SOCKS_RESPONSE_TARGET_NOT_FOUND); + } + + GIVEN("Success on target resolution") { + auto [addr_info, sockaddr] = expect_addr_info(test_target, 0); + addr_info.ai_addr = reinterpret_cast(&sockaddr); + auto *p_addr_info = &addr_info; + lwip_getaddrinfo_ReturnThruPtr_res(&p_addr_info); + auto expected_request = std::array{0x04, 0x01, 0x00, 0x50, 0x5a, 0x5a, 0x5a, 0x5a, 0x00 }; + mock_write_Stub(capture_lambda([&test_parent, expected_request, &timeout](esp_transport_handle_t transport, const char *request_sent, int len, int timeout_ms, [[maybe_unused]]int num_call) { + using namespace Catch::Matchers; + REQUIRE(transport == test_parent.get()); + REQUIRE(len == expected_request.size()); + REQUIRE(timeout_ms == timeout); + REQUIRE(std::equal(request_sent,request_sent+len, std::begin(expected_request), std::end(expected_request))); + return len; + })); + + SECTION("Successful connection request") { + + auto proxy_response = make_response(SOCKS_RESPONSE_SUCCESS); + + + mock_read_ExpectAndReturn(test_parent.get(), proxy_response.data(), proxy_response.size(), timeout, proxy_response.size()); + mock_read_IgnoreArg_buffer(); + mock_read_ReturnArrayThruPtr_buffer(proxy_response.data(), proxy_response.size()); + REQUIRE(esp_transport_connect(socks_transport.get(), test_target.data(), target_port, timeout) == 0); + }; + + SECTION("Proxy rejected request") { + auto proxy_response = make_response(SOCKS_RESPONSE_REQUEST_REJECTED); + + mock_read_ExpectAndReturn(test_parent.get(), proxy_response.data(), proxy_response.size(), timeout, proxy_response.size()); + mock_read_IgnoreArg_buffer(); + mock_read_ReturnArrayThruPtr_buffer(proxy_response.data(), proxy_response.size()); + REQUIRE(esp_transport_connect(socks_transport.get(), test_target.data(), target_port, timeout) == -1); + REQUIRE(errno == SOCKS_RESPONSE_REQUEST_REJECTED); + } + + SECTION("Client not running identification protocol") { + auto proxy_response = make_response(SOCKS_RESPONSE_NOT_RUNNING_IDENTD); + + mock_read_ExpectAndReturn(test_parent.get(), proxy_response.data(), proxy_response.size(), timeout, proxy_response.size()); + mock_read_IgnoreArg_buffer(); + mock_read_ReturnArrayThruPtr_buffer(proxy_response.data(), proxy_response.size()); + REQUIRE(esp_transport_connect(socks_transport.get(), test_target.data(), target_port, timeout) == -1); + REQUIRE(errno == SOCKS_RESPONSE_NOT_RUNNING_IDENTD); + } + + } + + } +} diff --git a/components/tcp_transport/host_test/sdkconfig.defaults b/components/tcp_transport/host_test/sdkconfig.defaults new file mode 100644 index 0000000000..89c65632c9 --- /dev/null +++ b/components/tcp_transport/host_test/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=n diff --git a/components/tcp_transport/include/esp_transport_socks_proxy.h b/components/tcp_transport/include/esp_transport_socks_proxy.h new file mode 100644 index 0000000000..566ed9a6de --- /dev/null +++ b/components/tcp_transport/include/esp_transport_socks_proxy.h @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_transport.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum socks_version_t {SOCKS4 = 4} socks_version_t; + +typedef enum socks_transport_response_t { + // The following values correspond to transport operation + SOCKS_RESPONSE_TARGET_NOT_FOUND = 0xF0, + SOCKS_RESPONSE_PROXY_UNREACHABLE = 0xF1, + SOCKS_TIMEOUT = 0xF2, + // The following values are defined by the SOCKS4 protocol + SOCKS_RESPONSE_SUCCESS = 0x5a, + SOCKS_RESPONSE_REQUEST_REJECTED = 0x5B, + SOCKS_RESPONSE_NOT_RUNNING_IDENTD = 0x5c, + SOCKS_RESPONSE_COULD_NOT_CONFIRM_ID = 0x5d, +} socks_transport_error_t; + +/* + * Socks configuration structure + */ +typedef struct esp_transport_socks_proxy_config_t { + const socks_version_t version; /*!< Socks protocol version.*/ + const char *address;/*!< Proxy address*/ + const int port; /*< Proxy port*/ +} esp_transport_socks_proxy_config_t; + +/** +* @brief Create a proxy transport +* @param parent_handle Handle for the parent transport +* @param config Pointer to the configuration structure to use +* +* @return +* - transport Handler for the created transport. +* - NULL in case of failure +*/ +esp_transport_handle_t esp_transport_socks_proxy_init(esp_transport_handle_t parent_handle, const esp_transport_socks_proxy_config_t *config); + +/** +* @brief Changes the configuration of the proxy +* @param socks_transport Handle for the transport +* @param config Pointer to the configuration structure to use +* +* @return +* - ESP_OK on success +*/ +esp_err_t esp_transport_socks_proxy_set_config(esp_transport_handle_t socks_transport, const esp_transport_socks_proxy_config_t *config); + +#ifdef __cplusplus +} +#endif diff --git a/components/tcp_transport/transport_socks_proxy.c b/components/tcp_transport/transport_socks_proxy.c new file mode 100644 index 0000000000..d18797adeb --- /dev/null +++ b/components/tcp_transport/transport_socks_proxy.c @@ -0,0 +1,224 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include "esp_err.h" +#include "esp_timer.h" +#include "esp_check.h" +#include "esp_transport_socks_proxy.h" +#include "netdb.h" +#include "string.h" + +#include "esp_log.h" +#include "include/esp_transport.h" + +static const char *TAG = "transport_proxy"; +static const uint32_t SOCKS4_FIX_MESSAGE_SIZE = 8; +#define SOCKS4_RESPONSE_SIZE 8 + +#define SOCKS_ERROR_IF(cond, err_code, format, ...) do {\ + ESP_GOTO_ON_FALSE(!(cond), err_code, Error, TAG, format , ##__VA_ARGS__);\ +}while(0) + +typedef enum {CONNECT = 0x01, BIND = 0x02} command_socks_t; + +typedef struct { + union { + char *user_id; + }; +} socks_authentication_data_t; + +typedef struct transport_socks_t { + socks_version_t version; + char *proxy_address; + uint16_t proxy_port; + socks_authentication_data_t authentication; + esp_transport_handle_t parent; +} transport_socks_t; + +typedef struct __attribute((packed)) socks_request { + uint8_t version; + uint8_t command; + uint16_t destination_port; + uint32_t destination_ip; +} socks_request_t; + +typedef struct __attribute((packed)) socks_response { + uint8_t version; + uint8_t code; + uint16_t destination_port; + uint32_t destination_ip; +} socks_response_t; + +static uint32_t get_IP(const char *const host) +{ + struct addrinfo *address_info; + struct addrinfo hints; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + int res = getaddrinfo(host, NULL, &hints, &address_info); + if (res) { + // IP_ADDR_ANY equivalent returned as error here because it doesn't make + // sense to connect to ANY + return 0; + } + uint32_t ip = ((struct sockaddr_in *)address_info->ai_addr)->sin_addr.s_addr; + freeaddrinfo(address_info); + return ip; +} + +static int64_t get_tick(void) +{ + return esp_timer_get_time() / (int64_t)1000; +} + +static int socks_connect(esp_transport_handle_t transport, const char *const host, int port, int timeout_ms) +{ + transport_socks_t *socks_transport = esp_transport_get_context_data(transport); + int64_t initial_tick = get_tick(); + int64_t remaining_time = timeout_ms; + int ret = 0; + uint32_t request_message_len = SOCKS4_FIX_MESSAGE_SIZE + 1; + char *request_message = NULL; + + int proxy_connected = esp_transport_connect(socks_transport->parent, socks_transport->proxy_address, socks_transport->proxy_port, remaining_time); + SOCKS_ERROR_IF(proxy_connected == -1, esp_transport_get_errno(socks_transport->parent), "Error connecting to proxy"); + + if (remaining_time > -1) { + remaining_time = (int64_t)timeout_ms - (get_tick() - initial_tick); + SOCKS_ERROR_IF(remaining_time < 0, SOCKS_TIMEOUT, "Connection Timeout"); + } + + socks_request_t request = {}; + request.version = socks_transport->version; + request.command = CONNECT; + request.destination_port = htons(port); + request.destination_ip = get_IP(host); + SOCKS_ERROR_IF(request.destination_ip == 0, SOCKS_RESPONSE_TARGET_NOT_FOUND, "Failed to resolve target address"); + + if (socks_transport->authentication.user_id) { + request_message_len += strlen(socks_transport->authentication.user_id); + request_message = calloc(request_message_len, sizeof(char)); + if (request_message) { + strcpy(request_message + sizeof(socks_request_t) +1, socks_transport->authentication.user_id); + } + } else { + request_message = calloc(request_message_len, sizeof(char)); + } + SOCKS_ERROR_IF(request_message == NULL, ESP_ERR_NO_MEM, "Failed to allocate request message"); + memcpy(request_message, (char *)&request, sizeof(socks_request_t)); + + SOCKS_ERROR_IF(esp_transport_write(socks_transport->parent, request_message, request_message_len, remaining_time) < 0, esp_transport_get_errno(socks_transport->parent), "Failed to write the request message"); + + if (remaining_time > -1) { + remaining_time = (int64_t)timeout_ms - (get_tick() - initial_tick); + SOCKS_ERROR_IF(remaining_time < 0, SOCKS_TIMEOUT, "Connection Timeout"); + } + + char proxy_response[SOCKS4_RESPONSE_SIZE]; + SOCKS_ERROR_IF(esp_transport_read(socks_transport->parent, proxy_response, SOCKS4_RESPONSE_SIZE, remaining_time) < 0, esp_transport_get_errno(socks_transport->parent), "Failed to get a response"); + + socks_response_t *response = (socks_response_t *)proxy_response; + SOCKS_ERROR_IF(response->code != SOCKS_RESPONSE_SUCCESS, response->code, "Request Rejected with : %02x", response->code); + free(request_message); + return 0; +Error: + free(request_message); + errno = ret; + return -1; +} + +static int socks_close(esp_transport_handle_t transport) +{ + transport_socks_t *socks_transport = esp_transport_get_context_data(transport); + return esp_transport_close(socks_transport->parent); + +} + +static int socks_write(esp_transport_handle_t transport, const char *buffer, int len, int timeout_ms) +{ + transport_socks_t *socks_transport = esp_transport_get_context_data(transport); + return esp_transport_write(socks_transport->parent, buffer, len, timeout_ms); +} + +static int socks_read(esp_transport_handle_t transport, char *buffer, int len, int timeout_ms) +{ + transport_socks_t *socks_transport = esp_transport_get_context_data(transport); + return esp_transport_read(socks_transport->parent, buffer, len, timeout_ms); +} + +static int socks_poll_read(esp_transport_handle_t transport, int timeout_ms) +{ + transport_socks_t *socks_transport = esp_transport_get_context_data(transport); + return esp_transport_poll_read(socks_transport->parent, timeout_ms); +} + +static int socks_poll_write(esp_transport_handle_t transport, int timeout_ms) +{ + transport_socks_t *socks_transport = esp_transport_get_context_data(transport); + return esp_transport_poll_write(socks_transport->parent, timeout_ms); +} + +static esp_err_t socks_destroy(esp_transport_handle_t transport) +{ + if (transport == NULL) { + return ESP_OK; + } + transport_socks_t *socks_transport = esp_transport_get_context_data(transport); + if (socks_transport == NULL) { + return ESP_FAIL; + } + free(socks_transport->proxy_address); + free(socks_transport); + + return ESP_OK; + +} +static esp_err_t check_parameters(esp_transport_handle_t parent_handle, const esp_transport_socks_proxy_config_t *config) +{ + if (parent_handle == NULL) { + ESP_LOGE(TAG, "Parent transport is invalid"); + return ESP_FAIL; + } + + if (config == NULL || config->address == NULL) { + ESP_LOGE(TAG, "Invalid Configuration"); + return ESP_FAIL; + } + return ESP_OK; +} + +esp_transport_handle_t esp_transport_socks_proxy_init(esp_transport_handle_t parent_handle, const esp_transport_socks_proxy_config_t *config) +{ + if (check_parameters(parent_handle, config) == ESP_FAIL) { + return NULL; + }; + + int ret = 0; + + esp_transport_handle_t transport = esp_transport_init(); + SOCKS_ERROR_IF(transport == NULL, ESP_ERR_NO_MEM, "Failed to allocate transport"); + + transport_socks_t *socks_context = calloc(1, sizeof(transport_socks_t)); + SOCKS_ERROR_IF(socks_context == NULL, ESP_ERR_NO_MEM, "Failed to allocate transport context"); + esp_transport_set_context_data(transport, socks_context); + esp_transport_set_func(transport, socks_connect, socks_read, socks_write, socks_close, socks_poll_read, socks_poll_write, socks_destroy); + + socks_context->parent = parent_handle; + socks_context->proxy_address = strdup(config->address); + SOCKS_ERROR_IF(socks_context->proxy_address == NULL, ESP_ERR_NO_MEM, "Failed to copy proxy address"); + socks_context->proxy_port = config->port; + socks_context->version = config->version; + + + return transport; +Error: + esp_transport_destroy(transport); + errno = ret; + return NULL; +} diff --git a/components/tcp_transport/transport_ssl.c b/components/tcp_transport/transport_ssl.c index b2b41881f6..23df5231a9 100644 --- a/components/tcp_transport/transport_ssl.c +++ b/components/tcp_transport/transport_ssl.c @@ -6,6 +6,7 @@ #include #include +#include #include #include "esp_tls.h" @@ -14,7 +15,6 @@ #include "esp_transport.h" #include "esp_transport_ssl.h" #include "esp_transport_internal.h" -#include "errno.h" #define INVALID_SOCKET (-1) diff --git a/components/tcp_transport/transport_ws.c b/components/tcp_transport/transport_ws.c index 9060a5dfa7..5d64c7d3c3 100644 --- a/components/tcp_transport/transport_ws.c +++ b/components/tcp_transport/transport_ws.c @@ -10,6 +10,7 @@ #include #include #include +#include #include "esp_log.h" #include "esp_transport.h" #include "esp_transport_tcp.h" diff --git a/examples/protocols/sockets/tcp_transport_client/README.md b/examples/protocols/sockets/tcp_transport_client/README.md index 202770de2d..5582a57763 100644 --- a/examples/protocols/sockets/tcp_transport_client/README.md +++ b/examples/protocols/sockets/tcp_transport_client/README.md @@ -6,21 +6,33 @@ (See the README.md file in the upper level 'examples' directory for more information about examples.) -The application creates a TCP transport connection and tries to connect to the server with predefined IP address and port number. When a connection is successfully established, the application sends message and waits for the answer. After the server's reply, application prints received reply as ASCII text, waits for 2 seconds and sends another message. +The application creates a TCP connection using `tcp_transport` component and tries to connect to the server with predefined IP address and port number. When a connection is successfully established, the application sends message and waits for the answer. After the server's reply, application prints received reply as ASCII text, waits for 2 seconds and sends another message. + +It's possible to enable SOCKS proxy support to make the connection to go through a proxy server. ## How to use example -In order to create TCP server that communicates with TCP TRANSPORT Client example, choose one of the following options. +In order to create TCP server that communicates with TCP TRANSPORT Client example, it's possible to choose some host-side tool or one of the scripts available in the example parent directory. There are many host-side tools which can be used to interact with the UDP/TCP server/client. One command line tool is [netcat](http://netcat.sourceforge.net) which can send and receive many kinds of packets. -Note: please replace `192.168.0.167 3333` with desired IPV4/IPV6 address (displayed in monitor console) and port number in the following command. -In addition to those tools, simple Python scripts can be found under sockets/scripts directory. Every script is designed to interact with one of the examples. +In addition to those tools, simple Python scripts can be found under the example parent directory. Every script is designed to interact with one of the examples. ### TCP server using netcat ``` -nc -l 192.168.0.167 3333 +nc -l 0.0.0.0 -p 3333 +``` + +In this scenario netcat doesn't send any data back. If the python scripts are used to test the monitor shows the message sent from the server. + +### Using a proxy server + +A simple way of testing the usage of the proxy client on Linux systems is +to use ssh as a proxy: + +``` +ssh -N -v -D 0.0.0.0:1080 localhost ``` ## Hardware Required @@ -35,12 +47,18 @@ idf.py menuconfig Set following parameters under Example Configuration Options: -* Set `IPV4 Address` that represents remote host the example will connect to. +* Set `Target server Address` that represents remote host the example will connect to. -* Set `Port` number that represents remote port the example will connect to. +* Set `Target Port` number that represents remote port the example will connect to. Configure Wi-Fi or Ethernet under "Example Connection Configuration" menu. See "Establishing Wi-Fi or Ethernet Connection" section in [examples/protocols/README.md](../../README.md) for more details. +### Enabling proxy client + +Set `Enable proxy client` +Set `Proxy server address` that represents the proxy server the example will connect to. +Set `Proxy server port` number that represents the proxy server port the example will connect to. + ## Build and Flash Build the project and flash it to the board, then run monitor tool to view serial output: diff --git a/examples/protocols/sockets/tcp_transport_client/main/Kconfig.projbuild b/examples/protocols/sockets/tcp_transport_client/main/Kconfig.projbuild index 3bee48e876..1b6c2ef80b 100644 --- a/examples/protocols/sockets/tcp_transport_client/main/Kconfig.projbuild +++ b/examples/protocols/sockets/tcp_transport_client/main/Kconfig.projbuild @@ -1,16 +1,37 @@ menu "Example Configuration" - config EXAMPLE_IPV4_ADDR - string "IPV4 Address" + config EXAMPLE_TARGET_ADDR + string "Target server Address" default "192.168.0.165" help The example will connect to this IPV4 address. - config EXAMPLE_PORT - int "Port" + config EXAMPLE_TARGET_PORT + int "Target server Port" range 0 65535 - default 3333 + default 8080 help The remote port to which the client example will connect to. + config EXAMPLE_ENABLE_PROXY + bool "Enable Proxy client" + default n + help + Use a SOCKS proxy to connect + + config EXAMPLE_PROXY_ADDR + string "Proxy server Address" + depends on ENABLE_PROXY + default "192.168.0.1" + help + The example will connect to this proxy address to request connection to target. + + config EXAMPLE_PROXY_PORT + int "Proxy server Port" + depends on ENABLE_PROXY + range 0 65535 + default 1080 + help + The proxy port to which the client example will request connection. + endmenu diff --git a/examples/protocols/sockets/tcp_transport_client/main/tcp_transport_client.c b/examples/protocols/sockets/tcp_transport_client/main/tcp_transport_client.c index c6b10e0b13..f3ed2d89cb 100644 --- a/examples/protocols/sockets/tcp_transport_client/main/tcp_transport_client.c +++ b/examples/protocols/sockets/tcp_transport_client/main/tcp_transport_client.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -16,26 +16,44 @@ #include "protocol_examples_common.h" #include "esp_transport.h" #include "esp_transport_tcp.h" +#include "esp_transport_socks_proxy.h" +#define TARGET_ADDR CONFIG_EXAMPLE_TARGET_ADDR +#define TARGET_PORT CONFIG_EXAMPLE_TARGET_PORT -#define HOST_IP_ADDR CONFIG_EXAMPLE_IPV4_ADDR - -#define PORT CONFIG_EXAMPLE_PORT +#ifdef CONFIG_EXAMPLE_ENABLE_PROXY +#define PROXY_ADDR CONFIG_EXAMPLE_PROXY_ADDR +#define PROXY_PORT CONFIG_EXAMPLE_PROXY_PORT +#endif static const char *TAG = "tcp_transport_client"; -static const char *payload = "Message from ESP32"; +static const char *payload = "Message from ESP32\n"; static void tcp_transport_client_task(void *pvParameters) { char rx_buffer[128]; - char host_ip[] = HOST_IP_ADDR; - esp_transport_handle_t tcp = esp_transport_tcp_init(); - while (1) { - if (tcp == NULL) { - ESP_LOGE(TAG, "Error occurred during esp_transport_tcp_init()"); + char host_ip[] = TARGET_ADDR; + esp_transport_handle_t transport = esp_transport_tcp_init(); + + #ifdef CONFIG_EXAMPLE_ENABLE_PROXY + /* + * The socks transport is a composed transport, so we save the previously created + * handler to use it as a parent transport, so our transport is now socks over tcp. + * We could have used the ssl transport as parent and we can use a socks transport as a + * parent to websocket transport. + * + */ + esp_transport_handle_t parent = transport; + esp_transport_socks_proxy_config_t proxy_config = {.port = PROXY_PORT, .address = PROXY_ADDR, .version = SOCKS4}; + transport = esp_transport_socks_proxy_init(parent, &proxy_config); + #endif + + while (1) { + if (transport == NULL) { + ESP_LOGE(TAG, "Error occurred during esp_transport_proxy_init()"); break; } - int err = esp_transport_connect(tcp, HOST_IP_ADDR, PORT, -1); + int err = esp_transport_connect(transport, TARGET_ADDR, TARGET_PORT, -1); if (err != 0) { ESP_LOGE(TAG, "Client unable to connect: errno %d", errno); break; @@ -43,34 +61,43 @@ static void tcp_transport_client_task(void *pvParameters) ESP_LOGI(TAG, "Successfully connected"); while (1) { - int bytes_written = esp_transport_write(tcp, payload, strlen(payload), 0); + int bytes_written = esp_transport_write(transport, payload, strlen(payload), 0); if (bytes_written < 0) { ESP_LOGE(TAG, "Error occurred during sending: esp_transport_write() returned %d, errno %d", bytes_written, errno); break; } - int len = esp_transport_read(tcp, rx_buffer, sizeof(rx_buffer) - 1, 0); + int len = esp_transport_read(transport, rx_buffer, sizeof(rx_buffer) - 1, 0); // Error occurred during receiving if (len < 0) { ESP_LOGE(TAG, "recv failed: esp_transport_read() returned %d, errno %d", len, errno); break; } // Data received - rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string - ESP_LOGI(TAG, "Received %d bytes from %s:", len, host_ip); - ESP_LOGI(TAG, "%s", rx_buffer); + rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string + ESP_LOGI(TAG, "Received %d bytes from %s:", len, host_ip); + ESP_LOGI(TAG, "Received data : %s", rx_buffer); vTaskDelay(2000 / portTICK_PERIOD_MS); } - ESP_LOGE(TAG, "Shutting down TCP and restarting..."); - esp_transport_close(tcp); + ESP_LOGE(TAG, "Shutting down transport and restarting..."); + esp_transport_close(transport); } - esp_transport_destroy(tcp); - vTaskDelete(NULL); + esp_transport_destroy(transport); + + #ifdef CONFIG_EXAMPLE_ENABLE_PROXY + esp_transport_destroy(parent); + #endif + + vTaskDelete(NULL); } void app_main(void) { + esp_log_level_set("transport", ESP_LOG_VERBOSE); + esp_log_level_set("transport_base", ESP_LOG_VERBOSE); + esp_log_level_set("transport_proxy", ESP_LOG_VERBOSE); + ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); diff --git a/tools/mocks/esp-tls/CMakeLists.txt b/tools/mocks/esp-tls/CMakeLists.txt index ffe9866874..8e0406e295 100644 --- a/tools/mocks/esp-tls/CMakeLists.txt +++ b/tools/mocks/esp-tls/CMakeLists.txt @@ -8,5 +8,6 @@ idf_component_mock(INCLUDE_DIRS "${original_esp_tls_dir}" "${original_esp_tls_dir}/esp-tls-crypto" MOCK_HEADER_FILES ${original_esp_tls_dir}/esp_tls.h ${original_esp_tls_dir}/esp-tls-crypto/esp_tls_crypto.h + REQUIRES mbedtls ) target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-array-parameter) diff --git a/tools/mocks/esp-tls/Kconfig b/tools/mocks/esp-tls/Kconfig new file mode 100644 index 0000000000..bcdc837f4a --- /dev/null +++ b/tools/mocks/esp-tls/Kconfig @@ -0,0 +1,5 @@ +menu "ESP-TLS" + config ESP_TLS_USING_MBEDTLS + bool "mbedTLS" + default y +endmenu diff --git a/tools/mocks/lwip/CMakeLists.txt b/tools/mocks/lwip/CMakeLists.txt index ea9f075886..3f79a4d287 100644 --- a/tools/mocks/lwip/CMakeLists.txt +++ b/tools/mocks/lwip/CMakeLists.txt @@ -4,7 +4,12 @@ message(STATUS "building LWIP MOCKS (only netdb)") idf_component_get_property(original_lwip_dir lwip COMPONENT_OVERRIDEN_DIR) -idf_component_mock(INCLUDE_DIRS "${original_lwip_dir}/port" +idf_component_mock(INCLUDE_DIRS "${original_lwip_dir}/include" "${original_lwip_dir}/include/lwip" "${original_lwip_dir}/lwip/src/include" - MOCK_HEADER_FILES ${original_lwip_dir}/lwip/src/include/lwip/netdb.h ) + "${original_lwip_dir}/lwip/src/include/compat/posix" + "${original_lwip_dir}/port/include" + "${original_lwip_dir}/port/linux/include" + "${original_lwip_dir}/port/freertos/include" + "${original_lwip_dir}/port/linux/include/arch" + MOCK_HEADER_FILES ${original_lwip_dir}/lwip/src/include/lwip/netdb.h) diff --git a/tools/mocks/lwip/Kconfig b/tools/mocks/lwip/Kconfig new file mode 100644 index 0000000000..1d302da4b5 --- /dev/null +++ b/tools/mocks/lwip/Kconfig @@ -0,0 +1,16 @@ +menu "LWIP" + config LWIP_TCP_OVERSIZE_MSS + bool "MSS" + default y + + config LWIP_DHCP_COARSE_TIMER_SECS + int "DHCP coarse timer interval(s)" + default 1 + range 1 10 + + config LWIP_IPV4 + bool "Enable IPv4" + default y + help + Enable IPv4 stack. If you want to use IPv6 only TCP/IP stack, disable this. +endmenu