diff --git a/CMakeLists.txt b/CMakeLists.txt index 191d1b6ed..4736d9e0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,10 +161,16 @@ file(GLOB IO_HEADERS ${AWS_IO_PRIV_HEADERS} ) +file(GLOB AWS_IO_SOCKS5_SRC + "source/socks5.c" + "source/socks5_channel_handler.c" + ) + file(GLOB IO_SRC ${AWS_IO_SRC} ${AWS_IO_OS_SRC} ${AWS_IO_TLS_SRC} + ${AWS_IO_SOCKS5_SRC} ) add_library(${PROJECT_NAME} ${LIBTYPE} ${IO_HEADERS} ${IO_SRC}) diff --git a/include/aws/io/io.h b/include/aws/io/io.h index 9d958939b..681c69a6b 100644 --- a/include/aws/io/io.h +++ b/include/aws/io/io.h @@ -257,6 +257,20 @@ enum aws_io_errors { AWS_IO_TLS_ERROR_READ_FAILURE, AWS_ERROR_PEM_MALFORMED, + + AWS_IO_SOCKS5_PROXY_ERROR_INIT, + AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_AUTH_METHOD, + AWS_IO_SOCKS5_PROXY_ERROR_AUTH_FAILED, + AWS_IO_SOCKS5_PROXY_ERROR_CONNECTION_FAILED, + AWS_IO_SOCKS5_PROXY_ERROR_REQUEST_FAILED, + AWS_IO_SOCKS5_PROXY_ERROR_MALFORMED_RESPONSE, + AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_ADDRESS_TYPE, + AWS_IO_SOCKS5_PROXY_ERROR_BAD_ADDRESS, + AWS_IO_SOCKS5_PROXY_ERROR_BAD_HANDSHAKE, + AWS_IO_SOCKS5_PROXY_ERROR_REJECTED, + AWS_IO_SOCKS5_PROXY_ERROR_GENERAL_FAILURE, + AWS_IO_SOCKS5_PROXY_ERROR_TTL_EXPIRED, + AWS_IO_SOCKS5_PROXY_ERROR_COMMAND_NOT_SUPPORTED, AWS_IO_SOCKET_MISSING_EVENT_LOOP, AWS_IO_TLS_UNKNOWN_ROOT_CERTIFICATE, diff --git a/include/aws/io/logging.h b/include/aws/io/logging.h index a3bbee2fa..665627af4 100644 --- a/include/aws/io/logging.h +++ b/include/aws/io/logging.h @@ -33,6 +33,7 @@ enum aws_io_log_subject { AWS_LS_IO_STANDARD_RETRY_STRATEGY, AWS_LS_IO_PKCS11, AWS_LS_IO_PEM, + AWS_LS_IO_SOCKS5, AWS_IO_LS_LAST = AWS_LOG_SUBJECT_END_RANGE(AWS_C_IO_PACKAGE_ID) }; AWS_POP_SANE_WARNING_LEVEL diff --git a/include/aws/io/socks5.h b/include/aws/io/socks5.h new file mode 100644 index 000000000..f895de021 --- /dev/null +++ b/include/aws/io/socks5.h @@ -0,0 +1,353 @@ +#ifndef AWS_IO_SOCKS5_H +#define AWS_IO_SOCKS5_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +/** + * SOCKS5 Protocol implementation for AWS CRT. + * + * This module provides functionality for connecting to TCP servers through a SOCKS5 proxy. + * It implements the client-side of the SOCKS5 protocol as defined in: + * + * - RFC 1928: "SOCKS Protocol Version 5" + * - RFC 1929: "Username/Password Authentication for SOCKS V5" + * + * The implementation supports: + * - No authentication and username/password authentication methods + * - IPv4, IPv6, and domain name address types + * - The CONNECT command + * + * Usage flow: + * 1. Initialize proxy options with aws_socks5_proxy_options_init() + * 2. Initialize SOCKS5 context with aws_socks5_context_init() + * 3. Perform the protocol handshake sequence: + * - Write greeting → Read greeting response + * - Write auth request → Read auth response (if required) + * - Write connect request → Read connect response + * 4. After success, the connection can be used normally for application protocols + */ + +/* SOCKS5 Protocol Constants */ +#define AWS_SOCKS5_VERSION 0x05 +#define AWS_SOCKS5_RESERVED 0x00 +#define AWS_SOCKS5_AUTH_VERSION 0x01 + +/* SOCKS5 Message Sizes */ +#define AWS_SOCKS5_GREETING_MIN_SIZE 3 +#define AWS_SOCKS5_GREETING_RESP_SIZE 2 +#define AWS_SOCKS5_AUTH_REQ_MIN_SIZE 5 /* Version(1) + ULen(1) + UName(1+) + PLen(1) + Pass(1+) */ +#define AWS_SOCKS5_AUTH_RESP_SIZE 2 +#define AWS_SOCKS5_CONN_REQ_MIN_SIZE 6 /* Version(1) + CMD(1) + RSV(1) + ATYP(1) + ADDR(1+) + PORT(2) */ +#define AWS_SOCKS5_CONN_RESP_MIN_SIZE 6 /* Version(1) + Status(1) + RSV(1) + ATYP(1) + ADDR(1+) + PORT(2) */ + +/* SOCKS5 Address Type */ +enum aws_socks5_address_type { + AWS_SOCKS5_ATYP_IPV4 = 0x01, + AWS_SOCKS5_ATYP_DOMAIN = 0x03, + AWS_SOCKS5_ATYP_IPV6 = 0x04, +}; + +/* SOCKS5 Authentication Methods */ +enum aws_socks5_auth_method { + AWS_SOCKS5_AUTH_NONE = 0x00, + AWS_SOCKS5_AUTH_GSSAPI = 0x01, + AWS_SOCKS5_AUTH_USERNAME_PASSWORD = 0x02, + AWS_SOCKS5_AUTH_NO_ACCEPTABLE = 0xFF, +}; + +/* SOCKS5 Commands */ +enum aws_socks5_command { + AWS_SOCKS5_COMMAND_CONNECT = 0x01, + AWS_SOCKS5_COMMAND_BIND = 0x02, + AWS_SOCKS5_COMMAND_UDP_ASSOCIATE = 0x03, +}; + +/* SOCKS5 Reply Status Codes */ +enum aws_socks5_response_status { + AWS_SOCKS5_STATUS_SUCCESS = 0x00, + AWS_SOCKS5_STATUS_GENERAL_FAILURE = 0x01, + AWS_SOCKS5_STATUS_CONNECTION_NOT_ALLOWED = 0x02, + AWS_SOCKS5_STATUS_NETWORK_UNREACHABLE = 0x03, + AWS_SOCKS5_STATUS_HOST_UNREACHABLE = 0x04, + AWS_SOCKS5_STATUS_CONNECTION_REFUSED = 0x05, + AWS_SOCKS5_STATUS_TTL_EXPIRED = 0x06, + AWS_SOCKS5_STATUS_COMMAND_NOT_SUPPORTED = 0x07, + AWS_SOCKS5_STATUS_ADDRESS_TYPE_NOT_SUPPORTED = 0x08, +}; + +/* SOCKS5 Protocol State */ +enum aws_socks5_state { + AWS_SOCKS5_STATE_INIT, + AWS_SOCKS5_STATE_GREETING_SENT, + AWS_SOCKS5_STATE_GREETING_RECEIVED, + AWS_SOCKS5_STATE_AUTH_STARTED, + AWS_SOCKS5_STATE_AUTH_COMPLETED, + AWS_SOCKS5_STATE_REQUEST_SENT, + AWS_SOCKS5_STATE_RESPONSE_RECEIVED, + AWS_SOCKS5_STATE_CONNECTED, + AWS_SOCKS5_STATE_ERROR, +}; + +/* SOCKS5 Proxy Options */ +enum aws_socks5_host_resolution_mode { + AWS_SOCKS5_HOST_RESOLUTION_PROXY = 0, + AWS_SOCKS5_HOST_RESOLUTION_CLIENT = 1, +}; + +struct aws_socks5_proxy_options { + /* Proxy server host and port */ + struct aws_string *host; + uint16_t port; + + /* Authentication credentials (optional) */ + struct aws_string *username; + struct aws_string *password; + + /* Configuration options */ + uint32_t connection_timeout_ms; + enum aws_socks5_host_resolution_mode host_resolution_mode; +}; + + +/* SOCKS5 Context - internal state for protocol handling */ +struct aws_socks5_context { + struct aws_allocator *allocator; + enum aws_socks5_state state; + struct aws_array_list auth_methods; /* List of enum aws_socks5_auth_method */ + enum aws_socks5_auth_method selected_auth; + + /* Connection information */ + struct aws_socks5_proxy_options options; + struct aws_string *endpoint_host; + uint16_t endpoint_port; + enum aws_socks5_address_type endpoint_address_type; + + /* Buffer management */ + struct aws_byte_buf send_buf; + struct aws_byte_buf recv_buf; +}; + +AWS_EXTERN_C_BEGIN + +/** + * Initialize SOCKS5 proxy options with defaults. + * + * @param options The options structure to initialize + * @param allocator The allocator to use for internal memory allocation + * @param host The proxy server hostname or IP address as a byte cursor + * @param port The proxy server port + * + * @return AWS_OP_SUCCESS if successful, AWS_OP_ERR otherwise with error code set + */ +AWS_IO_API int aws_socks5_proxy_options_init( + struct aws_socks5_proxy_options *options, + struct aws_allocator *allocator, + struct aws_byte_cursor host, + uint16_t port); + +/** + * Initialize SOCKS5 proxy options with default values. + * + * @param options The options structure to initialize + * + * @return AWS_OP_SUCCESS if successful, AWS_OP_ERR otherwise with error code set + */ +AWS_IO_API int aws_socks5_proxy_options_init_default( + struct aws_socks5_proxy_options *options); + +/** + * Deep copy SOCKS5 proxy options from source to destination. + * Destination must be already zero-initialized. + * + * @param dest The destination options structure (must be zero-initialized) + * @param src The source options structure to copy from + * + * @return AWS_OP_SUCCESS on success, AWS_OP_ERR on failure + */ +AWS_IO_API int aws_socks5_proxy_options_copy( + struct aws_socks5_proxy_options *dest, + const struct aws_socks5_proxy_options *src); + +/** + * Clean up SOCKS5 proxy options and free all internally allocated memory. + * + * @param options The SOCKS5 proxy options to clean up + */ +AWS_IO_API void aws_socks5_proxy_options_clean_up(struct aws_socks5_proxy_options *options); + +/** + * Set authentication credentials for SOCKS5 proxy. If set, the SOCKS5 client will + * attempt to authenticate using username/password authentication method. + * + * @param options The SOCKS5 proxy options to update + * @param username The username as a byte cursor + * @param password The password as a byte cursor + * + * @return AWS_OP_SUCCESS if successful, AWS_OP_ERR otherwise with error code set + * + * @note Both username and password must have length > 0 and <= 255 bytes as per RFC 1929 + */ +AWS_IO_API int aws_socks5_proxy_options_set_auth( + struct aws_socks5_proxy_options *options, + struct aws_allocator *allocator, + struct aws_byte_cursor username, + struct aws_byte_cursor password); + +/** + * Set the host resolution mode for the SOCKS5 proxy. + * + * @param options The SOCKS5 proxy options to update + * @param mode The host resolution mode to set + */ +AWS_IO_API void aws_socks5_proxy_options_set_host_resolution_mode( + struct aws_socks5_proxy_options *options, + enum aws_socks5_host_resolution_mode mode); + +/** + * Get the host resolution mode for the SOCKS5 proxy. + * + * @param options The SOCKS5 proxy options to query + * @return The host resolution mode + */ +AWS_IO_API enum aws_socks5_host_resolution_mode aws_socks5_proxy_options_get_host_resolution_mode( + const struct aws_socks5_proxy_options *options); + +/** + * Helper to infer the appropriate SOCKS5 address type for a given host string. If the host is an IPv4 or IPv6 literal, + * the corresponding address type will be returned even if AWS_SOCKS5_ATYP_DOMAIN was requested. + */ +AWS_IO_API enum aws_socks5_address_type aws_socks5_infer_address_type( + struct aws_byte_cursor host, + enum aws_socks5_address_type requested_type); + +/** + * Initialize a SOCKS5 protocol context for establishing a connection through a SOCKS5 proxy. + * The context manages the state and buffers needed for the SOCKS5 protocol handshake. + * + * @param context The context structure to initialize + * @param allocator The allocator to use for internal memory allocation + * @param options Configuration options for the SOCKS5 proxy connection + * @param target_host Destination host the proxy should reach (byte cursor) + * @param target_port Destination port the proxy should reach + * @param address_type Address type hint (DOMAIN will trigger automatic IPv4/IPv6 detection) + * + * @return AWS_OP_SUCCESS if successful, AWS_OP_ERR otherwise with error code set + */ +AWS_IO_API int aws_socks5_context_init( + struct aws_socks5_context *context, + struct aws_allocator *allocator, + const struct aws_socks5_proxy_options *options, + struct aws_byte_cursor target_host, + uint16_t target_port, + enum aws_socks5_address_type address_type); + +/** + * Clean up a SOCKS5 context and free all internally allocated memory. + * + * @param context The SOCKS5 context to clean up + */ +AWS_IO_API void aws_socks5_context_clean_up(struct aws_socks5_context *context); + +/** + * Format the initial SOCKS5 greeting message into the provided buffer. + * This message contains the list of supported authentication methods and + * is the first message sent to a SOCKS5 proxy server. + * + * @param context The SOCKS5 context containing connection information + * @param buffer The buffer to write the greeting message into (will be resized if needed) + * + * @return AWS_OP_SUCCESS if successful, AWS_OP_ERR otherwise with error code set + */ +AWS_IO_API int aws_socks5_write_greeting( + struct aws_socks5_context *context, + struct aws_byte_buf *buffer); + +/** + * Process the SOCKS5 greeting response from the server. + * The server selects an authentication method or rejects the connection. + * + * @param context The SOCKS5 context to update with server's selected auth method + * @param data The received data from the server to process + * + * @return AWS_OP_SUCCESS if successful, AWS_OP_ERR otherwise with error code set + * + * @note Updates context->state to AWS_SOCKS5_STATE_GREETING_RECEIVED on success + * @note Updates context->selected_auth with the server's chosen authentication method + */ +AWS_IO_API int aws_socks5_read_greeting_response( + struct aws_socks5_context *context, + struct aws_byte_cursor *data); + +/** + * Format the username/password authentication request into the provided buffer. + * This message is sent after the server has selected username/password authentication. + * + * @param context The SOCKS5 context containing authentication credentials + * @param buffer The buffer to write the authentication request into + * + * @return AWS_OP_SUCCESS if successful, AWS_OP_ERR otherwise with error code set + * + * @note If the selected authentication method is AWS_SOCKS5_AUTH_NONE, this function + * will update the context state without writing to the buffer + */ +AWS_IO_API int aws_socks5_write_auth_request( + struct aws_socks5_context *context, + struct aws_byte_buf *buffer); + +/** + * Process the SOCKS5 authentication response from the server. + * This verifies if authentication was successful. + * + * @param context The SOCKS5 context to update based on authentication result + * @param data The received data from the server to process + * + * @return AWS_OP_SUCCESS if authentication succeeded, AWS_OP_ERR otherwise with error code set + * + * @note Updates context->state to AWS_SOCKS5_STATE_AUTH_COMPLETED on success + * @note If the selected authentication method is AWS_SOCKS5_AUTH_NONE, this function + * will update the context state without processing the data + */ +AWS_IO_API int aws_socks5_read_auth_response( + struct aws_socks5_context *context, + struct aws_byte_cursor *data); + +/** + * Format the SOCKS5 connection request into the provided buffer. + * This message requests the proxy to establish a connection to the target host. + * + * @param context The SOCKS5 context containing target host information + * @param buffer The buffer to write the connect request into + * + * @return AWS_OP_SUCCESS if successful, AWS_OP_ERR otherwise with error code set + * + * @note Updates context->state to AWS_SOCKS5_STATE_REQUEST_SENT on success + * @note Currently only supports the CONNECT command (not BIND or UDP ASSOCIATE) + */ +AWS_IO_API int aws_socks5_write_connect_request( + struct aws_socks5_context *context, + struct aws_byte_buf *buffer); + +/** + * Process the SOCKS5 connection response from the server. + * This verifies if the connection to the target host was successful. + * + * @param context The SOCKS5 context to update based on connection result + * @param data The received data from the server to process + * + * @return AWS_OP_SUCCESS if connection succeeded, AWS_OP_ERR otherwise with error code set + * + * @note Updates context->state to AWS_SOCKS5_STATE_CONNECTED on success + * @note On error, the specific SOCKS5 error code will be mapped to an appropriate AWS error code + */ +AWS_IO_API int aws_socks5_read_connect_response( + struct aws_socks5_context *context, + struct aws_byte_cursor *data); + +AWS_EXTERN_C_END + +#endif /* AWS_IO_SOCKS5_H */ diff --git a/include/aws/io/socks5_channel_handler.h b/include/aws/io/socks5_channel_handler.h new file mode 100644 index 000000000..67f8804a6 --- /dev/null +++ b/include/aws/io/socks5_channel_handler.h @@ -0,0 +1,169 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_IO_SOCKS5_CHANNEL_HANDLER_H +#define AWS_IO_SOCKS5_CHANNEL_HANDLER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* SOCKS5 proxy connection states */ +enum aws_socks5_proxy_connection_state { + AWS_SPCS_NONE = 0, + AWS_SPCS_SOCKET_CONNECT, + AWS_SPCS_SOCKS5_NEGOTIATION, + AWS_SPCS_SUCCESS, + AWS_SPCS_FAILURE, +}; + +/** + * Context struct for SOCKS5 proxy connections, used as user_data throughout the SOCKS5 bootstrap/setup chain. + * + * This structure coordinates the connection flow through a SOCKS5 proxy and optional TLS setup, + * maintaining state through the entire connection lifecycle and ensuring proper callback chaining. + * The structure serves as a bridge between the original connection request and the SOCKS5-specific + * connection process, preserving critical context needed for proper callback invocation. + * + * During a SOCKS5 proxy connection: + * 1. This structure is initialized with connection parameters and callbacks + * 2. The bootstrap process installs a SOCKS5 channel handler that handles the proxy protocol + * 3. After successful SOCKS5 handshake, TLS can be established if requested + * 4. Original callbacks are invoked at appropriate points to maintain the expected behavior + */ +struct aws_socks5_bootstrap { + /** Memory allocator used for all allocations related to this bootstrap */ + struct aws_allocator *allocator; + + /** SOCKS5 proxy configuration */ + struct aws_socks5_proxy_options *socks5_proxy_options; + + /** User data to pass to callbacks; preserved from the original connection request */ + void *user_data; + + /** + * Callback function called when the SOCKS5 setup has successfully completed. + */ + aws_client_bootstrap_on_channel_event_fn *on_socks5_setup_completed; + + /** Original callback function to invoke when the channel is successfully established */ + aws_client_bootstrap_on_channel_event_fn *setup_callback; + + /** Original callback function to invoke when the channel is being shut down */ + aws_client_bootstrap_on_channel_event_fn *shutdown_callback; + + /** TLS connection options to apply after SOCKS5 handshake completes (if TLS is requested) */ + struct aws_tls_connection_options *tls_options; + + /** Flag indicating whether to establish TLS after SOCKS5 handshake completes */ + bool use_tls; + + /** Reference to the client bootstrap used for the connection */ + struct aws_client_bootstrap *bootstrap; + + /** Original TLS negotiation callback to invoke after TLS handshake completes */ + aws_tls_on_negotiation_result_fn *original_on_negotiation_result; + + /** User data to pass to the original TLS negotiation callback */ + void *original_tls_user_data; + + /** Destination endpoint resolved from the original connection request */ + struct aws_string *endpoint_host; + struct aws_string *original_endpoint_host; + uint16_t endpoint_port; + enum aws_socks5_address_type endpoint_address_type; + enum aws_socks5_host_resolution_mode host_resolution_mode; + bool endpoint_ready; + bool resolution_in_progress; + int resolution_error_code; + struct aws_channel *pending_channel; + struct aws_channel_task resolution_success_task; + struct aws_channel_task resolution_failure_task; + bool resolution_task_scheduled; + bool resolution_failure_task_scheduled; + /** Set when shutdown is requested while resolution is still running so the bootstrap can be destroyed safely after the callback completes */ + bool cleanup_pending; + struct aws_host_resolution_config host_resolution_config; + bool has_host_resolution_override; + struct aws_mutex lock; +}; + +/** + * System vtable used to decouple production behavior from tests. + * Tests may override these functions to observe or modify bootstrap behavior. + */ +struct aws_socks5_system_vtable { + int (*aws_client_bootstrap_new_socket_channel)(struct aws_socket_channel_bootstrap_options *options); +}; + + +AWS_EXTERN_C_BEGIN + +/** + * Creates a SOCKS5 channel handler that will establish a connection through a SOCKS5 proxy + * to the target host and port specified in the options. + * + * This handler manages the initial SOCKS5 handshake and authentication, and then becomes transparent + * once the connection is established. + * + * For a TLS connection through a SOCKS5 proxy, this handler should be installed before the TLS handler. + */ +AWS_IO_API struct aws_channel_handler *aws_socks5_channel_handler_new( + struct aws_allocator *allocator, + const struct aws_socks5_proxy_options *proxy_options, + struct aws_byte_cursor endpoint_host, + uint16_t endpoint_port, + enum aws_socks5_address_type endpoint_address_type, + aws_channel_on_setup_completed_fn *on_setup_completed, + void *user_data); + +/** + * Creates a new socket channel through a SOCKS5 proxy using the provided bootstrap options. + * This function wraps the standard socket channel creation process to insert a SOCKS5 channel handler + * into the channel's handler chain, enabling connections through the specified SOCKS5 proxy. + * @param options The socket channel bootstrap options, including SOCKS5 proxy configuration + * @return AWS_OP_SUCCESS if the channel creation process was initiated successfully, AWS_OP_ERR otherwise with error code set + */ +AWS_IO_API int aws_socks5_client_bootstrap_new_socket_channel( + struct aws_socket_channel_bootstrap_options *options); + +/** + * Creates a new socket channel through a SOCKS5 proxy using the provided bootstrap options. + * This function is similar to aws_client_bootstrap_new_socket_channel but specifically handles + * the inclusion of SOCKS5 proxy options. + * @param allocator The allocator to use for memory allocations + * @param channel_options The socket channel bootstrap + * options, including SOCKS5 proxy configuration + * @param socks5_proxy_options The SOCKS5 proxy options to use for the connection + * @return AWS_OP_SUCCESS if the channel creation process was initiated successfully, AWS_OP_ERR otherwise + * with error code set + */ +AWS_IO_API int aws_client_bootstrap_new_socket_channel_with_socks5( + struct aws_allocator *allocator, + struct aws_socket_channel_bootstrap_options *channel_options, + const struct aws_socks5_proxy_options *socks5_proxy_options); + +/** + * Starts the SOCKS5 handshake process. Must be called after the handler is added to a slot. + */ +AWS_IO_API int aws_socks5_channel_handler_start_handshake( + struct aws_channel_handler *handler); + +/** + * Overrides the system vtable used by the SOCKS5 bootstrap logic. Pass NULL to restore defaults. + * Intended for testing. + */ +AWS_IO_API void aws_socks5_channel_handler_set_system_vtable( + const struct aws_socks5_system_vtable *system_vtable); + +AWS_EXTERN_C_END + +#endif /* AWS_IO_SOCKS5_CHANNEL_HANDLER_H */ diff --git a/source/io.c b/source/io.c index e8a5216f9..41fee80e6 100644 --- a/source/io.c +++ b/source/io.c @@ -5,11 +5,12 @@ #include #include +#include #include #include -#define AWS_DEFINE_ERROR_INFO_IO(CODE, STR) [(CODE) - 0x0400] = AWS_DEFINE_ERROR_INFO(CODE, STR, "aws-c-io") +#define AWS_DEFINE_ERROR_INFO_IO(CODE, STR) [(CODE)-0x0400] = AWS_DEFINE_ERROR_INFO(CODE, STR, "aws-c-io") #define AWS_DEFINE_ERROR_PKCS11_CKR(CKR) \ AWS_DEFINE_ERROR_INFO_IO( \ @@ -306,6 +307,7 @@ static struct aws_error_info s_errors[] = { AWS_IO_TLS_ERROR_READ_FAILURE, "Failure during TLS read."), AWS_DEFINE_ERROR_INFO_IO(AWS_ERROR_PEM_MALFORMED, "Malformed PEM object encountered."), + AWS_DEFINE_ERROR_INFO_IO( AWS_IO_SOCKET_MISSING_EVENT_LOOP, "Socket is missing its event loop."), @@ -351,6 +353,87 @@ static struct aws_error_info s_errors[] = { AWS_DEFINE_ERROR_INFO_IO( AWS_IO_TLS_HOST_NAME_MISMATCH, "Channel shutdown due to certificate's host name does not match the endpoint host name."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_INIT, + "Failed to initialize SOCKS5 proxy connection."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_AUTH_METHOD, + "SOCKS5 proxy server doesn't support any of the client's authentication methods."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_AUTH_FAILED, + "Authentication with SOCKS5 proxy server failed."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_CONNECTION_FAILED, + "Failed to establish connection through SOCKS5 proxy."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_REQUEST_FAILED, + "SOCKS5 proxy request failed."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_MALFORMED_RESPONSE, + "Received malformed response from SOCKS5 proxy."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_ADDRESS_TYPE, + "SOCKS5 proxy doesn't support the requested address type."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_BAD_ADDRESS, + "Invalid address format for SOCKS5 proxy."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_BAD_HANDSHAKE, + "SOCKS5 handshake failed."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_REJECTED, + "Connection rejected by SOCKS5 proxy."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_GENERAL_FAILURE, + "General failure reported by SOCKS5 proxy server."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_TTL_EXPIRED, + "TTL expired for connection through SOCKS5 proxy."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_COMMAND_NOT_SUPPORTED, + "Command not supported by SOCKS5 proxy server."), + + + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_INIT, + "Failed to initialize SOCKS5 proxy connection."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_AUTH_METHOD, + "SOCKS5 proxy server doesn't support any of the client's authentication methods."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_AUTH_FAILED, + "Authentication with SOCKS5 proxy server failed."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_CONNECTION_FAILED, + "Failed to establish connection through SOCKS5 proxy."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_REQUEST_FAILED, + "SOCKS5 proxy request failed."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_MALFORMED_RESPONSE, + "Received malformed response from SOCKS5 proxy."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_ADDRESS_TYPE, + "SOCKS5 proxy doesn't support the requested address type."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_BAD_ADDRESS, + "Invalid address format for SOCKS5 proxy."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_BAD_HANDSHAKE, + "SOCKS5 handshake failed."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_REJECTED, + "Connection rejected by SOCKS5 proxy."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_GENERAL_FAILURE, + "General failure reported by SOCKS5 proxy server."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_TTL_EXPIRED, + "TTL expired for connection through SOCKS5 proxy."), + AWS_DEFINE_ERROR_INFO_IO( + AWS_IO_SOCKS5_PROXY_ERROR_COMMAND_NOT_SUPPORTED, + "Command not supported by SOCKS5 proxy server."), + }; /* clang-format on */ diff --git a/source/socks5.c b/source/socks5.c new file mode 100644 index 000000000..0d08fa283 --- /dev/null +++ b/source/socks5.c @@ -0,0 +1,1153 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +AWS_STATIC_STRING_FROM_LITERAL(s_socks_none_method, "NONE"); +AWS_STATIC_STRING_FROM_LITERAL(s_socks_username_password_method, "USERNAME_PASSWORD"); +AWS_STATIC_STRING_FROM_LITERAL(s_socks_gssapi_method, "GSSAPI"); + +static struct aws_byte_cursor s_trim_ipv6_brackets(struct aws_byte_cursor host_cursor) { + if (host_cursor.len >= 2 && host_cursor.ptr && host_cursor.ptr[0] == '[') { + size_t last_index = host_cursor.len - 1; + if (host_cursor.ptr[last_index] == ']') { + host_cursor.ptr += 1; + host_cursor.len -= 2; + } + } + return host_cursor; +} + +/* Buffer size constants for SOCKS5 protocol operations */ +#define AWS_SOCKS5_SEND_BUFFER_INITIAL_SIZE 256 +#define AWS_SOCKS5_RECV_BUFFER_INITIAL_SIZE 512 + +static size_t s_string_length(const struct aws_string *str) { + return str ? str->len : 0; +} + +static const uint8_t *s_string_bytes(const struct aws_string *str) { + return str ? aws_string_bytes(str) : NULL; +} + +/** + * Helper function to ensure a buffer has enough capacity for additional data. + * + * This function checks if the buffer has enough remaining capacity for the required + * space, and if not, reserves more space in the buffer. + * + * @param buffer The buffer to check and potentially resize + * @param required_space How much additional space is needed + * @return AWS_OP_SUCCESS if the buffer has enough space, AWS_OP_ERR otherwise + */ +static int s_ensure_buffer_has_capacity( + struct aws_byte_buf *buffer, + size_t required_space) { + + if (!buffer) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* Calculate available space safely */ + size_t available_space = (buffer->capacity > buffer->len) ? + (buffer->capacity - buffer->len) : 0; + + /* Only reserve more if we don't have enough */ + if (required_space > available_space) { + if (aws_byte_buf_reserve(buffer, buffer->len + required_space)) { + return AWS_OP_ERR; + } + } + + return AWS_OP_SUCCESS; +} + +/* Normalizes IPv4/IPv6 literals by stripping RFC-3986 decorations such as + * surrounding brackets and scope identifiers (e.g. "%eth0"). Operates in-place + * on a null-terminated buffer. */ +static void s_normalize_ip_literal(char *address_buffer) { + if (!address_buffer || address_buffer[0] == '\0') { + return; + } + + size_t buf_len = strlen(address_buffer); + if (buf_len > 1 && address_buffer[0] == '[') { + char *closing = strchr(address_buffer, ']'); + if (closing && closing > address_buffer) { + size_t literal_len = (size_t)(closing - (address_buffer + 1)); + memmove(address_buffer, address_buffer + 1, literal_len); + address_buffer[literal_len] = '\0'; + buf_len = literal_len; + } + } + + char *zone_delimiter = strchr(address_buffer, '%'); + if (zone_delimiter) { + *zone_delimiter = '\0'; + } +} + +/* Helper for converting auth method enum to string for logging */ +static struct aws_string *s_auth_method_to_string(enum aws_socks5_auth_method method) { + switch (method) { + case AWS_SOCKS5_AUTH_NONE: + return (struct aws_string *)s_socks_none_method; + case AWS_SOCKS5_AUTH_USERNAME_PASSWORD: + return (struct aws_string *)s_socks_username_password_method; + case AWS_SOCKS5_AUTH_GSSAPI: + return (struct aws_string *)s_socks_gssapi_method; + default: + return NULL; + } +} + +int aws_socks5_proxy_options_init( + struct aws_socks5_proxy_options *options, + struct aws_allocator *allocator, + struct aws_byte_cursor host, + uint16_t port) { + + if (!options || !allocator) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* Validate host cursor */ + if (!aws_byte_cursor_is_valid(&host) || host.len == 0 || host.ptr == NULL) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Invalid host provided to SOCKS5 proxy options init"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + aws_socks5_proxy_options_init_default(options); + options->port = port; + struct aws_byte_cursor normalized_host = s_trim_ipv6_brackets(host); + options->host = aws_string_new_from_cursor(allocator, &normalized_host); + if (options->host == NULL) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Failed to copy host for SOCKS5 proxy options"); + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +int aws_socks5_proxy_options_init_default( + struct aws_socks5_proxy_options *options) { + AWS_ZERO_STRUCT(*options); + options->port = 1080; /* Default SOCKS5 port */ + options->connection_timeout_ms = 3000; /* Default timeout of 3 seconds */ + options->host_resolution_mode = AWS_SOCKS5_HOST_RESOLUTION_PROXY; + + return AWS_OP_SUCCESS; +} + +/* Destination must be zero-initialized before calling to avoid leaking prior allocations. */ +int aws_socks5_proxy_options_copy( + struct aws_socks5_proxy_options *dest, + const struct aws_socks5_proxy_options *src) { + + if (!dest || !src) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + AWS_ZERO_STRUCT(*dest); + dest->port = src->port; + dest->connection_timeout_ms = src->connection_timeout_ms; + dest->host_resolution_mode = src->host_resolution_mode; + + if (src->host) { + dest->host = aws_string_new_from_string(src->host->allocator, src->host); + if (!dest->host) { + goto on_error; + } + } + + if (src->username) { + dest->username = aws_string_new_from_string(src->username->allocator, src->username); + if (!dest->username) { + goto on_error; + } + } + + if (src->password) { + dest->password = aws_string_new_from_string(src->password->allocator, src->password); + if (!dest->password) { + goto on_error; + } + } + + return AWS_OP_SUCCESS; + +on_error: + aws_socks5_proxy_options_clean_up(dest); + return AWS_OP_ERR; +} + +void aws_socks5_proxy_options_clean_up(struct aws_socks5_proxy_options *options) { + if (!options) { + return; + } + + aws_string_destroy(options->host); + aws_string_destroy_secure(options->username); + aws_string_destroy_secure(options->password); + + AWS_ZERO_STRUCT(*options); +} + +int aws_socks5_proxy_options_set_auth( + struct aws_socks5_proxy_options *options, + struct aws_allocator *allocator, + struct aws_byte_cursor username, + struct aws_byte_cursor password) { + + if (!options || !allocator) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (options->username) { + aws_string_destroy_secure(options->username); + options->username = NULL; + } + if (options->password) { + aws_string_destroy_secure(options->password); + options->password = NULL; + } + if (username.len > 0) { + options->username = aws_string_new_from_cursor(allocator, &username); + if (!options->username) { + return AWS_OP_ERR; + } + } + if (password.len > 0) { + options->password = aws_string_new_from_cursor(allocator, &password); + if (!options->password) { + return AWS_OP_ERR; + } + } + return AWS_OP_SUCCESS; +} + +void aws_socks5_proxy_options_set_host_resolution_mode( + struct aws_socks5_proxy_options *options, + enum aws_socks5_host_resolution_mode mode) { + + if (!options) { + return; + } + + options->host_resolution_mode = mode; +} + +enum aws_socks5_host_resolution_mode aws_socks5_proxy_options_get_host_resolution_mode( + const struct aws_socks5_proxy_options *options) { + + if (!options) { + return AWS_SOCKS5_HOST_RESOLUTION_PROXY; + } + + return options->host_resolution_mode; +} + +AWS_IO_API enum aws_socks5_address_type aws_socks5_infer_address_type( + struct aws_byte_cursor target_host, + enum aws_socks5_address_type requested_type) { + + if (requested_type != AWS_SOCKS5_ATYP_DOMAIN || target_host.len == 0 || target_host.ptr == NULL) { + return requested_type; + } + + char address_buffer[AWS_ADDRESS_MAX_LEN]; + size_t host_len = target_host.len; + if (host_len >= sizeof(address_buffer)) { + host_len = sizeof(address_buffer) - 1; + } + memcpy(address_buffer, target_host.ptr, host_len); + address_buffer[host_len] = '\0'; + + s_normalize_ip_literal(address_buffer); + + unsigned char ipv4_buffer[4]; + unsigned char ipv6_buffer[16]; + + if (inet_pton(AF_INET, address_buffer, ipv4_buffer) == 1) { + return AWS_SOCKS5_ATYP_IPV4; + } + + if (inet_pton(AF_INET6, address_buffer, ipv6_buffer) == 1) { + return AWS_SOCKS5_ATYP_IPV6; + } + + return requested_type; +} + +int aws_socks5_context_init( + struct aws_socks5_context *context, + struct aws_allocator *allocator, + const struct aws_socks5_proxy_options *options, + struct aws_byte_cursor target_host, + uint16_t target_port, + enum aws_socks5_address_type address_type) { + + if (!context) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: context is NULL in aws_socks5_context_init"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (!allocator) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: allocator is NULL in aws_socks5_context_init"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (!options) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: options is NULL in aws_socks5_context_init"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (!target_host.ptr || target_host.len == 0) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: target host is invalid (NULL or empty) in aws_socks5_context_init"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: Proxy options: host=%p len=%zu, port=%d", + (void *)options->host, + s_string_length(options->host), + options->port); + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: Proxy endpoint host='%.*s', port=%d", + (int)target_host.len, + target_host.ptr ? (const char *)target_host.ptr : "", + target_port); + + AWS_ZERO_STRUCT(*context); + context->allocator = allocator; + context->state = AWS_SOCKS5_STATE_INIT; + + if (aws_array_list_init_dynamic(&context->auth_methods, allocator, 3, sizeof(enum aws_socks5_auth_method))) { + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_INIT); + } + + enum aws_socks5_auth_method no_auth = AWS_SOCKS5_AUTH_NONE; + aws_array_list_push_back(&context->auth_methods, &no_auth); + + size_t options_username_len = options->username ? options->username->len : 0; + size_t options_password_len = options->password ? options->password->len : 0; + + if (options_username_len > 0 && options_password_len > 0) { + enum aws_socks5_auth_method user_pass = AWS_SOCKS5_AUTH_USERNAME_PASSWORD; + aws_array_list_push_back(&context->auth_methods, &user_pass); + } + + if (options->host == NULL || options->host->len == 0) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Invalid host in SOCKS5 proxy options (buffer=%p, len=%zu)", + (void *)options->host, + options->host ? options->host->len : 0); + aws_socks5_context_clean_up(context); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (aws_socks5_proxy_options_copy(&context->options, options)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Failed to copy proxy options: error=%d (%s)", + error_code, + aws_error_str(error_code)); + aws_array_list_clean_up(&context->auth_methods); + return AWS_OP_ERR; + } + + struct aws_byte_cursor host_copy = target_host; + context->endpoint_host = aws_string_new_from_cursor(allocator, &host_copy); + if (!context->endpoint_host) { + aws_array_list_clean_up(&context->auth_methods); + aws_socks5_proxy_options_clean_up(&context->options); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_INIT); + } + + context->endpoint_port = target_port; + context->endpoint_address_type = aws_socks5_infer_address_type(target_host, address_type); + + if (aws_byte_buf_init(&context->send_buf, allocator, AWS_SOCKS5_SEND_BUFFER_INITIAL_SIZE)) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: Failed to initialize send buffer"); + aws_array_list_clean_up(&context->auth_methods); + aws_socks5_proxy_options_clean_up(&context->options); + aws_string_destroy(context->endpoint_host); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_INIT); + } + + if (aws_byte_buf_init(&context->recv_buf, allocator, AWS_SOCKS5_RECV_BUFFER_INITIAL_SIZE)) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: Failed to initialize receive buffer"); + aws_array_list_clean_up(&context->auth_methods); + aws_socks5_proxy_options_clean_up(&context->options); + aws_string_destroy(context->endpoint_host); + aws_byte_buf_clean_up(&context->send_buf); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_INIT); + } + + return AWS_OP_SUCCESS; +} + +void aws_socks5_context_clean_up(struct aws_socks5_context *context) { + if (!context) { + return; + } + + aws_array_list_clean_up(&context->auth_methods); + aws_socks5_proxy_options_clean_up(&context->options); + aws_string_destroy(context->endpoint_host); + + if (context->send_buf.buffer) { + aws_byte_buf_clean_up(&context->send_buf); + } + + if (context->recv_buf.buffer) { + aws_byte_buf_clean_up(&context->recv_buf); + } + + AWS_ZERO_STRUCT(*context); +} + +int aws_socks5_write_greeting( + struct aws_socks5_context *context, + struct aws_byte_buf *buffer) { + + if (!context || !buffer) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + size_t num_methods = aws_array_list_length(&context->auth_methods); + if (num_methods == 0) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: No authentication methods available for SOCKS5 greeting"); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_INIT); + } + + /* SOCKS5 greeting format: + * +----+----------+----------+ + * |VER | NMETHODS | METHODS | + * +----+----------+----------+ + * | 1 | 1 | 1 to 255 | + * +----+----------+----------+ + */ + size_t greeting_size = 2 + num_methods; /* VER(1) + NMETHODS(1) + METHODS(n) */ + + /* Use the helper function to ensure buffer capacity */ + if (s_ensure_buffer_has_capacity(buffer, greeting_size)) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, "id=static: Failed to allocate buffer for SOCKS5 greeting, size=%zu", greeting_size); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_INIT); + } + + /* Write SOCKS5 version */ + buffer->buffer[buffer->len++] = AWS_SOCKS5_VERSION; + + /* Write number of auth methods */ + buffer->buffer[buffer->len++] = (uint8_t)num_methods; + + /* Write the auth methods */ + for (size_t i = 0; i < num_methods; i++) { + enum aws_socks5_auth_method method; + if (aws_array_list_get_at(&context->auth_methods, &method, i)) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: Failed to get auth method from list at index %zu", i); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_INIT); + } + + buffer->buffer[buffer->len++] = (uint8_t)method; + + /* Log which auth methods we're offering */ + struct aws_string *method_str = s_auth_method_to_string(method); + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: Offering SOCKS5 auth method %s", + method_str ? aws_string_c_str(method_str) : "UNKNOWN"); + } + + /* Update context state */ + context->state = AWS_SOCKS5_STATE_GREETING_SENT; + + AWS_LOGF_INFO( + AWS_LS_IO_SOCKS5, + "id=static: Prepared SOCKS5 greeting with %zu auth methods", + num_methods); + + return AWS_OP_SUCCESS; +} + +int aws_socks5_read_greeting_response( + struct aws_socks5_context *context, + struct aws_byte_cursor *data) { + + if (!context || !data || !data->ptr) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (context->state != AWS_SOCKS5_STATE_GREETING_SENT) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Invalid state for reading SOCKS5 greeting response, expected %d, got %d", + AWS_SOCKS5_STATE_GREETING_SENT, + context->state); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_BAD_HANDSHAKE); + } + + if (data->len < AWS_SOCKS5_GREETING_RESP_SIZE) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: SOCKS5 greeting response too short, expected %d bytes, got %zu", + AWS_SOCKS5_GREETING_RESP_SIZE, + data->len); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_MALFORMED_RESPONSE); + } + + /* SOCKS5 greeting response format: + * +----+--------+ + * |VER | METHOD | + * +----+--------+ + * | 1 | 1 | + * +----+--------+ + */ + uint8_t version = data->ptr[0]; + uint8_t method = data->ptr[1]; + + /* Verify SOCKS version */ + if (version != AWS_SOCKS5_VERSION) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Unexpected SOCKS version in greeting response, expected %d, got %d", + AWS_SOCKS5_VERSION, + version); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_MALFORMED_RESPONSE); + } + + /* Check selected auth method */ + if (method == AWS_SOCKS5_AUTH_NO_ACCEPTABLE) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server rejected all authentication methods"); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_AUTH_METHOD); + } + + /* Store the selected auth method */ + context->selected_auth = (enum aws_socks5_auth_method)method; + context->state = AWS_SOCKS5_STATE_GREETING_RECEIVED; + + struct aws_string *method_str = s_auth_method_to_string(context->selected_auth); + AWS_LOGF_INFO( + AWS_LS_IO_SOCKS5, + "id=static: SOCKS5 server selected auth method: %s", + method_str ? aws_string_c_str(method_str) : "UNKNOWN"); + + return AWS_OP_SUCCESS; +} + +int aws_socks5_write_auth_request( + struct aws_socks5_context *context, + struct aws_byte_buf *buffer) { + + if (!context || !buffer) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (context->state != AWS_SOCKS5_STATE_GREETING_RECEIVED) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Invalid state for writing SOCKS5 auth request, expected %d, got %d", + AWS_SOCKS5_STATE_GREETING_RECEIVED, + context->state); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_BAD_HANDSHAKE); + } + + /* Check which auth method was selected */ + switch (context->selected_auth) { + case AWS_SOCKS5_AUTH_NONE: + /* No authentication needed, skip to connection phase */ + context->state = AWS_SOCKS5_STATE_AUTH_COMPLETED; + return AWS_OP_SUCCESS; + + case AWS_SOCKS5_AUTH_USERNAME_PASSWORD: + /* Continue with username/password authentication */ + break; + + case AWS_SOCKS5_AUTH_GSSAPI: + /* GSSAPI not supported yet */ + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: GSSAPI authentication not supported"); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_AUTH_METHOD); + + default: + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, "id=static: Unknown authentication method: %d", context->selected_auth); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_AUTH_METHOD); + } + + /* Check if we have username/password in options */ + if (context->options.username->len == 0 || context->options.password->len == 0) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Username/password authentication required but credentials not provided"); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_AUTH_FAILED); + } + + /* Username/Password authentication (RFC 1929) + * +----+------+----------+------+----------+ + * |VER | ULEN | UNAME | PLEN | PASSWD | + * +----+------+----------+------+----------+ + * | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + * +----+------+----------+------+----------+ + */ + + /* Check username and password lengths (must be between 1-255 bytes) */ + size_t username_len = s_string_length(context->options.username); + size_t password_len = s_string_length(context->options.password); + if (username_len > 255 || password_len > 255) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Username or password too long (max 255 bytes): ulen=%zu, plen=%zu", + username_len, + password_len); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_AUTH_FAILED); + } + + /* Calculate total auth request size */ + size_t auth_size = 3 + username_len + password_len; + + /* Use the helper function to ensure buffer capacity */ + if (s_ensure_buffer_has_capacity(buffer, auth_size)) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, "id=static: Failed to allocate buffer for SOCKS5 auth request, size=%zu", auth_size); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_AUTH_FAILED); + } + + /* Write sub-negotiation version (0x01 for username/password) */ + buffer->buffer[buffer->len++] = AWS_SOCKS5_AUTH_VERSION; + + /* Write username length and username */ + buffer->buffer[buffer->len++] = (uint8_t)username_len; + if (username_len > 0) { + memcpy(buffer->buffer + buffer->len, s_string_bytes(context->options.username), username_len); + buffer->len += username_len; + } + + /* Write password length and password */ + buffer->buffer[buffer->len++] = (uint8_t)password_len; + if (password_len > 0) { + memcpy(buffer->buffer + buffer->len, s_string_bytes(context->options.password), password_len); + buffer->len += password_len; + } + + /* Update state */ + context->state = AWS_SOCKS5_STATE_AUTH_STARTED; + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: Prepared SOCKS5 username/password auth request with username=%.*s", + (int)username_len, + (const char *)s_string_bytes(context->options.username)); + + return AWS_OP_SUCCESS; +} + +int aws_socks5_read_auth_response( + struct aws_socks5_context *context, + struct aws_byte_cursor *data) { + + if (!context || !data || !data->ptr) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* If no auth required, we can skip this step */ + if (context->selected_auth == AWS_SOCKS5_AUTH_NONE) { + context->state = AWS_SOCKS5_STATE_AUTH_COMPLETED; + return AWS_OP_SUCCESS; + } + + if (context->state != AWS_SOCKS5_STATE_AUTH_STARTED) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Invalid state for reading SOCKS5 auth response, expected %d, got %d", + AWS_SOCKS5_STATE_AUTH_STARTED, + context->state); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_BAD_HANDSHAKE); + } + + if (data->len < AWS_SOCKS5_AUTH_RESP_SIZE) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: SOCKS5 auth response too short, expected %d bytes, got %zu", + AWS_SOCKS5_AUTH_RESP_SIZE, + data->len); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_MALFORMED_RESPONSE); + } + + /* Username/Password auth response format: + * +----+--------+ + * |VER | STATUS | + * +----+--------+ + * | 1 | 1 | + * +----+--------+ + */ + uint8_t version = data->ptr[0]; + uint8_t status = data->ptr[1]; + + /* Verify auth version */ + if (version != AWS_SOCKS5_AUTH_VERSION) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Unexpected auth version in SOCKS5 auth response, expected %d, got %d", + AWS_SOCKS5_AUTH_VERSION, + version); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_MALFORMED_RESPONSE); + } + + /* Check auth status (0 = success, anything else = failure) */ + if (status != 0) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, "id=static: SOCKS5 authentication failed with status code %d", status); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_AUTH_FAILED); + } + + /* Authentication successful */ + context->state = AWS_SOCKS5_STATE_AUTH_COMPLETED; + + AWS_LOGF_DEBUG(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 authentication successful"); + + return AWS_OP_SUCCESS; +} + +int aws_socks5_write_connect_request( + struct aws_socks5_context *context, + struct aws_byte_buf *buffer) { + + if (!context || !buffer) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (context->state != AWS_SOCKS5_STATE_AUTH_COMPLETED && + context->state != AWS_SOCKS5_STATE_GREETING_RECEIVED) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Invalid state for writing SOCKS5 connect request, expected %d, got %d", + AWS_SOCKS5_STATE_AUTH_COMPLETED, + context->state); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_BAD_HANDSHAKE); + } + + /* Check if target host and port are set */ + if (s_string_length(context->endpoint_host) == 0) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: Target host not set for SOCKS5 connection"); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_BAD_ADDRESS); + } + + /* Determine the address type and required buffer size */ + enum aws_socks5_address_type addr_type = context->endpoint_address_type; + size_t addr_size = 0; + + switch (addr_type) { + case AWS_SOCKS5_ATYP_DOMAIN: + /* Domain name (1 byte length + domain name) */ + if (s_string_length(context->endpoint_host) > 255) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Domain name too long for SOCKS5 (max 255 bytes): %zu", + s_string_length(context->endpoint_host)); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_BAD_ADDRESS); + } + addr_size = 1 + s_string_length(context->endpoint_host); /* Length byte + domain */ + break; + + case AWS_SOCKS5_ATYP_IPV4: + /* IPv4 address (4 bytes) */ + addr_size = 4; + break; + + case AWS_SOCKS5_ATYP_IPV6: + /* IPv6 address (16 bytes) */ + addr_size = 16; + break; + + default: + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, "id=static: Unsupported address type: %d", addr_type); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_ADDRESS_TYPE); + } + + /* SOCKS5 request format: + * +----+-----+-------+------+----------+----------+ + * |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + * +----+-----+-------+------+----------+----------+ + * | 1 | 1 | X'00' | 1 | Variable | 2 | + * +----+-----+-------+------+----------+----------+ + */ + + /* Calculate total request size */ + size_t req_size = 6 + addr_size; /* VER(1) + CMD(1) + RSV(1) + ATYP(1) + ADDR(var) + PORT(2) */ + + /* Use the helper function to ensure buffer capacity */ + if (s_ensure_buffer_has_capacity(buffer, req_size)) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, "id=static: Failed to allocate buffer for SOCKS5 connect request, size=%zu", req_size); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_REQUEST_FAILED); + } + + /* Write SOCKS5 version */ + buffer->buffer[buffer->len++] = AWS_SOCKS5_VERSION; + + /* Write command (CONNECT) */ + buffer->buffer[buffer->len++] = AWS_SOCKS5_COMMAND_CONNECT; + + /* Write reserved byte (0x00) */ + buffer->buffer[buffer->len++] = AWS_SOCKS5_RESERVED; + + /* Write address type */ + buffer->buffer[buffer->len++] = (uint8_t)addr_type; + + /* Write destination address */ + switch (addr_type) { + case AWS_SOCKS5_ATYP_DOMAIN: { + size_t target_len = s_string_length(context->endpoint_host); + buffer->buffer[buffer->len++] = (uint8_t)target_len; + if (target_len > 0) { + memcpy(buffer->buffer + buffer->len, s_string_bytes(context->endpoint_host), target_len); + buffer->len += target_len; + } + break; + } + + case AWS_SOCKS5_ATYP_IPV4: { + uint8_t binary_addr[4]; + size_t target_len = s_string_length(context->endpoint_host); + + if (target_len == 4) { + memcpy(buffer->buffer + buffer->len, s_string_bytes(context->endpoint_host), 4); + } else { + char ip_str[128]; + size_t copy_len = target_len < 127 ? target_len : 127; + memcpy(ip_str, s_string_bytes(context->endpoint_host), copy_len); + ip_str[copy_len] = '\0'; + s_normalize_ip_literal(ip_str); + + if (inet_pton(AF_INET, ip_str, binary_addr) != 1) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Failed to convert IPv4 address '%s' to binary", + ip_str); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_BAD_ADDRESS); + } + + memcpy(buffer->buffer + buffer->len, binary_addr, 4); + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: Converted IPv4 '%s' to binary: %d.%d.%d.%d", + ip_str, + binary_addr[0], binary_addr[1], binary_addr[2], binary_addr[3]); + } + + buffer->len += 4; + break; + } + + case AWS_SOCKS5_ATYP_IPV6: { + uint8_t binary_addr[16]; + size_t target_len = s_string_length(context->endpoint_host); + + if (target_len == 16) { + memcpy(buffer->buffer + buffer->len, s_string_bytes(context->endpoint_host), 16); + } else { + char ip_str[128]; + size_t copy_len = target_len < 127 ? target_len : 127; + memcpy(ip_str, s_string_bytes(context->endpoint_host), copy_len); + ip_str[copy_len] = '\0'; + s_normalize_ip_literal(ip_str); + + if (inet_pton(AF_INET6, ip_str, binary_addr) != 1) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Failed to convert IPv6 address '%s' to binary", + ip_str); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_BAD_ADDRESS); + } + + memcpy(buffer->buffer + buffer->len, binary_addr, 16); + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: Converted IPv6 '%s' to binary format", + ip_str); + } + + buffer->len += 16; + break; + } + + default: + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: Unsupported address type: %d", addr_type); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_ADDRESS_TYPE); + } + + /* Write destination port (network byte order) */ + uint16_t port_n = htons(context->endpoint_port); + memcpy(buffer->buffer + buffer->len, &port_n, sizeof(uint16_t)); + buffer->len += sizeof(uint16_t); + + /* Update state */ + context->state = AWS_SOCKS5_STATE_REQUEST_SENT; + + /* Log the connection attempt */ + switch (addr_type) { + case AWS_SOCKS5_ATYP_DOMAIN: + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: Prepared SOCKS5 CONNECT request for domain %.*s:%d", + (int)s_string_length(context->endpoint_host), + (const char *)s_string_bytes(context->endpoint_host), + context->endpoint_port); + break; + + case AWS_SOCKS5_ATYP_IPV4: + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: Prepared SOCKS5 CONNECT request for IPv4 address %.*s:%d", + (int)s_string_length(context->endpoint_host), + (const char *)s_string_bytes(context->endpoint_host), + context->endpoint_port); + break; + + case AWS_SOCKS5_ATYP_IPV6: + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: Prepared SOCKS5 CONNECT request for IPv6 address %.*s:%d", + (int)s_string_length(context->endpoint_host), + (const char *)s_string_bytes(context->endpoint_host), + context->endpoint_port); + break; + + default: + /* This should never happen as we already checked earlier */ + break; + } + + return AWS_OP_SUCCESS; +} + +int aws_socks5_read_connect_response( + struct aws_socks5_context *context, + struct aws_byte_cursor *data) { + + if (!context || !data || !data->ptr) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (context->state != AWS_SOCKS5_STATE_REQUEST_SENT) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Invalid state for reading SOCKS5 connect response, expected %d, got %d", + AWS_SOCKS5_STATE_REQUEST_SENT, + context->state); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_BAD_HANDSHAKE); + } + + /* SOCKS5 response format: + * +----+-----+-------+------+----------+----------+ + * |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + * +----+-----+-------+------+----------+----------+ + * | 1 | 1 | X'00' | 1 | Variable | 2 | + * +----+-----+-------+------+----------+----------+ + */ + + /* Check minimum response size */ + if (data->len < AWS_SOCKS5_CONN_RESP_MIN_SIZE) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: SOCKS5 connect response too short, expected at least %d bytes, got %zu", + AWS_SOCKS5_CONN_RESP_MIN_SIZE, + data->len); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_MALFORMED_RESPONSE); + } + + /* Read the fixed response fields */ + uint8_t version = data->ptr[0]; + uint8_t status = data->ptr[1]; + uint8_t reserved = data->ptr[2]; + uint8_t atype = data->ptr[3]; + + /* Verify SOCKS version */ + if (version != AWS_SOCKS5_VERSION) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: Unexpected SOCKS version in connect response, expected %d, got %d", + AWS_SOCKS5_VERSION, + version); + context->state = AWS_SOCKS5_STATE_ERROR; + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_MALFORMED_RESPONSE); + } + + /* Verify reserved byte */ + if (reserved != AWS_SOCKS5_RESERVED) { + AWS_LOGF_WARN( + AWS_LS_IO_SOCKS5, + "id=static: Unexpected reserved byte in SOCKS5 connect response, expected 0, got %d", + reserved); + /* Continue anyway, as this isn't critical */ + } + + /* Check status code */ + if (status != AWS_SOCKS5_STATUS_SUCCESS) { + /* Handle specific error codes */ + context->state = AWS_SOCKS5_STATE_ERROR; + + switch (status) { + case AWS_SOCKS5_STATUS_GENERAL_FAILURE: + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server reported general failure"); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_GENERAL_FAILURE); + + case AWS_SOCKS5_STATUS_CONNECTION_NOT_ALLOWED: + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server rejected connection"); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_REJECTED); + + case AWS_SOCKS5_STATUS_NETWORK_UNREACHABLE: + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server reported network unreachable"); + return aws_raise_error(AWS_IO_SOCKET_NO_ROUTE_TO_HOST); + + case AWS_SOCKS5_STATUS_HOST_UNREACHABLE: + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server reported host unreachable"); + return aws_raise_error(AWS_IO_DNS_NO_ADDRESS_FOR_HOST); + + case AWS_SOCKS5_STATUS_CONNECTION_REFUSED: + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server reported connection refused"); + return aws_raise_error(AWS_IO_SOCKET_CONNECTION_REFUSED); + + case AWS_SOCKS5_STATUS_TTL_EXPIRED: + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server reported TTL expired"); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_TTL_EXPIRED); + + case AWS_SOCKS5_STATUS_COMMAND_NOT_SUPPORTED: + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server does not support the CONNECT command"); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_COMMAND_NOT_SUPPORTED); + + case AWS_SOCKS5_STATUS_ADDRESS_TYPE_NOT_SUPPORTED: + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server does not support the address type"); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_ADDRESS_TYPE); + + default: + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server returned unknown error: %d", status); + return aws_raise_error(AWS_IO_SOCKS5_PROXY_ERROR_CONNECTION_FAILED); + } + } + + /* Connection successful */ + context->state = AWS_SOCKS5_STATE_RESPONSE_RECEIVED; + + /* Parse bound address and port if needed (for informational purposes) + * Note: We don't actually need to use the bound address/port for CONNECT, + * but we'll log it for debugging purposes. + */ + size_t addr_offset = 4; + size_t addr_size = 0; + + switch (atype) { + case AWS_SOCKS5_ATYP_DOMAIN: { + /* Domain address format: [len][domain]... */ + uint8_t dom_len = data->ptr[addr_offset]; + addr_offset++; + addr_size = dom_len; + + if (data->len < addr_offset + addr_size + 2) { + AWS_LOGF_WARN(AWS_LS_IO_SOCKS5, "id=static: Truncated domain address in SOCKS5 response"); + /* Continue anyway, as we've already confirmed success */ + } else { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: SOCKS5 server bound to domain %.*s", + (int)addr_size, + (char *)(data->ptr + addr_offset)); + } + break; + } + + case AWS_SOCKS5_ATYP_IPV4: + /* IPv4 address (4 bytes) */ + addr_size = 4; + if (data->len < addr_offset + addr_size + 2) { + AWS_LOGF_WARN(AWS_LS_IO_SOCKS5, "id=static: Truncated IPv4 address in SOCKS5 response"); + } else { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=static: SOCKS5 server bound to IPv4 address %d.%d.%d.%d", + data->ptr[addr_offset], + data->ptr[addr_offset + 1], + data->ptr[addr_offset + 2], + data->ptr[addr_offset + 3]); + } + break; + + case AWS_SOCKS5_ATYP_IPV6: + /* IPv6 address (16 bytes) */ + addr_size = 16; + if (data->len < addr_offset + addr_size + 2) { + AWS_LOGF_WARN(AWS_LS_IO_SOCKS5, "id=static: Truncated IPv6 address in SOCKS5 response"); + } + break; + + default: + AWS_LOGF_WARN( + AWS_LS_IO_SOCKS5, + "id=static: Unknown address type in SOCKS5 connect response: %d", + atype); + /* Continue anyway, as we've already confirmed success */ + break; + } + + /* Read port if we have enough data */ + if (data->len >= addr_offset + addr_size + 2) { + uint16_t port; + memcpy(&port, data->ptr + addr_offset + addr_size, sizeof(uint16_t)); + port = ntohs(port); + + AWS_LOGF_DEBUG(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 server bound to port %d", port); + } + + context->state = AWS_SOCKS5_STATE_CONNECTED; + + AWS_LOGF_DEBUG(AWS_LS_IO_SOCKS5, "id=static: SOCKS5 connection established successfully"); + + return AWS_OP_SUCCESS; +} diff --git a/source/socks5_channel_handler.c b/source/socks5_channel_handler.c new file mode 100644 index 000000000..908488f46 --- /dev/null +++ b/source/socks5_channel_handler.c @@ -0,0 +1,3302 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/* Structure for passing both proxy options and original user data */ +struct aws_http_proxy_user_data { + struct aws_allocator *allocator; + void *proxy_options; + void *user_data; +}; + +/* Structure to store context for the channel-bootstrap adapter */ +struct aws_socks5_adapter_context { + struct aws_client_bootstrap *bootstrap; + aws_client_bootstrap_on_channel_event_fn *original_callback; + void *original_user_data; +}; + +static int s_socks5_bootstrap_begin_handshake( + struct aws_socks5_bootstrap *socks5_bootstrap, + struct aws_channel *channel); + +static int s_socks5_bootstrap_start_endpoint_resolution( + struct aws_socks5_bootstrap *socks5_bootstrap, + const struct aws_socket_channel_bootstrap_options *channel_options); + +static void s_socks5_bootstrap_resolution_success_task( + struct aws_channel_task *task, + void *arg, + enum aws_task_status status); + +static void s_socks5_bootstrap_resolution_failure_task( + struct aws_channel_task *task, + void *arg, + enum aws_task_status status); + +static void s_socks5_on_host_resolved( + struct aws_host_resolver *resolver, + const struct aws_string *host_name, + int err_code, + const struct aws_array_list *host_addresses, + void *user_data); + +static const struct aws_socks5_system_vtable s_default_socks5_system_vtable = { + .aws_client_bootstrap_new_socket_channel = aws_client_bootstrap_new_socket_channel, +}; + +static const struct aws_socks5_system_vtable *s_socks5_system_vtable = &s_default_socks5_system_vtable; + +void aws_socks5_channel_handler_set_system_vtable(const struct aws_socks5_system_vtable *system_vtable) { + if (system_vtable != NULL) { + s_socks5_system_vtable = system_vtable; + } else { + s_socks5_system_vtable = &s_default_socks5_system_vtable; + } +} + +/* SOCKS5 URIs follow RFC-3986, where IPv6 literals are enclosed in brackets. + * DNS APIs expect bare literals, so strip a single leading '[' and trailing ']'. */ +static struct aws_byte_cursor s_normalize_proxy_host_cursor(struct aws_byte_cursor host_cursor) { + if (host_cursor.len >= 2 && host_cursor.ptr[0] == '[') { + size_t last_index = host_cursor.len - 1; + if (host_cursor.ptr[last_index] == ']') { + host_cursor.ptr += 1; + host_cursor.len -= 2; + } + } + return host_cursor; +} + +/** + * State machine for the SOCKS5 channel handler + * + * Valid state transitions: + * INIT -> GREETING (when handshake starts) + * GREETING -> AUTH (if authentication required) + * GREETING -> CONNECT (if no authentication required) + * AUTH -> CONNECT (after authentication completes) + * CONNECT -> ESTABLISHED (when connection established) + * ANY -> ERROR (on any error) + * + * These states align with but are distinct from the protocol states in aws_socks5_state. + * This represents the channel handler's state specifically. + */ +enum aws_socks5_channel_state { + AWS_SOCKS5_CHANNEL_STATE_INIT, /* Initial state before handshake begins */ + AWS_SOCKS5_CHANNEL_STATE_GREETING, /* Sending greeting and processing response */ + AWS_SOCKS5_CHANNEL_STATE_AUTH, /* Performing authentication if required */ + AWS_SOCKS5_CHANNEL_STATE_CONNECT, /* Sending connect request and processing response */ + AWS_SOCKS5_CHANNEL_STATE_ESTABLISHED, /* Connection established, ready for data transfer */ + AWS_SOCKS5_CHANNEL_STATE_ERROR /* Error occurred, no further progress possible */ +}; + +/** + * Converts a SOCKS5 channel state to a readable string for logging purposes. + * This improves log readability by providing human-readable state names. + * + */ +static inline const char *s_socks5_channel_state_to_string(enum aws_socks5_channel_state state) { + switch (state) { + case AWS_SOCKS5_CHANNEL_STATE_INIT: + return "INIT"; + case AWS_SOCKS5_CHANNEL_STATE_GREETING: + return "GREETING"; + case AWS_SOCKS5_CHANNEL_STATE_AUTH: + return "AUTH"; + case AWS_SOCKS5_CHANNEL_STATE_CONNECT: + return "CONNECT"; + case AWS_SOCKS5_CHANNEL_STATE_ESTABLISHED: + return "ESTABLISHED"; + case AWS_SOCKS5_CHANNEL_STATE_ERROR: + return "ERROR"; + default: + return "UNKNOWN"; + } +} + +struct aws_socks5_channel_handler { + /* Base handler data */ + struct aws_channel_handler handler; + struct aws_allocator *allocator; + struct aws_channel_slot *slot; /* Current channel slot */ + + /* Channel and connection state */ + enum aws_socks5_channel_state channel_state; + int error_code; + bool process_incoming_data; + + /* SOCKS5 protocol context and buffers */ + struct aws_socks5_context context; + struct aws_byte_buf send_buffer; /* Buffer for outgoing SOCKS5 protocol messages */ + struct aws_byte_buf read_buffer; /* Buffer for accumulating incoming data */ + + /* Callback management */ + aws_channel_on_setup_completed_fn *on_setup_completed; + void *user_data; + + /* Timeout management */ + uint64_t connect_timeout_ns; + struct aws_channel_task timeout_task; + bool timeout_task_scheduled; +}; + +/** + * Cancels any pending timeout task for a SOCKS5 handler. + * + * This function safely cancels any scheduled timeout task to prevent it + * from executing after it's no longer needed (such as when a connection + * is successfully established or when shutting down). + * + * @param handler The SOCKS5 channel handler + */ +static void s_cancel_timeout_task(struct aws_socks5_channel_handler *handler) { + if (!handler) { + return; + } + + /* Only mark as not scheduled so the handler can check this when it runs */ + if (handler->timeout_task_scheduled) { + handler->timeout_task_scheduled = false; + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Marked SOCKS5 timeout task as canceled", + (void *)handler); + } +} + +/** + * Helper function for logging and handling state transitions in the SOCKS5 handler. + * + * This function manages state transitions with proper logging and error handling. + * It ensures that error codes are recorded when transitioning to an error state + * and provides detailed logging about state changes for debugging. + * + * @param handler The SOCKS5 channel handler + * @param new_state The state to transition to + * @param error_code Error code if transitioning due to an error, 0 otherwise + */ +static void s_transition_state( + struct aws_socks5_channel_handler *handler, + enum aws_socks5_channel_state new_state, + int error_code) { + + if (!handler) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_transition_state called with NULL handler"); + return; + } + + /* Store old state before modifying anything */ + enum aws_socks5_channel_state old_state = handler->channel_state; + + /* Validate state transition - certain transitions aren't allowed */ + if (old_state == AWS_SOCKS5_CHANNEL_STATE_ESTABLISHED && + new_state != AWS_SOCKS5_CHANNEL_STATE_ERROR) { + AWS_LOGF_WARN( + AWS_LS_IO_SOCKS5, + "id=%p: Invalid state transition attempted: %s -> %s (only ERROR state allowed from ESTABLISHED)", + (void *)handler, + s_socks5_channel_state_to_string(old_state), + s_socks5_channel_state_to_string(new_state)); + return; + } + + if (old_state == AWS_SOCKS5_CHANNEL_STATE_ERROR) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: State transition from ERROR state ignored: %s -> %s", + (void *)handler, + s_socks5_channel_state_to_string(old_state), + s_socks5_channel_state_to_string(new_state)); + return; + } + + /* Get state names for logging */ + const char *old_state_name = s_socks5_channel_state_to_string(old_state); + const char *new_state_name = s_socks5_channel_state_to_string(new_state); + + /* Set new state */ + handler->channel_state = new_state; + + /* If transitioning to error state, record the error code */ + if (new_state == AWS_SOCKS5_CHANNEL_STATE_ERROR && error_code) { + handler->error_code = error_code; + } + + /* Log the state transition */ + if (old_state != new_state) { + uint64_t now = 0; + if (handler->slot && handler->slot->channel) { + /* Get current time for performance tracking if possible */ + aws_channel_current_clock_time(handler->slot->channel, &now); + } + + if (new_state == AWS_SOCKS5_CHANNEL_STATE_ERROR) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: State transition: %s -> %s with error %d (%s) at %" PRIu64 "ns", + (void *)handler, + old_state_name, + new_state_name, + error_code, + aws_error_str(error_code), + now); + } else { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: State transition: %s -> %s at %" PRIu64 "ns", + (void *)handler, + old_state_name, + new_state_name, + now); + + /* Add additional context based on the new state */ + if (new_state == AWS_SOCKS5_CHANNEL_STATE_ESTABLISHED) { + AWS_LOGF_INFO( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 connection established successfully", + (void *)handler); + + /* Cancel any timeout task when established */ + s_cancel_timeout_task(handler); + + } else if (new_state == AWS_SOCKS5_CHANNEL_STATE_GREETING) { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Beginning SOCKS5 handshake", + (void *)handler); + } + } + } +} + +/** + * Helper function to ensure a buffer has at least the specified capacity. + * If the buffer is NULL or not properly initialized, it initializes it. + * If the buffer doesn't have enough capacity, it reallocates. + * + * @return AWS_OP_SUCCESS on success, AWS_OP_ERR on failure + */ +static int s_ensure_buffer_capacity( + struct aws_byte_buf *buffer, + struct aws_allocator *allocator, + size_t needed_capacity) { + + /* Default minimum capacity for new buffers */ + const size_t DEFAULT_MIN_CAPACITY = 256; + + if (!buffer || !allocator) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* If buffer is not initialized or doesn't have enough capacity */ + if (buffer->buffer == NULL || buffer->capacity == 0 || buffer->capacity < needed_capacity) { + /* Clean up existing buffer if any */ + if (buffer->buffer != NULL) { + aws_byte_buf_clean_up(buffer); + } + + /* Calculate new capacity - at least double the needed capacity or DEFAULT_MIN_CAPACITY */ + size_t new_capacity = needed_capacity * 2; + if (new_capacity < DEFAULT_MIN_CAPACITY) { + new_capacity = DEFAULT_MIN_CAPACITY; + } + + /* Initialize with new capacity */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "Initializing buffer with capacity %zu (needed %zu)", + new_capacity, + needed_capacity); + + return aws_byte_buf_init(buffer, allocator, new_capacity); + } + + return AWS_OP_SUCCESS; +} + +/** + * Helper function to reset a buffer and ensure it has enough capacity. + * Useful before writing new data to a buffer. + * + * @return AWS_OP_SUCCESS on success, AWS_OP_ERR on failure + */ +static int s_reset_and_ensure_buffer( + struct aws_byte_buf *buffer, + struct aws_allocator *allocator, + size_t needed_capacity) { + + if (!buffer || !allocator) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* Reset the buffer but keep its memory */ + aws_byte_buf_reset(buffer, false); + + /* Ensure it has enough capacity */ + return s_ensure_buffer_capacity(buffer, allocator, needed_capacity); +} + +/** + * Cleanup and destroy function for the SOCKS5 channel handler. + * + * This function is responsible for properly cleaning up and releasing all resources + * associated with a SOCKS5 channel handler, including: + * - SOCKS5 protocol context (credentials, target info, etc.) + * - Internal buffers used for protocol messages + * - The handler structure itself + * + * It performs thorough null-checking to handle partially initialized handlers safely. + * Any sensitive data (like credentials) is zeroed out before memory is released. + */ +static void s_socks5_handler_destroy(struct aws_channel_handler *handler) { + AWS_LOGF_TRACE(AWS_LS_IO_SOCKS5, "id=%p: Destroying SOCKS5 channel handler", (void *)handler); + + if (handler == NULL) { + AWS_LOGF_DEBUG(AWS_LS_IO_SOCKS5, "id=static: s_socks5_handler_destroy - NULL handler, nothing to do"); + return; + } + + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + if (socks5_handler == NULL) { + AWS_LOGF_DEBUG(AWS_LS_IO_SOCKS5, "id=%p: s_socks5_handler_destroy - NULL implementation", (void *)handler); + return; + } + + /* Save allocator before cleaning up (we'll need it for final memory release) */ + struct aws_allocator *allocator = socks5_handler->allocator; + + if (!allocator) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=%p: s_socks5_handler_destroy - NULL allocator, memory leak likely", (void *)handler); + return; + } + + AWS_LOGF_DEBUG(AWS_LS_IO_SOCKS5, "id=%p: Cleaning up SOCKS5 context and buffers", (void *)handler); + + /* Clean up the SOCKS5 context */ + aws_socks5_context_clean_up(&socks5_handler->context); + + /* Clean up buffers (safely handles non-initialized buffers) */ + if (socks5_handler->send_buffer.buffer != NULL) { + aws_byte_buf_clean_up(&socks5_handler->send_buffer); + } + + if (socks5_handler->read_buffer.buffer != NULL) { + aws_byte_buf_clean_up(&socks5_handler->read_buffer); + } + + /* Clear any sensitive data before releasing memory */ + AWS_ZERO_STRUCT(*socks5_handler); + + /* Release the handler memory */ + AWS_LOGF_DEBUG(AWS_LS_IO_SOCKS5, "id=%p: Releasing SOCKS5 handler memory", (void *)handler); + aws_mem_release(allocator, socks5_handler); +} + +/* Forward declarations of helper functions */ +static void s_process_read_message( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_io_message *message); + +static void s_handle_timeout(struct aws_channel_task *task, void *arg, enum aws_task_status status); + +static void s_forward_pending_data_task(struct aws_channel_task *task, void *arg, enum aws_task_status status); + + +/** + * Safely invokes the setup callback, ensuring it's only called once. + * + * Callbacks are invoked safely by: + * 1. Storing callback references locally before nulling them + * 2. Performing single-operation checks to prevent race conditions + * 3. Using proper error handling for all edge cases + * + * @param handler The SOCKS5 channel handler + * @param error_code Error code to pass to the callback (AWS_OP_SUCCESS for success) + */ +static void s_invoke_setup_callback_safely( + struct aws_socks5_channel_handler *handler, + int error_code) { + + /* Safety checks */ + if (!handler) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: s_invoke_setup_callback_safely - NULL handler"); + return; + } + + /* Store callback and data locally before clearing */ + aws_channel_on_setup_completed_fn *callback = handler->on_setup_completed; + void *user_data = handler->user_data; + + /* Clear callback first to prevent double-invocation in any subsequent calls */ + handler->on_setup_completed = NULL; + + /* Only proceed if we had a valid callback */ + if (!callback) { + AWS_LOGF_TRACE(AWS_LS_IO_SOCKS5, "id=%p: No callback to invoke (already called or never set)", (void *)handler); + return; + } + + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Invoking setup callback with error_code=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + /* Determine channel to use (might be NULL if handler isn't properly connected) */ + struct aws_channel *channel = NULL; + + if (handler->slot && handler->slot->channel) { + channel = handler->slot->channel; + } else if (error_code == AWS_OP_SUCCESS) { + /* If we're reporting success but don't have a channel, that's an error */ + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Attempted to signal success without valid channel", + (void *)handler); + error_code = AWS_ERROR_INVALID_STATE; + } + + /* Invoke the stored callback with appropriate parameters */ + callback(channel, error_code, user_data); +} + +/** + * Processes incoming messages from the channel. + * + * This function handles all incoming data based on the current state of the SOCKS5 handler: + * 1. In ESTABLISHED state: forwards messages upstream (transparent proxy mode) + * 2. In ERROR state: drops messages and logs errors + * 3. During handshake states: processes protocol messages for the SOCKS5 handshake + * + * The function is a critical part of the channel's read path, determining whether + * messages are processed for SOCKS5 protocol handling or forwarded to the application. + * + * @param handler The SOCKS5 channel handler + * @param slot The channel slot this handler belongs to + * @param message The incoming message to process + * @return AWS_OP_SUCCESS on successful handling (even if message is consumed) + * AWS_OP_ERR on error (with aws_last_error set) + */ +static int s_socks5_handler_process_read_message( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_io_message *message) { + + /* Validate input parameters */ + if (!handler || !slot || !message) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_socks5_handler_process_read_message - Invalid arguments: handler=%p, slot=%p, message=%p", + (void *)handler, + (void *)slot, + (void *)message); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + + if (!socks5_handler) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: s_socks5_handler_process_read_message - NULL implementation", + (void *)handler); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* If we're in established state, pass the message up the channel */ + if (socks5_handler->channel_state == AWS_SOCKS5_CHANNEL_STATE_ESTABLISHED) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Connection established, forwarding message of size %zu", + (void *)handler, + message->message_data.len); + + /* Forward the message to the next handler in the read direction */ + int result = aws_channel_slot_send_message(slot, message, AWS_CHANNEL_DIR_READ); + if (result != AWS_OP_SUCCESS) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to forward message upstream, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + } + return result; + } + + /* If we're in error state, drop the message */ + if (socks5_handler->channel_state == AWS_SOCKS5_CHANNEL_STATE_ERROR) { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: In error state, dropping incoming message of size %zu", + (void *)handler, + message->message_data.len); + + /* Release the message since we're not forwarding it */ + aws_mem_release(message->allocator, message); + return AWS_OP_SUCCESS; + } + + /* We're in handshake state, check if we should process the message */ + if (socks5_handler->process_incoming_data) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Processing incoming data for SOCKS5 handshake in state %d", + (void *)handler, + socks5_handler->channel_state); + + /* Process the message using our internal handler */ + s_process_read_message(handler, slot, message); + return AWS_OP_SUCCESS; + } + + /* We're not processing data (unusual case), log and drop the message */ + AWS_LOGF_WARN( + AWS_LS_IO_SOCKS5, + "id=%p: Not processing incoming data (flag not set) in SOCKS5 state %d, dropping message of size %zu", + (void *)handler, + socks5_handler->channel_state, + message->message_data.len); + + aws_mem_release(message->allocator, message); + return AWS_OP_SUCCESS; +} + +/** + * Processes outgoing messages to the channel. + * + * This function handles all outgoing data based on the current state of the SOCKS5 handler: + * 1. In ESTABLISHED state: forwards messages downstream (transparent proxy mode) + * 2. During handshake or ERROR states: blocks application data from being sent + * until the SOCKS5 connection is fully established + * + * This ensures that application data isn't sent over the connection until the + * SOCKS5 handshake is complete and the tunnel is established. + * + * @param handler The SOCKS5 channel handler + * @param slot The channel slot this handler belongs to + * @param message The outgoing message to process + * @return AWS_OP_SUCCESS on successful handling (even if message is dropped) + * AWS_OP_ERR on error (with aws_last_error set) + */ +static int s_socks5_handler_process_write_message( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_io_message *message) { + + /* Validate input parameters */ + if (!handler || !slot || !message) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_socks5_handler_process_write_message - Invalid arguments: handler=%p, slot=%p, message=%p", + (void *)handler, + (void *)slot, + (void *)message); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + + if (!socks5_handler) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: s_socks5_handler_process_write_message - NULL implementation", + (void *)handler); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* If we're in established state, pass the message down the channel */ + if (socks5_handler->channel_state == AWS_SOCKS5_CHANNEL_STATE_ESTABLISHED) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Connection established, forwarding outgoing message of size %zu", + (void *)handler, + message->message_data.len); + + /* Forward the message to the next handler in the write direction */ + int result = aws_channel_slot_send_message(slot, message, AWS_CHANNEL_DIR_WRITE); + if (result != AWS_OP_SUCCESS) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to forward outgoing message, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + } + return result; + } + + /* If we're in error state or still in handshake, drop application data */ + AWS_LOGF_WARN( + AWS_LS_IO_SOCKS5, + "id=%p: Not in established state (current state: %d), dropping outgoing message of size %zu", + (void *)handler, + socks5_handler->channel_state, + message->message_data.len); + + /* Release the message since we're not forwarding it */ + aws_mem_release(message->allocator, message); + return AWS_OP_SUCCESS; +} + +/** + * Handles window updates for flow control in the SOCKS5 channel handler. + * + * When the handler receives a window update (meaning more data can be received), + * it propagates this update to the adjacent handler to maintain proper flow control + * throughout the channel. This function is part of the AWS CRT channel's + * backpressure mechanism. + * + * @param handler The SOCKS5 channel handler + * @param slot The channel slot this handler belongs to + * @param window_update The number of bytes to increase the window by + * @return AWS_OP_SUCCESS on successful handling + * AWS_OP_ERR on error (with aws_last_error set) + */ +static int s_socks5_handler_initial_window_update( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + size_t window_update) { + + /* Validate input parameters */ + if (!handler || !slot) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_socks5_handler_initial_window_update - Invalid arguments: handler=%p, slot=%p", + (void *)handler, + (void *)slot); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* Log the window update */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Window update of %zu bytes", + (void *)handler, + window_update); + + /* Propagate the window update to the adjacent handler */ + if (slot->adj_right) { + aws_channel_slot_increment_read_window(slot->adj_right, window_update); + } else { + /* Not having an adjacent slot is normal during setup/teardown */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: No adjacent slot for window update of %zu bytes", + (void *)handler, + window_update); + } + + return AWS_OP_SUCCESS; +} + +/** + * Handles channel shutdown events for the SOCKS5 channel handler. + * + * This function is called during channel shutdown to: + * 1. Cancel any pending timeouts + * 2. Record error information if shutdown occurs during handshake + * 3. Safely invoke any pending callbacks + * 4. Propagate the shutdown signal to adjacent handlers + * + * Proper handling of shutdown is critical for clean resource cleanup and + * appropriate error propagation throughout the channel stack. + * + * @param handler The SOCKS5 channel handler + * @param slot The channel slot this handler belongs to + * @param dir The direction of shutdown (read or write) + * @param error_code The error that caused shutdown, or 0 for normal shutdown + * @param free_scarce_resources_immediately Whether to free resources immediately + * @return AWS_OP_SUCCESS on successful shutdown handling + * AWS_OP_ERR on error (with aws_last_error set) + */ +static int s_socks5_handler_shutdown( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + enum aws_channel_direction dir, + int error_code, + bool free_scarce_resources_immediately) { + + /* Validate input parameters */ + if (!handler || !slot) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_socks5_handler_shutdown - Invalid arguments: handler=%p, slot=%p", + (void *)handler, + (void *)slot); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + + if (!socks5_handler) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: s_socks5_handler_shutdown - NULL implementation", + (void *)handler); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* Log shutdown information */ + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Shutting down SOCKS5 handler, direction=%s, error_code=%d, free_resources=%d", + (void *)handler, + dir == AWS_CHANNEL_DIR_READ ? "READ" : "WRITE", + error_code, + free_scarce_resources_immediately); + + /* For read direction with no error, use socket closed as the reason */ + if (dir == AWS_CHANNEL_DIR_READ && !error_code) { + error_code = AWS_IO_SOCKET_CLOSED; + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Read direction shutdown with no error, using AWS_IO_SOCKET_CLOSED", + (void *)handler); + } + + /* Properly cancel any pending timeout task */ + s_cancel_timeout_task(socks5_handler); + + /* Handle shutdown during handshake */ + if (socks5_handler->channel_state != AWS_SOCKS5_CHANNEL_STATE_ESTABLISHED && + socks5_handler->channel_state != AWS_SOCKS5_CHANNEL_STATE_ERROR && + error_code) { + /* If we're not established yet, transition to error state */ + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Shutdown during handshake (state %d), error_code=%d (%s)", + (void *)handler, + socks5_handler->channel_state, + error_code, + aws_error_str(error_code)); + + /* Record the error and update state */ + socks5_handler->error_code = error_code; + s_transition_state(socks5_handler, AWS_SOCKS5_CHANNEL_STATE_ERROR, error_code); + + /* If we have a pending callback, invoke it with the error */ + if (socks5_handler->on_setup_completed != NULL) { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Invoking pending callback with error during shutdown", + (void *)handler); + s_invoke_setup_callback_safely(socks5_handler, error_code); + } + } + + /* Propagate shutdown to adjacent handlers */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Propagating shutdown to adjacent handlers", + (void *)handler); + + aws_channel_slot_on_handler_shutdown_complete( + slot, dir, error_code, free_scarce_resources_immediately); + + return AWS_OP_SUCCESS; +} + +/** + * Returns the initial window size for the SOCKS5 channel handler. + * + * This function delegates to the next handler in the chain to ensure + * consistent window sizing throughout the channel. If there's no + * next handler available, it returns a sensible default value. + * + * The window size is critical for flow control in the channel architecture, + * controlling how much data a handler is willing to receive before needing + * acknowledgment. + */ +static size_t s_socks5_handler_get_initial_window_size(struct aws_channel_handler *handler) { + /* Default window size if we can't delegate */ + const size_t DEFAULT_WINDOW_SIZE = 16 * 1024; /* 16 KB is a reasonable default */ + + if (!handler) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_socks5_handler_get_initial_window_size - NULL handler"); + return DEFAULT_WINDOW_SIZE; + } + + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + + /* Safety check for the handler implementation */ + if (!socks5_handler) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: s_socks5_handler_get_initial_window_size - NULL implementation", + (void *)handler); + return DEFAULT_WINDOW_SIZE; + } + + /* Safety check for the slot */ + if (!socks5_handler->slot) { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: s_socks5_handler_get_initial_window_size - No slot assigned yet", + (void *)handler); + return DEFAULT_WINDOW_SIZE; + } + + struct aws_channel_slot *adj_slot = socks5_handler->slot->adj_right; + + /* Check for adjacent slot and handler */ + if (adj_slot && adj_slot->handler && adj_slot->handler->vtable && + adj_slot->handler->vtable->initial_window_size) { + + /* Delegate to the next handler */ + size_t next_window_size = adj_slot->handler->vtable->initial_window_size(adj_slot->handler); + + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Using adjacent handler's window size: %zu", + (void *)handler, + next_window_size); + + return next_window_size; + } + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: No adjacent handler with window size, using default: %zu", + (void *)handler, + DEFAULT_WINDOW_SIZE); + + /* Return default window size if we can't delegate */ + return DEFAULT_WINDOW_SIZE; +} + +/** + * Returns the message overhead that the SOCKS5 handler adds to each message. + * + * Once the SOCKS5 handshake is complete, the handler becomes a simple pass-through + * that doesn't add any additional overhead to messages. Therefore, this function + * returns 0 to indicate no additional memory allocation is needed for messages + * passing through this handler. + * + * During the handshake phase, the handler processes protocol-specific messages + * internally and doesn't add overhead to application messages. + */ +static size_t s_socks5_handler_message_overhead(struct aws_channel_handler *handler) { + (void)handler; + /* Return 0 since SOCKS5 doesn't add any overhead to messages passing through */ + return 0; +} + +static struct aws_channel_handler_vtable s_socks5_handler_vtable = { + .destroy = s_socks5_handler_destroy, + .process_read_message = s_socks5_handler_process_read_message, + .process_write_message = s_socks5_handler_process_write_message, + .increment_read_window = s_socks5_handler_initial_window_update, + .shutdown = s_socks5_handler_shutdown, + .initial_window_size = s_socks5_handler_get_initial_window_size, + .message_overhead = s_socks5_handler_message_overhead, +}; + +/* Send a SOCKS5 protocol message down the channel */ +static int s_send_socks5_message( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_byte_buf *buffer) { + + if (!handler || !slot || !buffer || buffer->len == 0) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (!slot->channel || !slot->adj_left) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_io_message *message = aws_channel_acquire_message_from_pool( + slot->channel, + AWS_IO_MESSAGE_APPLICATION_DATA, + buffer->len); if (!message) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to acquire message from pool, size=%zu", + (void *)handler, + buffer->len); + return AWS_OP_ERR; + } + + /* Copy the buffer content into the message */ + if (!aws_byte_buf_write( + &message->message_data, buffer->buffer, buffer->len)) { + aws_mem_release(message->allocator, message); + return AWS_OP_ERR; + } + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Sending SOCKS5 message, size=%zu", + (void *)handler, + message->message_data.len); + + + /* Send the message down the channel */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=static: s_send_socks5_message - Sending message of size %zu down channel in slot %p", + message->message_data.len, (void *)slot); + if (aws_channel_slot_send_message(slot, message, AWS_CHANNEL_DIR_WRITE)) { + aws_mem_release(message->allocator, message); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_send_socks5_message - Failed to send message down channel"); + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +/* Invoke the user's setup completed callback */ +/** + * Helper function to transition to error state and invoke callback + * + * This centralizes error handling for the SOCKS5 handler by: + * 1. Transitioning to error state with proper logging + * 2. Recording the error code + * 3. Invoking the setup callback with the error + * + */ +static void s_transition_to_error( + struct aws_socks5_channel_handler *handler, + int error_code) { + + if (!handler) { + return; + } + + /* Update the state */ + s_transition_state(handler, AWS_SOCKS5_CHANNEL_STATE_ERROR, error_code); + + /* Invoke the callback with the error */ + s_invoke_setup_callback_safely(handler, error_code); +} + +/* Start the connection timeout timer */ +/** + * Schedules a timeout task for the SOCKS5 handshake. + * + * This ensures that the SOCKS5 handshake doesn't hang indefinitely + * if the proxy server doesn't respond or if there are network issues. + * The timeout is based on the connect_timeout_ns value specified in + * the handler's configuration. + * + * @param handler The SOCKS5 channel handler + */ +static void s_schedule_timeout(struct aws_socks5_channel_handler *handler) { + /* Validate handler and channel availability */ + if (!handler) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_schedule_timeout called with NULL handler"); + return; + } + + if (!handler->slot || !handler->slot->channel) { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Cannot schedule timeout - missing slot or channel", + (void *)handler); + return; + } + + /* Skip if timeout disabled or already scheduled */ + if (handler->connect_timeout_ns == 0) { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 connection timeout disabled (connect_timeout_ns=0)", + (void *)handler); + return; + } + + if (handler->timeout_task_scheduled) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 timeout task already scheduled", + (void *)handler); + return; + } + + /* Get current time for scheduling */ + uint64_t now = 0; + if (aws_channel_current_clock_time(handler->slot->channel, &now)) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to get current time for timeout scheduling", + (void *)handler); + return; + } + + /* Calculate absolute timeout time */ + uint64_t timeout_time = now + handler->connect_timeout_ns; + + /* Log timeout details in milliseconds for readability */ + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Scheduling SOCKS5 timeout for %" PRIu64 " ms from now (current state: %s)", + (void *)handler, + handler->connect_timeout_ns / 1000000, /* Convert ns to ms for more readable logs */ + s_socks5_channel_state_to_string(handler->channel_state)); + + /* Initialize and schedule the timeout task */ + aws_channel_task_init( + &handler->timeout_task, + s_handle_timeout, + handler, + "socks5_channel_connect_timeout"); + + aws_channel_schedule_task_future( + handler->slot->channel, + &handler->timeout_task, + timeout_time); + + handler->timeout_task_scheduled = true; + + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 timeout task scheduled for absolute time %" PRIu64, + (void *)handler, + timeout_time); +} + +/** + * Task function for safely forwarding data after the SOCKS5 handshake completes. + * + * This is used to ensure that application data received alongside the final SOCKS5 + * response is properly forwarded to the application after all handlers have been + * properly installed in the channel. + * + * @param task The channel task + * @param arg Context containing the slot and message to forward and allocator + * @param status Task status (cancelled or running) + */ +static void s_forward_pending_data_task(struct aws_channel_task *task, void *arg, enum aws_task_status status) { + /* Check if task was cancelled */ + if (status == AWS_TASK_STATUS_CANCELED) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=static: s_forward_pending_data_task - Task was cancelled"); + return; + } + + /* Check for valid context */ + if (!arg) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_forward_pending_data_task - NULL context"); + return; + } + + /* Extract context */ + struct { + struct aws_channel_slot *slot; + struct aws_io_message *message; + struct aws_allocator *allocator; + } *forward_ctx = arg; + + /* Free the task immediately using the allocator from our context */ + struct aws_allocator *allocator = forward_ctx->allocator; + if (task && allocator) { + aws_mem_release(allocator, task); + } + + /* Ensure we have valid slot and message */ + if (!forward_ctx->slot || !forward_ctx->message) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_forward_pending_data_task - Invalid slot or message"); + + /* Free the context using the allocator we captured */ + if (allocator) { + aws_mem_release(allocator, forward_ctx); + } + return; + } + + /* Extract local copies for safety */ + struct aws_channel_slot *slot = forward_ctx->slot; + struct aws_io_message *message = forward_ctx->message; + + /* Ensure channel is still valid */ + if (!slot->channel) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_forward_pending_data_task - Channel no longer valid"); + + /* Clean up the message and context */ + aws_mem_release(message->allocator, message); + if (allocator) { + aws_mem_release(allocator, forward_ctx); + } + return; + } + + /* Forward the message up the channel */ + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Forwarding %zu bytes of application data after SOCKS5 handshake", + (void *)slot, + message->message_data.len); + + if (aws_channel_slot_send_message(slot, message, AWS_CHANNEL_DIR_READ)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to forward pending data, error=%d (%s)", + (void *)slot, + error_code, + aws_error_str(error_code)); + + /* Clean up the message if send failed */ + aws_mem_release(message->allocator, message); + } + + /* Clean up our context */ + if (allocator) { + aws_mem_release(allocator, forward_ctx); + } +} + +/** + * Handles the SOCKS5 connection timeout event. + * + * This function is called when the timeout task executes, indicating that + * the SOCKS5 handshake has taken too long. It fails the connection with + * a timeout error and invokes the setup callback to notify higher layers. + * + * This implementation includes better thread safety with explicit state checks + * to ensure the timeout isn't processed if it was cancelled or the state changed. + * + * @param task The timeout task + * @param arg Handler pointer passed as context (cast to aws_socks5_channel_handler) + * @param status The task status (may be cancelled) + */ +static void s_handle_timeout(struct aws_channel_task *task, void *arg, enum aws_task_status status) { + (void)task; + + /* Check if task was cancelled or has invalid arg */ + if (status == AWS_TASK_STATUS_CANCELED) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=static: s_handle_timeout - Task was cancelled"); + return; + } + + if (!arg) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: s_handle_timeout - NULL handler argument"); + return; + } + + struct aws_socks5_channel_handler *handler = arg; + + /* Critical atomic check - don't run if task was cancelled via flag */ + if (!handler->timeout_task_scheduled) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Timeout task cancelled before execution", + (void *)handler); + return; + } + + /* Clear the scheduled flag to prevent double execution */ + handler->timeout_task_scheduled = false; + + /* Log the timeout execution with handler state */ + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 timeout handler executed in state %s", + (void *)handler, + s_socks5_channel_state_to_string(handler->channel_state)); + + /* Check if timeout is still relevant */ + if (handler->channel_state == AWS_SOCKS5_CHANNEL_STATE_ESTABLISHED) { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 timeout ignored - connection already established", + (void *)handler); + return; + } + + if (handler->channel_state == AWS_SOCKS5_CHANNEL_STATE_ERROR) { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 timeout ignored - already in error state with code %d (%s)", + (void *)handler, + handler->error_code, + aws_error_str(handler->error_code)); + return; + } + + /* Connection timed out - log details of the current state */ + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 connection timed out after %" PRIu64 " ms in state %s", + (void *)handler, + handler->connect_timeout_ns / 1000000, /* Convert to ms for readable logs */ + s_socks5_channel_state_to_string(handler->channel_state)); + + /* Use transition_state for consistent state management and logging */ + s_transition_state(handler, AWS_SOCKS5_CHANNEL_STATE_ERROR, AWS_IO_SOCKET_TIMEOUT); + + /* Invoke the callback with timeout error */ + s_invoke_setup_callback_safely(handler, AWS_IO_SOCKET_TIMEOUT); +} + +/* Initialize the SOCKS5 handshake */ +static int s_start_socks5_handshake(struct aws_channel_handler *handler, struct aws_channel_slot *slot) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=static: s_start_socks5_handshake called with handler %p, slot %p", + (void*)handler, (void*)slot); + + if (!handler) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: s_start_socks5_handshake - NULL handler!"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (!slot) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: s_start_socks5_handshake - NULL slot! (This is a programming error)"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (!slot->channel) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: s_start_socks5_handshake - Slot has no channel! (This is a programming error)"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + if (!socks5_handler) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: s_start_socks5_handshake - Handler has no impl! (This is a programming error)"); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* Store the slot for future reference */ + handler->slot = slot; + socks5_handler->slot = slot; + + /* Validate target host before proceeding */ + struct aws_string *ctx_target_host = socks5_handler->context.endpoint_host; + + + if (ctx_target_host == NULL) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=%p: Cannot start handshake - SOCKS5 target host buffer is NULL!", (void *)handler); + + int error_code = AWS_ERROR_INVALID_STATE; + socks5_handler->channel_state = AWS_SOCKS5_CHANNEL_STATE_ERROR; + socks5_handler->error_code = error_code; + s_invoke_setup_callback_safely(socks5_handler, error_code); + return aws_raise_error(error_code); + } + + if (ctx_target_host->len == 0) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=%p: Cannot start handshake - SOCKS5 target host length is 0!", (void *)handler); + + int error_code = AWS_ERROR_INVALID_STATE; + socks5_handler->channel_state = AWS_SOCKS5_CHANNEL_STATE_ERROR; + socks5_handler->error_code = error_code; + s_invoke_setup_callback_safely(socks5_handler, error_code); + return aws_raise_error(error_code); + } + + /* Debug target host info */ + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 target host: '%.*s', port: %d", + (void *)handler, + (int)ctx_target_host->len, + (const char *)ctx_target_host->bytes, + socks5_handler->context.endpoint_port); + + /* Clear the buffer for sending the greeting */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Resetting send buffer before greeting (buffer=%p, capacity=%zu, len=%zu)", + (void *)handler, + (void*)socks5_handler->send_buffer.buffer, + socks5_handler->send_buffer.capacity, + socks5_handler->send_buffer.len); + aws_byte_buf_reset(&socks5_handler->send_buffer, false); + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: After reset: send buffer state (buffer=%p, capacity=%zu, len=%zu)", + (void *)handler, + (void*)socks5_handler->send_buffer.buffer, + socks5_handler->send_buffer.capacity, + socks5_handler->send_buffer.len); + + /* Start the timeout timer */ + s_schedule_timeout(socks5_handler); + + /* Start processing incoming data */ + socks5_handler->process_incoming_data = true; + + /* Start with greeting state */ + AWS_LOGF_TRACE(AWS_LS_IO_SOCKS5, "id=%p: Starting handshake by transitioning to GREETING state", (void *)handler); + s_transition_state(socks5_handler, AWS_SOCKS5_CHANNEL_STATE_GREETING, 0); + + /* Write SOCKS5 greeting message */ + if (aws_socks5_write_greeting(&socks5_handler->context, &socks5_handler->send_buffer)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to write SOCKS5 greeting, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + s_transition_to_error(socks5_handler, error_code); + return AWS_OP_ERR; + } + + /* Debug the greeting bytes */ + + /* Send the greeting message */ + if (s_send_socks5_message(handler, slot, &socks5_handler->send_buffer)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to send SOCKS5 greeting, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + s_transition_to_error(socks5_handler, error_code); + return AWS_OP_ERR; + } + + AWS_LOGF_INFO(AWS_LS_IO_SOCKS5, "id=%p: Started SOCKS5 handshake", (void *)handler); + return AWS_OP_SUCCESS; +} + +/** + * Processes the SOCKS5 greeting response from the proxy server. + * + * This function handles the server's response to our initial greeting, + * which includes the authentication method selected by the server. + * Based on this response, we either proceed to authentication or + * directly to the connect phase. + * + * @param handler The SOCKS5 channel handler + * @param slot The channel slot this handler belongs to + * @param data Cursor pointing to the response data + * @return AWS_OP_SUCCESS if processing succeeded, AWS_OP_ERR otherwise + */ +static int s_process_greeting_response( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_byte_cursor *data) { + + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + + /* Log greeting response data for debugging (limited bytes for security) */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Processing SOCKS5 greeting response of size %zu bytes", + (void *)handler, + data->len); + + /* Process the greeting response */ + uint64_t start_time = 0; + if (socks5_handler->slot && socks5_handler->slot->channel) { + aws_channel_current_clock_time(socks5_handler->slot->channel, &start_time); + } + + if (aws_socks5_read_greeting_response(&socks5_handler->context, data)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to process SOCKS5 greeting response, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + s_transition_to_error(socks5_handler, error_code); + return AWS_OP_ERR; + } + + /* Log success and selected authentication method */ + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 greeting response processed successfully, selected auth method: %d", + (void *)handler, + socks5_handler->context.selected_auth); + + uint64_t end_time = 0; + if (socks5_handler->slot && socks5_handler->slot->channel) { + aws_channel_current_clock_time(socks5_handler->slot->channel, &end_time); + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Greeting response processing took %" PRIu64 "ns", + (void *)handler, + end_time - start_time); + } + + /* Reset and ensure buffer capacity for next message */ + if (s_reset_and_ensure_buffer(&socks5_handler->send_buffer, socks5_handler->allocator, 256)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to reset send buffer, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + s_transition_to_error(socks5_handler, error_code); + return AWS_OP_ERR; + } + + /* Check if authentication is needed */ + if (socks5_handler->context.selected_auth == AWS_SOCKS5_AUTH_NONE) { + /* No auth needed, proceed to connect phase */ + s_transition_state(socks5_handler, AWS_SOCKS5_CHANNEL_STATE_CONNECT, 0); + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: No authentication needed, proceeding to connect phase", + (void *)handler); + + /* Send connect request */ + if (aws_socks5_write_connect_request(&socks5_handler->context, &socks5_handler->send_buffer)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to write SOCKS5 connect request, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + s_transition_to_error(socks5_handler, error_code); + return AWS_OP_ERR; + } + + /* Send the connect request message */ + if (s_send_socks5_message(handler, slot, &socks5_handler->send_buffer)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to send SOCKS5 connect request, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + s_transition_to_error(socks5_handler, error_code); + return AWS_OP_ERR; + } + } else { + /* Authentication needed */ + s_transition_state(socks5_handler, AWS_SOCKS5_CHANNEL_STATE_AUTH, 0); + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Authentication required, sending auth request", + (void *)handler); + + /* Prepare auth request */ + if (aws_socks5_write_auth_request(&socks5_handler->context, &socks5_handler->send_buffer)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to write SOCKS5 auth request, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + socks5_handler->channel_state = AWS_SOCKS5_CHANNEL_STATE_ERROR; + socks5_handler->error_code = error_code; + s_invoke_setup_callback_safely(socks5_handler, error_code); + return AWS_OP_ERR; + } + + /* Send the auth request message */ + if (s_send_socks5_message(handler, slot, &socks5_handler->send_buffer)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to send SOCKS5 auth request, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + s_transition_to_error(socks5_handler, error_code); + return AWS_OP_ERR; + } + } + + return AWS_OP_SUCCESS; +} + +/** + * Processes the SOCKS5 authentication response from the proxy server. + * + * After sending authentication credentials to the proxy server, this function + * handles the server's response. If authentication succeeds, we proceed to + * the connect phase to establish the connection to the target server. + * + * @param handler The SOCKS5 channel handler + * @param slot The channel slot this handler belongs to + * @param data Cursor pointing to the response data + * @return AWS_OP_SUCCESS if processing succeeded, AWS_OP_ERR otherwise + */ +static int s_process_auth_response( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_byte_cursor *data) { + + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + + /* Log authentication response data (limited bytes for security) */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Processing SOCKS5 authentication response of size %zu bytes", + (void *)handler, + data->len); + + /* Process the authentication response */ + uint64_t start_time = 0; + if (socks5_handler->slot && socks5_handler->slot->channel) { + aws_channel_current_clock_time(socks5_handler->slot->channel, &start_time); + } + + if (aws_socks5_read_auth_response(&socks5_handler->context, data)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 authentication failed, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + /* Use transition_state for consistency and better logging */ + s_transition_state(socks5_handler, AWS_SOCKS5_CHANNEL_STATE_ERROR, error_code); + s_invoke_setup_callback_safely(socks5_handler, error_code); + return AWS_OP_ERR; + } + + /* Log successful authentication */ + AWS_LOGF_INFO( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 authentication successful", + (void *)handler); + + uint64_t end_time = 0; + if (socks5_handler->slot && socks5_handler->slot->channel) { + aws_channel_current_clock_time(socks5_handler->slot->channel, &end_time); + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Auth response processing took %" PRIu64 "ns", + (void *)handler, + end_time - start_time); + } + + /* Clear the buffer for connect request */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Resetting send buffer after auth response (buffer=%p, capacity=%zu, len=%zu)", + (void *)handler, + (void*)socks5_handler->send_buffer.buffer, + socks5_handler->send_buffer.capacity, + socks5_handler->send_buffer.len); + aws_byte_buf_reset(&socks5_handler->send_buffer, false); + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: After reset: send buffer state (buffer=%p, capacity=%zu, len=%zu)", + (void *)handler, + (void*)socks5_handler->send_buffer.buffer, + socks5_handler->send_buffer.capacity, + socks5_handler->send_buffer.len); + + /* Authentication successful, proceed to connect phase */ + s_transition_state(socks5_handler, AWS_SOCKS5_CHANNEL_STATE_CONNECT, 0); + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Authentication successful, proceeding to connect phase", + (void *)handler); + + /* Send connect request */ + if (aws_socks5_write_connect_request(&socks5_handler->context, &socks5_handler->send_buffer)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to write SOCKS5 connect request, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + socks5_handler->channel_state = AWS_SOCKS5_CHANNEL_STATE_ERROR; + socks5_handler->error_code = error_code; + s_invoke_setup_callback_safely(socks5_handler, error_code); + return AWS_OP_ERR; + } + + /* Send the connect request message */ + if (s_send_socks5_message(handler, slot, &socks5_handler->send_buffer)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to send SOCKS5 connect request, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + socks5_handler->channel_state = AWS_SOCKS5_CHANNEL_STATE_ERROR; + socks5_handler->error_code = error_code; + s_invoke_setup_callback_safely(socks5_handler, error_code); + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +/** + * Processes the SOCKS5 connection response from the proxy server. + * + * After requesting a connection to the target server, this function + * handles the proxy's response. If successful, it transitions the handler + * to ESTABLISHED state and invokes the setup callback to notify higher + * layers that the connection is ready for use. + * + * This is the final step in the SOCKS5 handshake process. + * + * @param handler The SOCKS5 channel handler + * @param slot The channel slot this handler belongs to + * @param data Cursor pointing to the response data + * @return AWS_OP_SUCCESS if processing succeeded, AWS_OP_ERR otherwise + */ +static int s_process_connect_response( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_byte_cursor *data) { + + (void)slot; + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + uint64_t start_time = 0; + + if (socks5_handler->slot && socks5_handler->slot->channel) { + aws_channel_current_clock_time(socks5_handler->slot->channel, &start_time); + } + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Processing SOCKS5 connect response of size %zu bytes", + (void *)handler, + data->len); + + /* Log buffer details for comprehensive diagnostics */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: CONNECT phase - read_buffer details (buffer=%p, capacity=%zu, len=%zu)", + (void *)handler, + (void*)socks5_handler->read_buffer.buffer, + socks5_handler->read_buffer.capacity, + socks5_handler->read_buffer.len); + + /* Validate response format before processing */ + if (data->len < 4) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 CONNECT response format invalid - too short (%zu bytes, minimum 4 required)", + (void *)handler, + data->len); + + /* Use transition_state for consistent error handling */ + s_transition_state(socks5_handler, AWS_SOCKS5_CHANNEL_STATE_ERROR, AWS_ERROR_INVALID_ARGUMENT); + s_invoke_setup_callback_safely(socks5_handler, AWS_ERROR_INVALID_ARGUMENT); + return AWS_OP_ERR; + } + + /* Log response version and status code for debugging */ + if (data->len >= 2) { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 CONNECT response - version=0x%02x, status=0x%02x", + (void *)handler, + data->ptr[0], + data->ptr[1]); + } + + /* Process the connection response */ + if (aws_socks5_read_connect_response(&socks5_handler->context, data)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 connection request failed, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + /* If we have status code information, log it for diagnostics */ + if (data->len >= 2) { + uint8_t status = data->ptr[1]; + const char* status_str = "Unknown"; + + /* Convert SOCKS5 status codes to readable strings */ + switch(status) { + case 0: status_str = "Success"; break; + case 1: status_str = "General failure"; break; + case 2: status_str = "Connection not allowed"; break; + case 3: status_str = "Network unreachable"; break; + case 4: status_str = "Host unreachable"; break; + case 5: status_str = "Connection refused"; break; + case 6: status_str = "TTL expired"; break; + case 7: status_str = "Command not supported"; break; + case 8: status_str = "Address type not supported"; break; + } + + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 server returned status code %d (%s)", + (void *)handler, + status, + status_str); + } + + /* Use transition_state for consistent error handling */ + s_transition_state(socks5_handler, AWS_SOCKS5_CHANNEL_STATE_ERROR, error_code); + s_invoke_setup_callback_safely(socks5_handler, error_code); + return AWS_OP_ERR; + } + + /* Log successful connection with target details if available */ + struct aws_string * ctx_target_host_log = + socks5_handler->context.endpoint_host; + AWS_LOGF_INFO( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 connection to target host '%.*s:%d' established successfully", + (void *)handler, + (int)ctx_target_host_log->len, + (const char *)ctx_target_host_log->bytes, + socks5_handler->context.endpoint_port); + + /* Calculate handshake duration for performance metrics */ + uint64_t end_time = 0; + if (socks5_handler->slot && socks5_handler->slot->channel) { + aws_channel_current_clock_time(socks5_handler->slot->channel, &end_time); + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 connect response processing took %" PRIu64 "ns", + (void *)handler, + end_time - start_time); + } + + /* Connection established, transition to established state */ + s_transition_state(socks5_handler, AWS_SOCKS5_CHANNEL_STATE_ESTABLISHED, 0); + + /* Cancel any pending timeout task */ + if (socks5_handler->timeout_task_scheduled) { + /* The task will remain in the event loop's task queue, but when it runs, + it will check if we're in ESTABLISHED state and do nothing */ + socks5_handler->timeout_task_scheduled = false; + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Connection established before timeout occurred, canceling timeout task", + (void *)handler); + } + + /* Debug handler setup callback and user data */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Callback %p, User data %p", + (void *)handler, + (void *)(uintptr_t)socks5_handler->on_setup_completed, + socks5_handler->user_data); + + /* Check for composite context in user_data */ + if (socks5_handler->user_data != NULL) { + struct { + struct aws_socks5_proxy_options *socks5_options; + void *original_user_data; + } *composite_ctx = socks5_handler->user_data; + + /* If this looks like our composite context, print details */ + if (composite_ctx->socks5_options != NULL && + composite_ctx->original_user_data != NULL) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Found composite context: socks5_options=%p, original_user_data=%p", + (void *)handler, + (void*)composite_ctx->socks5_options, + composite_ctx->original_user_data); + } + } + + /* Invoke the user callback with success */ + AWS_LOGF_TRACE(AWS_LS_IO_SOCKS5, "id=%p: Invoking setup callback with success status", (void *)handler); + s_invoke_setup_callback_safely(socks5_handler, AWS_OP_SUCCESS); + + return AWS_OP_SUCCESS; +} + +/* Process incoming data during handshake */ +static void s_process_read_message( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_io_message *message) { + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: s_process_read_message called with message size %zu", + (void *)handler, + message->message_data.len); + + /* Debug raw message data */ + if (message->message_data.len > 0) { + } + + if (!handler || !slot || !message) { + return; /* Nothing we can do without valid parameters */ + } + + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + + if (!socks5_handler) { + return; /* Can't process without a valid handler context */ + } + + /* Add the message data to our read buffer */ + struct aws_byte_cursor message_cursor = aws_byte_cursor_from_buf(&message->message_data); + + /* Check if the read buffer is in a valid state */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Read buffer state check - buffer=%p, capacity=%zu, len=%zu, allocator=%p", + (void *)handler, + (void*)socks5_handler->read_buffer.buffer, + socks5_handler->read_buffer.capacity, + socks5_handler->read_buffer.len, + (void*)socks5_handler->read_buffer.allocator); + + /* Fail immediately if the buffer is NULL */ + if (socks5_handler->read_buffer.buffer == NULL) { + + int error_code = AWS_ERROR_INVALID_STATE; + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: CRITICAL ERROR - read_buffer.buffer is NULL! Cannot append data", + (void *)handler); + + socks5_handler->channel_state = AWS_SOCKS5_CHANNEL_STATE_ERROR; + socks5_handler->error_code = error_code; + s_invoke_setup_callback_safely(socks5_handler, error_code); + aws_mem_release(message->allocator, message); + return; + } + + /* Ensure buffer has sufficient capacity */ + size_t needed_capacity = socks5_handler->read_buffer.len + message_cursor.len; + if (s_ensure_buffer_capacity(&socks5_handler->read_buffer, socks5_handler->allocator, needed_capacity)) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to ensure read buffer capacity", + (void *)handler); + return; + } + + if (aws_byte_buf_append( + &socks5_handler->read_buffer, + &message_cursor)) { + + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to append to read buffer, error=%d (%s)", + (void *)handler, + error_code, + aws_error_str(error_code)); + + socks5_handler->channel_state = AWS_SOCKS5_CHANNEL_STATE_ERROR; + socks5_handler->error_code = error_code; + s_invoke_setup_callback_safely(socks5_handler, error_code); + aws_mem_release(message->allocator, message); + return; + } + + /* We've consumed the message, so we can release it */ + aws_mem_release(message->allocator, message); + + /* Process the data based on the current state */ + struct aws_byte_cursor data = aws_byte_cursor_from_buf(&socks5_handler->read_buffer); + int result = AWS_OP_SUCCESS; + + switch (socks5_handler->channel_state) { + case AWS_SOCKS5_CHANNEL_STATE_GREETING: + if (data.len >= AWS_SOCKS5_GREETING_RESP_SIZE) { + result = s_process_greeting_response(handler, slot, &data); + + /* Consume the processed data using memmove instead of temp buffer */ + size_t remaining_size = socks5_handler->read_buffer.len - AWS_SOCKS5_GREETING_RESP_SIZE; + if (remaining_size > 0) { + /* Shift the remaining data to the beginning of the buffer */ + memmove(socks5_handler->read_buffer.buffer, + socks5_handler->read_buffer.buffer + AWS_SOCKS5_GREETING_RESP_SIZE, + remaining_size); + } + /* Update the buffer length */ + socks5_handler->read_buffer.len = remaining_size; + } + break; + + case AWS_SOCKS5_CHANNEL_STATE_AUTH: + if (data.len >= AWS_SOCKS5_AUTH_RESP_SIZE) { + result = s_process_auth_response(handler, slot, &data); + + /* Consume the processed data using memmove instead of temp buffer */ + size_t remaining_size = socks5_handler->read_buffer.len - AWS_SOCKS5_AUTH_RESP_SIZE; + if (remaining_size > 0) { + /* Shift the remaining data to the beginning of the buffer */ + memmove(socks5_handler->read_buffer.buffer, + socks5_handler->read_buffer.buffer + AWS_SOCKS5_AUTH_RESP_SIZE, + remaining_size); + } + /* Update the buffer length */ + socks5_handler->read_buffer.len = remaining_size; + + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: AUTH state - After memmove: read_buffer (buffer=%p, capacity=%zu, len=%zu)", + (void *)handler, + (void*)socks5_handler->read_buffer.buffer, + socks5_handler->read_buffer.capacity, + socks5_handler->read_buffer.len); + } + break; + + case AWS_SOCKS5_CHANNEL_STATE_CONNECT: + /* For connect response, we need to parse the first few bytes to determine size */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Processing CONNECT response, data length %zu", + (void *)handler, + data.len); + + /* Dump the response bytes for debugging */ + if (data.len > 0) { + } + + if (data.len >= 4) { /* At least VER(1) + REP(1) + RSV(1) + ATYP(1) */ + uint8_t ver = data.ptr[0]; + uint8_t rep = data.ptr[1]; + uint8_t atype = data.ptr[3]; + + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 response - VER: %d, REP: %d, ATYP: %d", + (void *)handler, + ver, rep, atype); + + size_t addr_size = 0; + + /* Determine the address size based on the address type */ + switch (atype) { + case AWS_SOCKS5_ATYP_DOMAIN: + if (data.len >= 5) { /* Check if we can read the domain length byte */ + uint8_t dom_len = data.ptr[4]; + addr_size = 1 + dom_len; /* Length byte + domain */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Domain address type with length %u", + (void *)handler, + dom_len); + } else { + /* Wait for more data */ + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Waiting for more data to read domain length", + (void *)handler); + return; + } + break; + + case AWS_SOCKS5_ATYP_IPV4: + addr_size = 4; + AWS_LOGF_TRACE(AWS_LS_IO_SOCKS5, "id=%p: IPv4 address type", (void *)handler); + break; + + case AWS_SOCKS5_ATYP_IPV6: + addr_size = 16; + AWS_LOGF_TRACE(AWS_LS_IO_SOCKS5, "id=%p: IPv6 address type", (void *)handler); + break; + + default: + AWS_LOGF_WARN( + AWS_LS_IO_SOCKS5, + "id=%p: Unknown address type: %d", + (void *)handler, + atype); + /* Unknown address type, try to proceed with minimal parsing */ + addr_size = 1; + break; + } + + /* Calculate the full response size */ + size_t response_size = 4 + addr_size + 2; /* Header + address + port */ + + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Expected response size: %zu, current data size: %zu", + (void *)handler, + response_size, data.len); + + /* Check if we have the complete response */ + if (addr_size > 0 && data.len >= response_size) { + result = s_process_connect_response(handler, slot, &data); + + /* If successful, we don't need to shift the buffer because + we're now in established state and will forward any remaining data */ + if (result == AWS_OP_SUCCESS && + socks5_handler->channel_state == AWS_SOCKS5_CHANNEL_STATE_ESTABLISHED) { + + /* Calculate the size of the SOCKS5 response */ + size_t response_size = 4 + addr_size + 2; /* Header + address + port */ + + /* If there's more data after the SOCKS5 response, we need to forward it */ + if (socks5_handler->read_buffer.len > response_size) { + AWS_LOGF_INFO( + AWS_LS_IO_SOCKS5, + "id=%p: Found %zu bytes of trailing data after SOCKS5 handshake completion", + (void *)handler, + socks5_handler->read_buffer.len - response_size); + + /* Create a new message with the remaining data for forwarding */ + struct aws_io_message *forward_message = aws_channel_acquire_message_from_pool( + slot->channel, + AWS_IO_MESSAGE_APPLICATION_DATA, + socks5_handler->read_buffer.len - response_size); + + if (forward_message) { + /* Copy the remaining data after the SOCKS5 response */ + if (aws_byte_buf_write( + &forward_message->message_data, + socks5_handler->read_buffer.buffer + response_size, + socks5_handler->read_buffer.len - response_size)) { + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Forwarding %zu bytes of application data received alongside final SOCKS5 response", + (void *)handler, + forward_message->message_data.len); + + /* We need to delay sending this message until after the setup callback + * completes to ensure higher layer handlers are properly installed */ + + /* Schedule a task to forward the data after the current event loop iteration */ + struct aws_channel_task *forward_task = aws_mem_calloc( + socks5_handler->allocator, 1, sizeof(struct aws_channel_task)); + + if (forward_task) { + struct { + struct aws_channel_slot *slot; + struct aws_io_message *message; + struct aws_allocator *allocator; + } *forward_ctx = aws_mem_calloc( + socks5_handler->allocator, 1, sizeof(*forward_ctx)); + + if (forward_ctx) { + forward_ctx->slot = slot; + forward_ctx->message = forward_message; + forward_ctx->allocator = socks5_handler->allocator; + + aws_channel_task_init( + forward_task, + s_forward_pending_data_task, + forward_ctx, + "socks5_forward_pending_data"); + + aws_channel_schedule_task_now(slot->channel, forward_task); + + AWS_LOGF_TRACE( + AWS_LS_IO_SOCKS5, + "id=%p: Scheduled task to forward pending data", + (void *)handler); + } else { + aws_mem_release(socks5_handler->allocator, forward_task); + aws_mem_release(forward_message->allocator, forward_message); + } + } else { + aws_mem_release(forward_message->allocator, forward_message); + } + } else { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to write remaining data to forward message", + (void *)handler); + aws_mem_release(forward_message->allocator, forward_message); + } + } else { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to acquire message for forwarding remaining data", + (void *)handler); + } + + /* Reset the buffer now that we've handled the remaining data */ + socks5_handler->read_buffer.len = 0; + } else { + /* No remaining data, simply reset the buffer length to 0 but keep the capacity */ + socks5_handler->read_buffer.len = 0; + } + } + } + } + break; + + default: + /* In any other state, do nothing with the data */ + break; + } + + if (result != AWS_OP_SUCCESS) { + /* An error occurred while processing the message */ + socks5_handler->channel_state = AWS_SOCKS5_CHANNEL_STATE_ERROR; + socks5_handler->error_code = aws_last_error(); + } +} + +/* Public API functions */ + +struct aws_channel_handler *aws_socks5_channel_handler_new( + struct aws_allocator *allocator, + const struct aws_socks5_proxy_options *proxy_options, + struct aws_byte_cursor endpoint_host, + uint16_t endpoint_port, + enum aws_socks5_address_type endpoint_address_type, + aws_channel_on_setup_completed_fn *on_setup_completed, + void *user_data) { + + + AWS_ASSERT(allocator); + AWS_ASSERT(proxy_options); + + if (!allocator) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + if (!proxy_options) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + if (!endpoint_host.ptr || endpoint_host.len == 0) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + struct aws_socks5_channel_handler *socks5_handler = aws_mem_calloc( + allocator, 1, sizeof(struct aws_socks5_channel_handler)); + + if (!socks5_handler) { + return NULL; + } + + AWS_ZERO_STRUCT(*socks5_handler); + socks5_handler->allocator = allocator; + + /* Initialize the SOCKS5 context */ + if (aws_socks5_context_init( + &socks5_handler->context, + allocator, + proxy_options, + endpoint_host, + endpoint_port, + endpoint_address_type)) { + goto on_error; + } + + /* Initialize the handler */ + socks5_handler->handler.impl = socks5_handler; + socks5_handler->handler.vtable = &s_socks5_handler_vtable; + socks5_handler->on_setup_completed = on_setup_completed; + socks5_handler->user_data = user_data; + s_transition_state(socks5_handler, AWS_SOCKS5_CHANNEL_STATE_INIT, 0); + socks5_handler->process_incoming_data = false; + + /* Initialize send buffer */ + if (aws_byte_buf_init(&socks5_handler->send_buffer, allocator, 256)) { + goto on_error; + } + + /* Initialize read buffer */ + if (aws_byte_buf_init(&socks5_handler->read_buffer, allocator, 256)) { + goto on_error; + } + + /* Set the connection timeout */ + socks5_handler->connect_timeout_ns = + aws_timestamp_convert(proxy_options->connection_timeout_ms, AWS_TIMESTAMP_MILLIS, AWS_TIMESTAMP_NANOS, NULL); + + return &socks5_handler->handler; + +on_error: + s_socks5_handler_destroy(&socks5_handler->handler); + return NULL; +} + +/** + * Custom TLS negotiation result callback that chains to the original callback + * and then calls the setup callback with the final result. + * + * This function is critical for SOCKS5+TLS integration as it ensures proper + * callback chaining and resource cleanup after TLS negotiation completes. + */ +static void s_release_bootstrap_resources(struct aws_socks5_bootstrap *bootstrap); + +static void s_socks5_tls_on_negotiation_result( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + int error_code, + void *user_data) { + + struct aws_socks5_bootstrap *socks5_bootstrap = (struct aws_socks5_bootstrap *)user_data; + + if (!socks5_bootstrap) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=static: TLS negotiation callback called with NULL bootstrap"); + return; + } + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: TLS negotiation completed with result %d (%s)", + (void *)socks5_bootstrap, + error_code, + aws_error_str(error_code)); + + /* Make local copies of all values we need, since bootstrap might be freed */ + struct aws_client_bootstrap *client_bootstrap = socks5_bootstrap->bootstrap; + void *callback_user_data = socks5_bootstrap->user_data; + aws_client_bootstrap_on_channel_event_fn *setup_callback = socks5_bootstrap->setup_callback; + aws_tls_on_negotiation_result_fn *original_on_negotiation_result = socks5_bootstrap->original_on_negotiation_result; + void *original_tls_user_data = socks5_bootstrap->original_tls_user_data; + + /* First call the original TLS negotiation callback if set */ + if (original_on_negotiation_result) { + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Calling original TLS negotiation callback %p with user data %p", + (void *)socks5_bootstrap, + (void *)(uintptr_t)original_on_negotiation_result, + original_tls_user_data); + + original_on_negotiation_result(handler, slot, error_code, original_tls_user_data); + } + + /* Always call the setup callback regardless of success/failure to ensure proper completion */ + if (setup_callback) { + if (error_code != AWS_ERROR_SUCCESS) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: TLS negotiation failed with error %d (%s), notifying client", + (void *)socks5_bootstrap, + error_code, + aws_error_str(error_code)); + + /* For failures, pass NULL channel to indicate connection failed */ + setup_callback(client_bootstrap, error_code, NULL, callback_user_data); + } else { + AWS_LOGF_INFO( + AWS_LS_IO_SOCKS5, + "id=%p: TLS negotiation successful, calling setup callback to complete connection", + (void *)socks5_bootstrap); + + setup_callback(client_bootstrap, AWS_ERROR_SUCCESS, slot->channel, callback_user_data); + } + } + + /* Release resources but keep bootstrap alive for the shutdown callback */ + s_release_bootstrap_resources(socks5_bootstrap); +} + +/** + * Helper function to install a TLS handler after SOCKS5 setup completes successfully. + * + * This function extracts the TLS handler installation logic from s_on_socks5_setup_completed + * to make the code more maintainable. + */ +static int s_install_tls_handler_after_socks5( + struct aws_channel *channel, + struct aws_socks5_bootstrap *bootstrap) { + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Installing TLS handler after SOCKS5 handshake", + (void *)bootstrap); + + /* Set our custom TLS negotiation result callback */ + bootstrap->tls_options->on_negotiation_result = s_socks5_tls_on_negotiation_result; + bootstrap->tls_options->user_data = bootstrap; + + /* Set up TLS handler */ + struct aws_channel_slot *tls_slot = aws_channel_slot_new(channel); + if (!tls_slot) { + int err_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to create TLS slot, error=%d (%s)", + (void *)bootstrap, + err_code, + aws_error_str(err_code)); + return err_code; + } + + /* Create TLS handler using stored TLS options */ + struct aws_channel_handler *tls_handler = aws_tls_client_handler_new( + bootstrap->allocator, bootstrap->tls_options, tls_slot); + + if (!tls_handler) { + int err_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to create TLS handler, error=%d (%s)", + (void *)bootstrap, + err_code, + aws_error_str(err_code)); + + aws_channel_slot_remove(tls_slot); + return err_code; + } + + /* Add TLS handler to channel */ + aws_channel_slot_insert_end(channel, tls_slot); + + if (aws_channel_slot_set_handler(tls_slot, tls_handler)) { + int err_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to set TLS handler on slot, error=%d (%s)", + (void *)bootstrap, + err_code, + aws_error_str(err_code)); + return err_code; + } + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Starting TLS negotiation after SOCKS5 handshake", + (void *)bootstrap); + + /* Start TLS negotiation - NOW it's safe to begin TLS handshake + * since SOCKS5 tunnel is established */ + if (aws_tls_client_handler_start_negotiation(tls_handler)) { + int err_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to start TLS negotiation, error=%d (%s)", + (void *)bootstrap, + err_code, + aws_error_str(err_code)); + return err_code; + } + + AWS_LOGF_INFO( + AWS_LS_IO_SOCKS5, + "id=%p: TLS handler installed and negotiation started after SOCKS5 handshake", + (void *)bootstrap); + + return AWS_OP_SUCCESS; +} + +/** + * Releases dynamically allocated members held by the bootstrap without freeing the struct itself. + * This allows the bootstrap wrapper to remain alive for shutdown callbacks while avoiding leaks. + */ +static void s_release_bootstrap_resources(struct aws_socks5_bootstrap *bootstrap) { + if (!bootstrap) { + return; + } + + struct aws_allocator *allocator = bootstrap->allocator; + + /* Clean up TLS options if present */ + if (bootstrap->tls_options) { + aws_tls_connection_options_clean_up(bootstrap->tls_options); + aws_mem_release(allocator, bootstrap->tls_options); + bootstrap->tls_options = NULL; + bootstrap->use_tls = false; + bootstrap->original_on_negotiation_result = NULL; + bootstrap->original_tls_user_data = NULL; + } + + /* Clean up SOCKS5 options if present */ + if (bootstrap->socks5_proxy_options) { + aws_socks5_proxy_options_clean_up(bootstrap->socks5_proxy_options); + aws_mem_release(allocator, bootstrap->socks5_proxy_options); + bootstrap->socks5_proxy_options = NULL; + } + + if (bootstrap->pending_channel) { + aws_channel_release_hold(bootstrap->pending_channel); + bootstrap->pending_channel = NULL; + } + + if (bootstrap->endpoint_host) { + aws_string_destroy(bootstrap->endpoint_host); + bootstrap->endpoint_host = NULL; + } + + if (bootstrap->original_endpoint_host) { + aws_string_destroy(bootstrap->original_endpoint_host); + bootstrap->original_endpoint_host = NULL; + } + + bootstrap->endpoint_ready = false; + bootstrap->resolution_in_progress = false; + bootstrap->resolution_error_code = AWS_ERROR_SUCCESS; + bootstrap->resolution_task_scheduled = false; + bootstrap->resolution_failure_task_scheduled = false; +} + +/** + * Helper function to clean up a bootstrap structure and its associated resources + */ +static void s_destroy_bootstrap(struct aws_socks5_bootstrap *bootstrap) { + if (!bootstrap) { + return; + } + + s_release_bootstrap_resources(bootstrap); + aws_mutex_clean_up(&bootstrap->lock); + aws_mem_release(bootstrap->allocator, bootstrap); +} + +static void s_cleanup_bootstrap(struct aws_socks5_bootstrap *bootstrap) { + if (!bootstrap) { + return; + } + + s_release_bootstrap_resources(bootstrap); + + bool defer_cleanup = false; + + aws_mutex_lock(&bootstrap->lock); + if (bootstrap->resolution_in_progress) { + /* Defer destruction until the resolver callback runs so it can drain outstanding tasks without touching freed memory */ + bootstrap->cleanup_pending = true; + defer_cleanup = true; + } else { + bootstrap->cleanup_pending = false; + } + aws_mutex_unlock(&bootstrap->lock); + + if (defer_cleanup) { + return; + } + + s_destroy_bootstrap(bootstrap); +} + +/** + * Called when the SOCKS5 handshake completes. + * If TLS is requested, this function will install the TLS handler. + * Otherwise, it will call the setup callback directly. + */ +static void s_on_socks5_setup_completed( + struct aws_channel *channel, + int error_code, + void *user_data) +{ + struct aws_socks5_bootstrap *bootstrap = (struct aws_socks5_bootstrap *)user_data; + + if (!bootstrap) { + AWS_LOGF_ERROR(AWS_LS_IO_SOCKS5, "id=static: s_on_socks5_setup_completed called with NULL bootstrap"); + return; + } + + /* Make a local copy of the data we need in case bootstrap gets freed */ + struct aws_client_bootstrap *client_bootstrap = bootstrap->bootstrap; + void *callback_user_data = bootstrap->user_data; + aws_client_bootstrap_on_channel_event_fn *setup_callback = bootstrap->setup_callback; + + if (error_code != AWS_ERROR_SUCCESS) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 handshake failed with error_code=%d (%s)", + (void *)bootstrap, + error_code, + aws_error_str(error_code)); + + /* Call the original callback with the error */ + if (setup_callback) { + setup_callback(client_bootstrap, error_code, NULL, callback_user_data); + } + + s_release_bootstrap_resources(bootstrap); + return; + } + + /* SOCKS5 handshake successful */ + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: SOCKS5 handshake completed successfully", + (void *)bootstrap); + + /* If TLS is requested, install TLS handler now that SOCKS5 is established */ + if (bootstrap->use_tls && bootstrap->tls_options) { + int result = s_install_tls_handler_after_socks5(channel, bootstrap); + if (result != AWS_OP_SUCCESS) { + /* Failed to install TLS handler, call setup callback with error */ + if (setup_callback) { + setup_callback(client_bootstrap, result, NULL, callback_user_data); + } + + s_release_bootstrap_resources(bootstrap); + } + /* Note: bootstrap is NOT cleaned up here on success, as that will be done + * in the TLS negotiation result callback */ + } else { + /* No TLS needed, call the original setup callback directly */ + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: No TLS requested, calling original callback", + (void *)bootstrap); + + if (setup_callback) { + setup_callback(client_bootstrap, AWS_ERROR_SUCCESS, channel, callback_user_data); + } + + s_release_bootstrap_resources(bootstrap); + } +} + +static void s_socks5_socket_channel_setup( + struct aws_client_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) +{ + struct aws_socks5_bootstrap *socks5_bootstrap = (struct aws_socks5_bootstrap *)user_data; + AWS_LOGF_TRACE(AWS_LS_IO_SOCKS5, "id=%p: Context=%p", (void *)channel, (void*)socks5_bootstrap); + AWS_LOGF_TRACE(AWS_LS_IO_SOCKS5, "id=%p: setup_callback=%p", (void *)channel, (void *)(uintptr_t)socks5_bootstrap->setup_callback); + AWS_LOGF_TRACE(AWS_LS_IO_SOCKS5, "id=%p: original_user_data=%p", (void *)channel, (void*)socks5_bootstrap->user_data); + + if (error_code != AWS_ERROR_SUCCESS || channel == NULL) { + if (socks5_bootstrap->setup_callback) { + socks5_bootstrap->setup_callback(bootstrap, error_code, NULL, socks5_bootstrap->user_data); + } + if (channel == NULL) { + s_cleanup_bootstrap(socks5_bootstrap); + } else { + s_release_bootstrap_resources(socks5_bootstrap); + } + return; + } + + bool endpoint_ready = false; + bool resolution_in_progress = false; + int resolution_error = AWS_ERROR_SUCCESS; + + aws_mutex_lock(&socks5_bootstrap->lock); + endpoint_ready = socks5_bootstrap->endpoint_ready; + resolution_in_progress = socks5_bootstrap->resolution_in_progress; + resolution_error = socks5_bootstrap->resolution_error_code; + + if (!endpoint_ready && resolution_error == AWS_ERROR_SUCCESS && resolution_in_progress) { + /* DNS still running: hold the channel so the callback can resume the handshake later */ + if (!socks5_bootstrap->pending_channel) { + socks5_bootstrap->pending_channel = channel; + aws_channel_acquire_hold(channel); + } + aws_mutex_unlock(&socks5_bootstrap->lock); + return; + } + + if (error_code != AWS_ERROR_SUCCESS) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Client-side resolution failed for '%s' with error %d (%s)", + (void *)socks5_bootstrap, + socks5_bootstrap->original_endpoint_host + ? aws_string_c_str(socks5_bootstrap->original_endpoint_host) + : "(null)", + error_code, + aws_error_str(error_code)); + } + + aws_mutex_unlock(&socks5_bootstrap->lock); + + if (resolution_error != AWS_ERROR_SUCCESS) { + if (socks5_bootstrap->setup_callback) { + socks5_bootstrap->setup_callback(bootstrap, resolution_error, NULL, socks5_bootstrap->user_data); + } + aws_channel_shutdown(channel, resolution_error); + s_release_bootstrap_resources(socks5_bootstrap); + return; + } + + if (!endpoint_ready) { + int err_code = AWS_ERROR_INVALID_STATE; + if (socks5_bootstrap->setup_callback) { + socks5_bootstrap->setup_callback(bootstrap, err_code, NULL, socks5_bootstrap->user_data); + } + aws_channel_shutdown(channel, err_code); + s_release_bootstrap_resources(socks5_bootstrap); + return; + } + + if (s_socks5_bootstrap_begin_handshake(socks5_bootstrap, channel)) { + int err_code = aws_last_error(); + if (socks5_bootstrap->setup_callback) { + socks5_bootstrap->setup_callback(bootstrap, err_code, NULL, socks5_bootstrap->user_data); + } + s_release_bootstrap_resources(socks5_bootstrap); + return; + } + /* At this point, the SOCKS5 handler will invoke the setup callback. Final cleanup happens during shutdown. */ +} + +static int s_socks5_bootstrap_begin_handshake( + struct aws_socks5_bootstrap *socks5_bootstrap, + struct aws_channel *channel) { + + if (!socks5_bootstrap || !channel) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_channel_slot *slot = aws_channel_get_first_slot(channel); + if (!slot) { + return aws_raise_error(AWS_ERROR_INVALID_STATE); + } + + struct aws_byte_cursor endpoint_host_cursor = aws_byte_cursor_from_array( + socks5_bootstrap->endpoint_host ? aws_string_bytes(socks5_bootstrap->endpoint_host) : NULL, + socks5_bootstrap->endpoint_host ? socks5_bootstrap->endpoint_host->len : 0); + + if (endpoint_host_cursor.len == 0 || endpoint_host_cursor.ptr == NULL) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_channel_handler *socks5_handler = aws_socks5_channel_handler_new( + socks5_bootstrap->allocator, + socks5_bootstrap->socks5_proxy_options, + endpoint_host_cursor, + socks5_bootstrap->endpoint_port, + socks5_bootstrap->endpoint_address_type, + s_on_socks5_setup_completed, + socks5_bootstrap); + if (!socks5_handler) { + return AWS_OP_ERR; + } + + struct aws_channel_slot *socks5_slot = aws_channel_slot_new(channel); + if (!socks5_slot) { + aws_channel_handler_destroy(socks5_handler); + return AWS_OP_ERR; + } + + aws_channel_slot_insert_right(slot, socks5_slot); + socks5_handler->slot = socks5_slot; + + struct aws_socks5_channel_handler *impl_ptr = socks5_handler->impl; + if (impl_ptr) { + impl_ptr->slot = socks5_slot; + impl_ptr->user_data = socks5_bootstrap; + } + + if (aws_channel_slot_set_handler(socks5_slot, socks5_handler)) { + aws_channel_slot_remove(socks5_slot); + return AWS_OP_ERR; + } + + if (aws_socks5_channel_handler_start_handshake(socks5_handler)) { + aws_channel_slot_remove(socks5_slot); + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static void s_socks5_bootstrap_resolution_success_task( + struct aws_channel_task *task, + void *arg, + enum aws_task_status status) { + (void)task; + + struct aws_socks5_bootstrap *socks5_bootstrap = arg; + if (!socks5_bootstrap) { + return; + } + + struct aws_channel *channel = NULL; + + aws_mutex_lock(&socks5_bootstrap->lock); + socks5_bootstrap->resolution_task_scheduled = false; + channel = socks5_bootstrap->pending_channel; + socks5_bootstrap->pending_channel = NULL; + aws_mutex_unlock(&socks5_bootstrap->lock); + + if (!channel) { + return; + } + + /* Defer handshake work to the channel's event-loop thread */ + if (status != AWS_TASK_STATUS_RUN_READY) { + aws_channel_release_hold(channel); + return; + } + + if (s_socks5_bootstrap_begin_handshake(socks5_bootstrap, channel)) { + int err_code = aws_last_error(); + if (socks5_bootstrap->setup_callback) { + socks5_bootstrap->setup_callback( + socks5_bootstrap->bootstrap, + err_code, + NULL, + socks5_bootstrap->user_data); + } + aws_channel_shutdown(channel, err_code); + s_release_bootstrap_resources(socks5_bootstrap); + } + + aws_channel_release_hold(channel); +} + +static void s_socks5_bootstrap_resolution_failure_task( + struct aws_channel_task *task, + void *arg, + enum aws_task_status status) { + (void)task; + struct aws_socks5_bootstrap *socks5_bootstrap = arg; + if (!socks5_bootstrap) { + return; + } + + struct aws_channel *channel = NULL; + int error_code = socks5_bootstrap->resolution_error_code; + if (error_code == AWS_ERROR_SUCCESS) { + error_code = AWS_IO_DNS_INVALID_NAME; + } + + aws_mutex_lock(&socks5_bootstrap->lock); + socks5_bootstrap->resolution_failure_task_scheduled = false; + channel = socks5_bootstrap->pending_channel; + socks5_bootstrap->pending_channel = NULL; + aws_mutex_unlock(&socks5_bootstrap->lock); + + /* Propagate DNS failure on the channel thread to keep shutdown ordering intact */ + if (channel && status == AWS_TASK_STATUS_RUN_READY) { + aws_channel_shutdown(channel, error_code); + aws_channel_release_hold(channel); + } else if (channel) { + aws_channel_release_hold(channel); + } + + if (socks5_bootstrap->setup_callback) { + socks5_bootstrap->setup_callback( + socks5_bootstrap->bootstrap, + error_code, + NULL, + socks5_bootstrap->user_data); + } + + s_release_bootstrap_resources(socks5_bootstrap); +} + +/* Handle channel shutdown */ +static void s_socks5_socket_channel_shutdown( + struct aws_client_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) { + struct aws_socks5_bootstrap *socks5_bootstrap = (struct aws_socks5_bootstrap *)user_data; + if (!socks5_bootstrap) { + return; + } + + if (socks5_bootstrap->shutdown_callback) { + socks5_bootstrap->shutdown_callback(bootstrap, error_code, channel, socks5_bootstrap->user_data); + } + + s_cleanup_bootstrap(socks5_bootstrap); +} + +static void s_socks5_bootstrap_create_channel_options( + struct aws_socks5_bootstrap *socks5_bootstrap, + struct aws_socket_channel_bootstrap_options *channel_options) +{ + channel_options->host_name = aws_string_c_str(socks5_bootstrap->socks5_proxy_options->host); + channel_options->port = socks5_bootstrap->socks5_proxy_options->port; + channel_options->setup_callback = s_socks5_socket_channel_setup; + channel_options->shutdown_callback = s_socks5_socket_channel_shutdown; + channel_options->user_data = socks5_bootstrap; + channel_options->tls_options = NULL; // Handled internally after SOCKS5 handshake +} + +static int s_socks5_bootstrap_set_socks5_proxy_options( + struct aws_socks5_bootstrap *socks5_bootstrap, + struct aws_allocator *allocator, + const struct aws_socks5_proxy_options *source_proxy_options, + const char *host_name, + uint16_t port +) +{ + if (!source_proxy_options) { + return AWS_OP_SUCCESS; + } + + struct aws_socks5_proxy_options * socks5_proxy_options = + aws_mem_calloc(allocator, 1, sizeof(struct aws_socks5_proxy_options)); + if (!socks5_proxy_options) { + return AWS_OP_ERR; + } + + if (aws_socks5_proxy_options_copy(socks5_proxy_options, source_proxy_options)) { + aws_socks5_proxy_options_clean_up(socks5_proxy_options); + aws_mem_release(allocator, socks5_proxy_options); + return AWS_OP_ERR; + } + + if (!host_name || host_name[0] == '\0') { + aws_socks5_proxy_options_clean_up(socks5_proxy_options); + aws_mem_release(allocator, socks5_proxy_options); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_byte_cursor endpoint_host_cursor = aws_byte_cursor_from_c_str(host_name); + struct aws_byte_cursor normalized_host_cursor = s_normalize_proxy_host_cursor(endpoint_host_cursor); + + aws_string_destroy(socks5_bootstrap->endpoint_host); + socks5_bootstrap->endpoint_host = NULL; + aws_string_destroy(socks5_bootstrap->original_endpoint_host); + socks5_bootstrap->original_endpoint_host = NULL; + + socks5_bootstrap->endpoint_port = port; + enum aws_socks5_address_type inferred_type = + aws_socks5_infer_address_type(normalized_host_cursor, AWS_SOCKS5_ATYP_DOMAIN); + socks5_bootstrap->host_resolution_mode = + aws_socks5_proxy_options_get_host_resolution_mode(socks5_proxy_options); + socks5_bootstrap->resolution_error_code = AWS_ERROR_SUCCESS; + socks5_bootstrap->endpoint_ready = + socks5_bootstrap->host_resolution_mode != AWS_SOCKS5_HOST_RESOLUTION_CLIENT; + socks5_bootstrap->resolution_in_progress = false; + + if (socks5_bootstrap->host_resolution_mode == AWS_SOCKS5_HOST_RESOLUTION_CLIENT && + inferred_type != AWS_SOCKS5_ATYP_DOMAIN) { + socks5_bootstrap->endpoint_host = + aws_string_new_from_cursor(allocator, &normalized_host_cursor); + if (!socks5_bootstrap->endpoint_host) { + aws_socks5_proxy_options_clean_up(socks5_proxy_options); + aws_mem_release(allocator, socks5_proxy_options); + return AWS_OP_ERR; + } + socks5_bootstrap->original_endpoint_host = + aws_string_new_from_cursor(allocator, &endpoint_host_cursor); + if (!socks5_bootstrap->original_endpoint_host) { + aws_string_destroy(socks5_bootstrap->endpoint_host); + socks5_bootstrap->endpoint_host = NULL; + aws_socks5_proxy_options_clean_up(socks5_proxy_options); + aws_mem_release(allocator, socks5_proxy_options); + return AWS_OP_ERR; + } + socks5_bootstrap->endpoint_address_type = inferred_type; + socks5_bootstrap->endpoint_ready = true; + } else if (socks5_bootstrap->host_resolution_mode == AWS_SOCKS5_HOST_RESOLUTION_CLIENT) { + socks5_bootstrap->original_endpoint_host = + aws_string_new_from_cursor(allocator, &endpoint_host_cursor); + if (!socks5_bootstrap->original_endpoint_host) { + aws_socks5_proxy_options_clean_up(socks5_proxy_options); + aws_mem_release(allocator, socks5_proxy_options); + return AWS_OP_ERR; + } + socks5_bootstrap->endpoint_address_type = AWS_SOCKS5_ATYP_DOMAIN; + socks5_bootstrap->endpoint_ready = false; + } else { + socks5_bootstrap->endpoint_host = + aws_string_new_from_cursor(allocator, &normalized_host_cursor); + if (!socks5_bootstrap->endpoint_host) { + aws_socks5_proxy_options_clean_up(socks5_proxy_options); + aws_mem_release(allocator, socks5_proxy_options); + return AWS_OP_ERR; + } + socks5_bootstrap->endpoint_address_type = inferred_type; + } + + socks5_bootstrap->socks5_proxy_options = socks5_proxy_options; + + return AWS_OP_SUCCESS; +} + +static int s_socks5_bootstrap_set_tls_options( + struct aws_socks5_bootstrap *socks5_bootstrap, + struct aws_allocator *allocator, + const struct aws_tls_connection_options *tls_options) +{ + if (!tls_options) { + return AWS_OP_SUCCESS; + } + socks5_bootstrap->tls_options = + aws_mem_calloc(allocator, 1, sizeof(struct aws_tls_connection_options)); + if (!socks5_bootstrap->tls_options) { + return AWS_OP_ERR; + } + socks5_bootstrap->original_on_negotiation_result = tls_options->on_negotiation_result; + socks5_bootstrap->original_tls_user_data = tls_options->user_data; + if (aws_tls_connection_options_copy(socks5_bootstrap->tls_options, tls_options)) { + aws_tls_connection_options_clean_up(socks5_bootstrap->tls_options); + aws_mem_release(allocator, socks5_bootstrap->tls_options); + socks5_bootstrap->tls_options = NULL; + return AWS_OP_ERR; + } + socks5_bootstrap->use_tls = true; + return AWS_OP_SUCCESS; +} + +static int s_socks5_bootstrap_start_endpoint_resolution( + struct aws_socks5_bootstrap *socks5_bootstrap, + const struct aws_socket_channel_bootstrap_options *channel_options) { + + if (!socks5_bootstrap || !channel_options) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (socks5_bootstrap->host_resolution_mode != AWS_SOCKS5_HOST_RESOLUTION_CLIENT || socks5_bootstrap->endpoint_ready) { + return AWS_OP_SUCCESS; + } + + if (!socks5_bootstrap->original_endpoint_host) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_client_bootstrap *client_bootstrap = channel_options->bootstrap; + if (!client_bootstrap || !client_bootstrap->host_resolver) { + return aws_raise_error(AWS_ERROR_INVALID_STATE); + } + + /* Prefer per-request overrides, otherwise fall back to bootstrap defaults */ + const struct aws_host_resolution_config *config_to_use = + channel_options->host_resolution_override_config; + + if (config_to_use) { + socks5_bootstrap->host_resolution_config = *config_to_use; + socks5_bootstrap->has_host_resolution_override = true; + config_to_use = &socks5_bootstrap->host_resolution_config; + } else { + socks5_bootstrap->host_resolution_config = client_bootstrap->host_resolver_config; + socks5_bootstrap->has_host_resolution_override = false; + config_to_use = &client_bootstrap->host_resolver_config; + } + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Starting client-side resolution for endpoint '%s'", + (void *)socks5_bootstrap, + aws_string_c_str(socks5_bootstrap->original_endpoint_host)); + + /* Track outstanding work so the setup path can defer the handshake */ + socks5_bootstrap->resolution_error_code = AWS_ERROR_SUCCESS; + socks5_bootstrap->resolution_in_progress = true; + + if (aws_host_resolver_resolve_host( + client_bootstrap->host_resolver, + socks5_bootstrap->original_endpoint_host, + s_socks5_on_host_resolved, + config_to_use, + socks5_bootstrap)) { + socks5_bootstrap->resolution_in_progress = false; + socks5_bootstrap->resolution_error_code = aws_last_error(); + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +static void s_socks5_on_host_resolved( + struct aws_host_resolver *resolver, + const struct aws_string *host_name, + int err_code, + const struct aws_array_list *host_addresses, + void *user_data) { + (void)resolver; + (void)host_name; + + struct aws_socks5_bootstrap *socks5_bootstrap = user_data; + if (!socks5_bootstrap) { + return; + } + + struct aws_channel *channel_for_success = NULL; + struct aws_channel *channel_for_failure = NULL; + struct aws_channel_task *success_task = NULL; + struct aws_channel_task *failure_task = NULL; + + int error_code = err_code; + + aws_mutex_lock(&socks5_bootstrap->lock); + socks5_bootstrap->resolution_in_progress = false; + + if (error_code != AWS_ERROR_SUCCESS) { + socks5_bootstrap->resolution_error_code = error_code; + } else { + size_t address_count = host_addresses ? aws_array_list_length(host_addresses) : 0; + if (!host_addresses || address_count == 0) { + error_code = AWS_IO_DNS_INVALID_NAME; + socks5_bootstrap->resolution_error_code = error_code; + } else { + const struct aws_host_address *chosen_address = NULL; + const struct aws_host_address *first_available = NULL; + + /* Prefer IPv4 when available, otherwise fall back to the first usable entry */ + for (size_t i = 0; i < address_count; ++i) { + const struct aws_host_address *current = NULL; + aws_array_list_get_at_ptr(host_addresses, (void **)¤t, i); + if (!current || !current->address) { + continue; + } + if (!first_available) { + first_available = current; + } + if (current->record_type == AWS_ADDRESS_RECORD_TYPE_A) { + chosen_address = current; + break; + } + } + + if (!chosen_address) { + chosen_address = first_available; + } + + if (!chosen_address || !chosen_address->address) { + error_code = AWS_IO_DNS_INVALID_NAME; + socks5_bootstrap->resolution_error_code = error_code; + } else { + struct aws_string *resolved_ip = + aws_string_new_from_string(socks5_bootstrap->allocator, chosen_address->address); + if (!resolved_ip) { + error_code = aws_last_error(); + socks5_bootstrap->resolution_error_code = error_code; + } else { + aws_string_destroy(socks5_bootstrap->endpoint_host); + socks5_bootstrap->endpoint_host = resolved_ip; + socks5_bootstrap->endpoint_address_type = + chosen_address->record_type == AWS_ADDRESS_RECORD_TYPE_AAAA + ? AWS_SOCKS5_ATYP_IPV6 + : AWS_SOCKS5_ATYP_IPV4; + socks5_bootstrap->endpoint_ready = true; + socks5_bootstrap->resolution_error_code = AWS_ERROR_SUCCESS; + error_code = AWS_ERROR_SUCCESS; + + AWS_LOGF_DEBUG( + AWS_LS_IO_SOCKS5, + "id=%p: Resolved endpoint '%s' to %s", + (void *)socks5_bootstrap, + socks5_bootstrap->original_endpoint_host + ? aws_string_c_str(socks5_bootstrap->original_endpoint_host) + : "", + aws_string_c_str(resolved_ip)); + } + } + } + } + + if (error_code != AWS_ERROR_SUCCESS) { + socks5_bootstrap->endpoint_ready = false; + } + + bool cleanup_now = false; + + if (error_code == AWS_ERROR_SUCCESS) { + if (socks5_bootstrap->pending_channel && !socks5_bootstrap->resolution_task_scheduled) { + channel_for_success = socks5_bootstrap->pending_channel; + aws_channel_task_init( + &socks5_bootstrap->resolution_success_task, + s_socks5_bootstrap_resolution_success_task, + socks5_bootstrap, + "socks5_resolution_success"); + success_task = &socks5_bootstrap->resolution_success_task; + socks5_bootstrap->resolution_task_scheduled = true; + } + } else { + if (socks5_bootstrap->pending_channel && !socks5_bootstrap->resolution_failure_task_scheduled) { + channel_for_failure = socks5_bootstrap->pending_channel; + aws_channel_task_init( + &socks5_bootstrap->resolution_failure_task, + s_socks5_bootstrap_resolution_failure_task, + socks5_bootstrap, + "socks5_resolution_failure"); + failure_task = &socks5_bootstrap->resolution_failure_task; + socks5_bootstrap->resolution_failure_task_scheduled = true; + } + } + + if (socks5_bootstrap->cleanup_pending && !socks5_bootstrap->resolution_in_progress && + success_task == NULL && failure_task == NULL) { + /* shutdown requested earlier; resolver is the last owner so finish cleanup now */ + cleanup_now = true; + socks5_bootstrap->cleanup_pending = false; + } + + aws_mutex_unlock(&socks5_bootstrap->lock); + + if (success_task && channel_for_success) { + aws_channel_schedule_task_now(channel_for_success, success_task); + } + + if (failure_task && channel_for_failure) { + aws_channel_schedule_task_now(channel_for_failure, failure_task); + } + + if (cleanup_now) { + s_cleanup_bootstrap(socks5_bootstrap); + } +} + +int aws_socks5_client_bootstrap_new_socket_channel(struct aws_socket_channel_bootstrap_options *options) { + AWS_PRECONDITION(options); + AWS_FATAL_ASSERT( + s_socks5_system_vtable && s_socks5_system_vtable->aws_client_bootstrap_new_socket_channel && + "socks5 system vtable must provide aws_client_bootstrap_new_socket_channel"); + return s_socks5_system_vtable->aws_client_bootstrap_new_socket_channel(options); +} + +static int s_socks5_bootstrap_create_proxy_options( + struct aws_socks5_bootstrap *socks5_bootstrap, + struct aws_allocator *allocator, + const struct aws_socks5_proxy_options *socks5_proxy_options, + struct aws_socket_channel_bootstrap_options *channel_options) +{ + if (!socks5_bootstrap) { + return AWS_OP_ERR; + } + + socks5_bootstrap->allocator = allocator; + socks5_bootstrap->bootstrap = channel_options->bootstrap; + socks5_bootstrap->setup_callback = channel_options->setup_callback; + socks5_bootstrap->shutdown_callback = channel_options->shutdown_callback; + socks5_bootstrap->user_data = channel_options->user_data; + + if (s_socks5_bootstrap_set_socks5_proxy_options( + socks5_bootstrap, + allocator, + socks5_proxy_options, + channel_options->host_name, + channel_options->port)) { + s_release_bootstrap_resources(socks5_bootstrap); + return AWS_OP_ERR; + } + + if (s_socks5_bootstrap_set_tls_options(socks5_bootstrap, allocator, channel_options->tls_options)) { + s_release_bootstrap_resources(socks5_bootstrap); + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} + +int aws_client_bootstrap_new_socket_channel_with_socks5( + struct aws_allocator *allocator, + struct aws_socket_channel_bootstrap_options *channel_options, + const struct aws_socks5_proxy_options *socks5_proxy_options) +{ + if (!allocator || !socks5_proxy_options || !channel_options) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_socks5_bootstrap *socks5_bootstrap = aws_mem_calloc(allocator, 1, sizeof(struct aws_socks5_bootstrap)); + if (!socks5_bootstrap) { + return AWS_OP_ERR; + } + + if (aws_mutex_init(&socks5_bootstrap->lock) != AWS_OP_SUCCESS) { + aws_mem_release(allocator, socks5_bootstrap); + return AWS_OP_ERR; + } + + if (s_socks5_bootstrap_create_proxy_options( + socks5_bootstrap, allocator, socks5_proxy_options, channel_options)) { + s_cleanup_bootstrap(socks5_bootstrap); + return AWS_OP_ERR; + } + + if (s_socks5_bootstrap_start_endpoint_resolution(socks5_bootstrap, channel_options)) { + s_cleanup_bootstrap(socks5_bootstrap); + return AWS_OP_ERR; + } + + // Update channel options for socks5 socket + s_socks5_bootstrap_create_channel_options(socks5_bootstrap, channel_options); + + AWS_FATAL_ASSERT( + s_socks5_system_vtable && s_socks5_system_vtable->aws_client_bootstrap_new_socket_channel && + "socks5 system vtable must provide aws_client_bootstrap_new_socket_channel"); + + int result = s_socks5_system_vtable->aws_client_bootstrap_new_socket_channel(channel_options); + if (result == AWS_OP_ERR) { + s_cleanup_bootstrap(socks5_bootstrap); + } + + return result; +} + +/* Start the SOCKS5 handshake process manually */ +int aws_socks5_channel_handler_start_handshake(struct aws_channel_handler *handler) { + AWS_ASSERT(handler); + + if (!handler) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (handler->vtable != &s_socks5_handler_vtable) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + struct aws_socks5_channel_handler *socks5_handler = handler->impl; + if (!socks5_handler) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* Check if handler has slot */ + if (!handler->slot) { + /* Don't fail here - the handshake will be started when the slot is set */ + return AWS_OP_SUCCESS; + } + + /* Make sure the slot has a channel */ + if (handler->slot->channel == NULL) { + return aws_raise_error(AWS_ERROR_INVALID_STATE); + } + + /* Don't start handshake if we're not in INIT state */ + if (socks5_handler->channel_state != AWS_SOCKS5_CHANNEL_STATE_INIT) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Cannot start handshake in state %d", + (void *)handler, + socks5_handler->channel_state); + return aws_raise_error(AWS_ERROR_INVALID_STATE); + } + + /* Make sure processing incoming data is enabled */ + socks5_handler->process_incoming_data = true; + + /* Start the SOCKS5 connection process */ + return s_start_socks5_handshake(handler, handler->slot); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 571b5d42a..822d1c280 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,6 +17,14 @@ endmacro() add_test_case(io_library_init) add_test_case(io_library_init_cleanup_init_cleanup) add_test_case(io_library_error_order) +add_test_case(socks5_proxy_options_basic) +add_test_case(socks5_infer_address_type_cases) +add_test_case(socks5_context_init_lifecycle) +add_test_case(socks5_handshake_happy_path) +add_test_case(socks5_handshake_error_paths) +add_test_case(socks5_channel_handler_happy_path) +add_test_case(socks5_channel_handler_greeting_failure) +add_test_case(socks5_bootstrap_system_vtable_failure) # Dispatch Queue does not support pipe if(NOT AWS_USE_APPLE_NETWORK_FRAMEWORK) diff --git a/tests/socks5_test.c b/tests/socks5_test.c new file mode 100644 index 000000000..33a16f8a4 --- /dev/null +++ b/tests/socks5_test.c @@ -0,0 +1,1193 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef int(s_channel_run_fn)(struct aws_channel *channel, void *user_data); + +struct channel_call_context { + struct aws_channel *channel; + s_channel_run_fn *fn; + void *user_data; + struct aws_mutex mutex; + struct aws_condition_variable condition; + bool completed; + int result; + int error_code; + struct aws_channel_task task; +}; + +static bool s_channel_call_complete_predicate(void *user_data) { + struct channel_call_context *context = user_data; + return context->completed; +} + +static void s_channel_call_task(struct aws_channel_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct channel_call_context *context = arg; + int result = AWS_OP_ERR; + int error_code = AWS_ERROR_SUCCESS; + + if (status == AWS_TASK_STATUS_CANCELED) { + result = AWS_OP_ERR; + error_code = AWS_ERROR_INVALID_STATE; + } else { + result = context->fn(context->channel, context->user_data); + if (result != AWS_OP_SUCCESS) { + error_code = aws_last_error(); + } + } + + aws_mutex_lock(&context->mutex); + context->result = result; + context->error_code = error_code; + context->completed = true; + aws_mutex_unlock(&context->mutex); + aws_condition_variable_notify_one(&context->condition); +} + +static int s_channel_run_on_thread(struct aws_channel *channel, s_channel_run_fn *fn, void *user_data) { + + /* If already on the channel's thread, run directly; otherwise, schedule as a task */ + if (aws_channel_thread_is_callers_thread(channel)) { + return fn(channel, user_data); + } + + struct channel_call_context context; + AWS_ZERO_STRUCT(context); + + context.channel = channel; + context.fn = fn; + context.user_data = user_data; + context.result = AWS_OP_ERR; + context.completed = false; + context.error_code = AWS_ERROR_UNKNOWN; + + aws_mutex_init(&context.mutex); + aws_condition_variable_init(&context.condition); + aws_channel_task_init(&context.task, s_channel_call_task, &context, "socks5_test_channel_call"); + + aws_channel_schedule_task_now(channel, &context.task); + + aws_mutex_lock(&context.mutex); + int wait_result = aws_condition_variable_wait_pred( + &context.condition, &context.mutex, s_channel_call_complete_predicate, &context); + int result = context.result; + aws_mutex_unlock(&context.mutex); + + aws_condition_variable_clean_up(&context.condition); + aws_mutex_clean_up(&context.mutex); + + if (wait_result) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: s_channel_run_on_thread wait failed with error %d (%s)", + (void *)channel, + error_code, + aws_error_str(error_code)); + return aws_raise_error(error_code); + } + + if (result == AWS_OP_SUCCESS) { + return AWS_OP_SUCCESS; + } + + if (context.error_code != AWS_ERROR_SUCCESS && context.error_code != AWS_ERROR_UNKNOWN) { + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: s_channel_run_on_thread task failed with error %d (%s)", + (void *)channel, + context.error_code, + aws_error_str(context.error_code)); + return aws_raise_error(context.error_code); + } + + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: s_channel_run_on_thread task failed with unknown error", + (void *)channel); + return aws_raise_error(AWS_ERROR_UNKNOWN); +} + +struct install_handler_args { + struct aws_channel_handler *handler; + struct aws_channel_slot **out_slot; +}; + +static int s_install_handler_on_thread(struct aws_channel *channel, void *user_data) { + struct install_handler_args *args = user_data; + + + /* Create a new slot for the handler in the channel */ + struct aws_channel_slot *slot = aws_channel_slot_new(channel); + if (!slot) { + return aws_raise_error(AWS_ERROR_OOM); + } + + struct aws_channel_slot *first_slot = aws_channel_get_first_slot(channel); + if (first_slot != slot) { + if (aws_channel_slot_insert_end(channel, slot)) { + int error_code = aws_last_error(); + aws_mem_release(slot->alloc, slot); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to insert slot at end, error %d (%s)", + (void *)channel, + error_code, + aws_error_str(error_code)); + return aws_raise_error(error_code); + } + } + + if (aws_channel_slot_set_handler(slot, args->handler)) { + int error_code = aws_last_error(); + aws_channel_slot_remove(slot); + AWS_LOGF_ERROR( + AWS_LS_IO_SOCKS5, + "id=%p: Failed to set handler on slot, error %d (%s)", + (void *)channel, + error_code, + aws_error_str(error_code)); + return aws_raise_error(error_code); + } + + *args->out_slot = slot; + return AWS_OP_SUCCESS; +} + +static int s_install_handler( + struct aws_channel *channel, + struct aws_channel_handler *handler, + struct aws_channel_slot **out_slot) { + + struct install_handler_args args = { + .handler = handler, + .out_slot = out_slot, + }; + return s_channel_run_on_thread(channel, s_install_handler_on_thread, &args); +} + +struct send_message_args { + struct aws_allocator *allocator; + struct aws_channel_slot *slot; + enum aws_channel_direction direction; + struct aws_byte_buf payload; +}; + +static int s_send_message_on_thread(struct aws_channel *channel, void *user_data) { + (void)channel; + + struct send_message_args *args = user_data; + + + /* Acquire a message from the pool and fill with payload */ + struct aws_io_message *message = aws_channel_acquire_message_from_pool( + args->slot->channel, AWS_IO_MESSAGE_APPLICATION_DATA, args->payload.len); + if (!message) { + return AWS_OP_ERR; + } + + struct aws_byte_cursor payload_cursor = aws_byte_cursor_from_buf(&args->payload); + if (aws_byte_buf_append(&message->message_data, &payload_cursor)) { + aws_mem_release(message->allocator, message); + return AWS_OP_ERR; + } + + if (aws_channel_slot_send_message(args->slot, message, args->direction)) { + int error_code = aws_last_error(); + aws_mem_release(message->allocator, message); + return aws_raise_error(error_code); + } + + return AWS_OP_SUCCESS; +} + +static int s_channel_send_bytes( + struct aws_allocator *allocator, + struct aws_channel_slot *slot, + enum aws_channel_direction direction, + const uint8_t *data, + size_t len) { + + struct send_message_args args; + AWS_ZERO_STRUCT(args); + args.allocator = allocator; + args.slot = slot; + args.direction = direction; + + if (aws_byte_buf_init_copy_from_cursor( + &args.payload, allocator, aws_byte_cursor_from_array(data, len))) { + return AWS_OP_ERR; + } + + int result = s_channel_run_on_thread(slot->channel, s_send_message_on_thread, &args); + aws_byte_buf_clean_up(&args.payload); + return result; +} + +static int s_channel_send_cursor( + struct aws_allocator *allocator, + struct aws_channel_slot *slot, + enum aws_channel_direction direction, + struct aws_byte_cursor cursor) { + return s_channel_send_bytes(allocator, slot, direction, cursor.ptr, cursor.len); +} + +static int s_start_handshake_on_thread(struct aws_channel *channel, void *user_data) { + struct aws_channel_handler *handler = user_data; + return aws_socks5_channel_handler_start_handshake(handler); +} + +static int s_socks5_proxy_options_basic(struct aws_allocator *allocator, void *ctx) { + + /* Test basic initialization and configuration of SOCKS5 proxy options */ + (void)ctx; + + struct aws_socks5_proxy_options defaults; + ASSERT_SUCCESS(aws_socks5_proxy_options_init_default(&defaults)); + ASSERT_INT_EQUALS(1080, defaults.port); + ASSERT_INT_EQUALS(3000, defaults.connection_timeout_ms); + ASSERT_INT_EQUALS(AWS_SOCKS5_HOST_RESOLUTION_PROXY, defaults.host_resolution_mode); + aws_socks5_proxy_options_clean_up(&defaults); + + struct aws_socks5_proxy_options options; + AWS_ZERO_STRUCT(options); + struct aws_byte_cursor proxy_host = aws_byte_cursor_from_c_str("proxy.example.com"); + ASSERT_SUCCESS(aws_socks5_proxy_options_init(&options, allocator, proxy_host, 9000)); + ASSERT_NOT_NULL(options.host); + ASSERT_BIN_ARRAYS_EQUALS( + proxy_host.ptr, proxy_host.len, aws_string_bytes(options.host), options.host->len); + ASSERT_INT_EQUALS(9000, options.port); + ASSERT_INT_EQUALS(3000, options.connection_timeout_ms); + ASSERT_INT_EQUALS(AWS_SOCKS5_HOST_RESOLUTION_PROXY, options.host_resolution_mode); + + struct aws_byte_cursor username = aws_byte_cursor_from_c_str("user"); + struct aws_byte_cursor password = aws_byte_cursor_from_c_str("pass"); + ASSERT_SUCCESS(aws_socks5_proxy_options_set_auth(&options, allocator, username, password)); + ASSERT_NOT_NULL(options.username); + ASSERT_NOT_NULL(options.password); + ASSERT_BIN_ARRAYS_EQUALS( + username.ptr, username.len, aws_string_bytes(options.username), options.username->len); + ASSERT_BIN_ARRAYS_EQUALS( + password.ptr, password.len, aws_string_bytes(options.password), options.password->len); + + aws_socks5_proxy_options_set_host_resolution_mode(&options, AWS_SOCKS5_HOST_RESOLUTION_CLIENT); + ASSERT_INT_EQUALS(AWS_SOCKS5_HOST_RESOLUTION_CLIENT, aws_socks5_proxy_options_get_host_resolution_mode(&options)); + + aws_socks5_proxy_options_clean_up(&options); + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(socks5_proxy_options_basic, s_socks5_proxy_options_basic) + +static int s_socks5_infer_address_type_cases(struct aws_allocator *allocator, void *ctx) { + + /* Test address type inference for various host formats */ + (void)allocator; + (void)ctx; + + struct aws_byte_cursor ipv4_host = aws_byte_cursor_from_c_str("127.0.0.1"); + ASSERT_INT_EQUALS( + AWS_SOCKS5_ATYP_IPV4, aws_socks5_infer_address_type(ipv4_host, AWS_SOCKS5_ATYP_DOMAIN)); + + struct aws_byte_cursor ipv6_host = aws_byte_cursor_from_c_str("2001:db8::1"); + ASSERT_INT_EQUALS( + AWS_SOCKS5_ATYP_IPV6, aws_socks5_infer_address_type(ipv6_host, AWS_SOCKS5_ATYP_DOMAIN)); + + struct aws_byte_cursor bracketed_ipv6 = aws_byte_cursor_from_c_str("[fe80::1]"); + ASSERT_INT_EQUALS( + AWS_SOCKS5_ATYP_IPV6, aws_socks5_infer_address_type(bracketed_ipv6, AWS_SOCKS5_ATYP_DOMAIN)); + + struct aws_byte_cursor scoped_ipv6 = aws_byte_cursor_from_c_str("fe80::1%eth0"); + ASSERT_INT_EQUALS( + AWS_SOCKS5_ATYP_IPV6, aws_socks5_infer_address_type(scoped_ipv6, AWS_SOCKS5_ATYP_DOMAIN)); + + struct aws_byte_cursor domain_host = aws_byte_cursor_from_c_str("example.com"); + ASSERT_INT_EQUALS( + AWS_SOCKS5_ATYP_DOMAIN, aws_socks5_infer_address_type(domain_host, AWS_SOCKS5_ATYP_DOMAIN)); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(socks5_infer_address_type_cases, s_socks5_infer_address_type_cases) + +static int s_socks5_context_init_lifecycle(struct aws_allocator *allocator, void *ctx) { + + /* Test context initialization, cleanup, and error handling */ + (void)ctx; + + struct aws_socks5_proxy_options options; + AWS_ZERO_STRUCT(options); + struct aws_byte_cursor proxy_host = aws_byte_cursor_from_c_str("proxy.example.com"); + ASSERT_SUCCESS(aws_socks5_proxy_options_init(&options, allocator, proxy_host, 1080)); + + struct aws_byte_cursor username = aws_byte_cursor_from_c_str("user"); + struct aws_byte_cursor password = aws_byte_cursor_from_c_str("pass"); + ASSERT_SUCCESS(aws_socks5_proxy_options_set_auth(&options, allocator, username, password)); + + struct aws_socks5_context context; + AWS_ZERO_STRUCT(context); + struct aws_byte_cursor endpoint_host = aws_byte_cursor_from_c_str("destination.example.com"); + + ASSERT_SUCCESS(aws_socks5_context_init( + &context, + allocator, + &options, + endpoint_host, + 443, + AWS_SOCKS5_ATYP_DOMAIN)); + + ASSERT_NOT_NULL(context.endpoint_host); + ASSERT_BIN_ARRAYS_EQUALS( + endpoint_host.ptr, endpoint_host.len, aws_string_bytes(context.endpoint_host), context.endpoint_host->len); + ASSERT_INT_EQUALS(AWS_SOCKS5_STATE_INIT, context.state); + ASSERT_INT_EQUALS(AWS_SOCKS5_ATYP_DOMAIN, context.endpoint_address_type); + ASSERT_UINT_EQUALS(2, aws_array_list_length(&context.auth_methods)); + + aws_socks5_context_clean_up(&context); + ASSERT_NULL(context.endpoint_host); + ASSERT_NULL(context.options.host); + ASSERT_UINT_EQUALS(0, context.auth_methods.length); + + struct aws_socks5_context bad_context; + AWS_ZERO_STRUCT(bad_context); + struct aws_byte_cursor empty_host = { + .ptr = NULL, + .len = 0, + }; + ASSERT_ERROR( + AWS_ERROR_INVALID_ARGUMENT, + aws_socks5_context_init( + &bad_context, + allocator, + &options, + empty_host, + 443, + AWS_SOCKS5_ATYP_DOMAIN)); + + aws_socks5_proxy_options_clean_up(&options); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(socks5_context_init_lifecycle, s_socks5_context_init_lifecycle) + +static int s_socks5_handshake_happy_path(struct aws_allocator *allocator, void *ctx) { + + /* Simulate a successful SOCKS5 handshake sequence */ + (void)ctx; + + struct aws_socks5_proxy_options options; + AWS_ZERO_STRUCT(options); + struct aws_byte_cursor proxy_host = aws_byte_cursor_from_c_str("proxy.example.com"); + ASSERT_SUCCESS(aws_socks5_proxy_options_init(&options, allocator, proxy_host, 1080)); + struct aws_byte_cursor username = aws_byte_cursor_from_c_str("user"); + struct aws_byte_cursor password = aws_byte_cursor_from_c_str("pass"); + ASSERT_SUCCESS(aws_socks5_proxy_options_set_auth(&options, allocator, username, password)); + + struct aws_socks5_context context; + AWS_ZERO_STRUCT(context); + struct aws_byte_cursor endpoint_host = aws_byte_cursor_from_c_str("destination.example.com"); + ASSERT_SUCCESS(aws_socks5_context_init( + &context, + allocator, + &options, + endpoint_host, + 443, + AWS_SOCKS5_ATYP_DOMAIN)); + + struct aws_byte_buf buffer; + ASSERT_SUCCESS(aws_byte_buf_init(&buffer, allocator, 64)); + + ASSERT_SUCCESS(aws_socks5_write_greeting(&context, &buffer)); + ASSERT_INT_EQUALS(AWS_SOCKS5_STATE_GREETING_SENT, context.state); + ASSERT_UINT_EQUALS(4, buffer.len); /* VER + NMETHODS + 2 methods */ + ASSERT_UINT_EQUALS(AWS_SOCKS5_VERSION, buffer.buffer[0]); + ASSERT_UINT_EQUALS(2, buffer.buffer[1]); + + uint8_t greeting_resp[] = {AWS_SOCKS5_VERSION, AWS_SOCKS5_AUTH_USERNAME_PASSWORD}; + struct aws_byte_cursor cursor = aws_byte_cursor_from_array(greeting_resp, sizeof(greeting_resp)); + ASSERT_SUCCESS(aws_socks5_read_greeting_response(&context, &cursor)); + ASSERT_INT_EQUALS(AWS_SOCKS5_STATE_GREETING_RECEIVED, context.state); + ASSERT_INT_EQUALS(AWS_SOCKS5_AUTH_USERNAME_PASSWORD, context.selected_auth); + + aws_byte_buf_reset(&buffer, false); + ASSERT_SUCCESS(aws_socks5_write_auth_request(&context, &buffer)); + ASSERT_INT_EQUALS(AWS_SOCKS5_STATE_AUTH_STARTED, context.state); + ASSERT_UINT_EQUALS(3 + username.len + password.len, buffer.len); + ASSERT_UINT_EQUALS(AWS_SOCKS5_AUTH_VERSION, buffer.buffer[0]); + ASSERT_UINT_EQUALS(username.len, buffer.buffer[1]); + ASSERT_UINT_EQUALS(password.len, buffer.buffer[1 + 1 + username.len]); + + uint8_t auth_resp[] = {AWS_SOCKS5_AUTH_VERSION, 0}; + cursor = aws_byte_cursor_from_array(auth_resp, sizeof(auth_resp)); + ASSERT_SUCCESS(aws_socks5_read_auth_response(&context, &cursor)); + ASSERT_INT_EQUALS(AWS_SOCKS5_STATE_AUTH_COMPLETED, context.state); + + aws_byte_buf_reset(&buffer, false); + ASSERT_SUCCESS(aws_socks5_write_connect_request(&context, &buffer)); + ASSERT_TRUE(buffer.len > AWS_SOCKS5_CONN_REQ_MIN_SIZE); + ASSERT_INT_EQUALS(AWS_SOCKS5_STATE_REQUEST_SENT, context.state); + + uint8_t connect_resp[] = { + AWS_SOCKS5_VERSION, + AWS_SOCKS5_STATUS_SUCCESS, + AWS_SOCKS5_RESERVED, + AWS_SOCKS5_ATYP_IPV4, + 10, + 0, + 0, + 1, + 0, + 80}; + cursor = aws_byte_cursor_from_array(connect_resp, sizeof(connect_resp)); + ASSERT_SUCCESS(aws_socks5_read_connect_response(&context, &cursor)); + ASSERT_INT_EQUALS(AWS_SOCKS5_STATE_CONNECTED, context.state); + + aws_byte_buf_clean_up(&buffer); + aws_socks5_context_clean_up(&context); + aws_socks5_proxy_options_clean_up(&options); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(socks5_handshake_happy_path, s_socks5_handshake_happy_path) + +static int s_socks5_handshake_error_paths(struct aws_allocator *allocator, void *ctx) { + + /* Test error handling for handshake failures (greeting, auth, connect) */ + (void)ctx; + + /* Greeting rejection */ + struct aws_socks5_proxy_options options_default; + AWS_ZERO_STRUCT(options_default); + struct aws_byte_cursor proxy_host = aws_byte_cursor_from_c_str("proxy.example.com"); + ASSERT_SUCCESS(aws_socks5_proxy_options_init(&options_default, allocator, proxy_host, 1080)); + + struct aws_socks5_context context_default; + AWS_ZERO_STRUCT(context_default); + struct aws_byte_cursor endpoint_host = aws_byte_cursor_from_c_str("endpoint.example.com"); + ASSERT_SUCCESS(aws_socks5_context_init( + &context_default, + allocator, + &options_default, + endpoint_host, + 80, + AWS_SOCKS5_ATYP_DOMAIN)); + + struct aws_byte_buf buffer; + ASSERT_SUCCESS(aws_byte_buf_init(&buffer, allocator, 32)); + ASSERT_SUCCESS(aws_socks5_write_greeting(&context_default, &buffer)); + + uint8_t reject_resp[] = {AWS_SOCKS5_VERSION, AWS_SOCKS5_AUTH_NO_ACCEPTABLE}; + struct aws_byte_cursor cursor = aws_byte_cursor_from_array(reject_resp, sizeof(reject_resp)); + ASSERT_ERROR( + AWS_IO_SOCKS5_PROXY_ERROR_UNSUPPORTED_AUTH_METHOD, + aws_socks5_read_greeting_response(&context_default, &cursor)); + ASSERT_INT_EQUALS(AWS_SOCKS5_STATE_ERROR, context_default.state); + + aws_byte_buf_clean_up(&buffer); + aws_socks5_context_clean_up(&context_default); + aws_socks5_proxy_options_clean_up(&options_default); + + /* Auth failure */ + struct aws_socks5_proxy_options auth_options; + AWS_ZERO_STRUCT(auth_options); + ASSERT_SUCCESS(aws_socks5_proxy_options_init(&auth_options, allocator, proxy_host, 1080)); + struct aws_byte_cursor username = aws_byte_cursor_from_c_str("user"); + struct aws_byte_cursor password = aws_byte_cursor_from_c_str("pass"); + ASSERT_SUCCESS(aws_socks5_proxy_options_set_auth(&auth_options, allocator, username, password)); + + struct aws_socks5_context auth_context; + AWS_ZERO_STRUCT(auth_context); + ASSERT_SUCCESS(aws_socks5_context_init( + &auth_context, + allocator, + &auth_options, + endpoint_host, + 443, + AWS_SOCKS5_ATYP_DOMAIN)); + + ASSERT_SUCCESS(aws_byte_buf_init(&buffer, allocator, 32)); + ASSERT_SUCCESS(aws_socks5_write_greeting(&auth_context, &buffer)); + uint8_t greeting_resp[] = {AWS_SOCKS5_VERSION, AWS_SOCKS5_AUTH_USERNAME_PASSWORD}; + cursor = aws_byte_cursor_from_array(greeting_resp, sizeof(greeting_resp)); + ASSERT_SUCCESS(aws_socks5_read_greeting_response(&auth_context, &cursor)); + aws_byte_buf_reset(&buffer, false); + ASSERT_SUCCESS(aws_socks5_write_auth_request(&auth_context, &buffer)); + uint8_t auth_fail_resp[] = {AWS_SOCKS5_AUTH_VERSION, 1}; + cursor = aws_byte_cursor_from_array(auth_fail_resp, sizeof(auth_fail_resp)); + ASSERT_ERROR(AWS_IO_SOCKS5_PROXY_ERROR_AUTH_FAILED, aws_socks5_read_auth_response(&auth_context, &cursor)); + ASSERT_INT_EQUALS(AWS_SOCKS5_STATE_ERROR, auth_context.state); + + aws_byte_buf_clean_up(&buffer); + aws_socks5_context_clean_up(&auth_context); + aws_socks5_proxy_options_clean_up(&auth_options); + + /* Connect failure */ + struct aws_socks5_proxy_options connect_options; + AWS_ZERO_STRUCT(connect_options); + ASSERT_SUCCESS(aws_socks5_proxy_options_init(&connect_options, allocator, proxy_host, 1080)); + ASSERT_SUCCESS(aws_socks5_proxy_options_set_auth(&connect_options, allocator, username, password)); + + struct aws_socks5_context connect_context; + AWS_ZERO_STRUCT(connect_context); + ASSERT_SUCCESS(aws_socks5_context_init( + &connect_context, + allocator, + &connect_options, + endpoint_host, + 443, + AWS_SOCKS5_ATYP_DOMAIN)); + + ASSERT_SUCCESS(aws_byte_buf_init(&buffer, allocator, 32)); + ASSERT_SUCCESS(aws_socks5_write_greeting(&connect_context, &buffer)); + cursor = aws_byte_cursor_from_array(greeting_resp, sizeof(greeting_resp)); + ASSERT_SUCCESS(aws_socks5_read_greeting_response(&connect_context, &cursor)); + aws_byte_buf_reset(&buffer, false); + ASSERT_SUCCESS(aws_socks5_write_auth_request(&connect_context, &buffer)); + uint8_t auth_ok_resp[] = {AWS_SOCKS5_AUTH_VERSION, 0}; + cursor = aws_byte_cursor_from_array(auth_ok_resp, sizeof(auth_ok_resp)); + ASSERT_SUCCESS(aws_socks5_read_auth_response(&connect_context, &cursor)); + aws_byte_buf_reset(&buffer, false); + ASSERT_SUCCESS(aws_socks5_write_connect_request(&connect_context, &buffer)); + uint8_t connect_fail_resp[] = { + AWS_SOCKS5_VERSION, + AWS_SOCKS5_STATUS_CONNECTION_REFUSED, + AWS_SOCKS5_RESERVED, + AWS_SOCKS5_ATYP_IPV4, + 10, + 0, + 0, + 1, + 0, + 80}; + cursor = aws_byte_cursor_from_array(connect_fail_resp, sizeof(connect_fail_resp)); + ASSERT_ERROR(AWS_IO_SOCKET_CONNECTION_REFUSED, aws_socks5_read_connect_response(&connect_context, &cursor)); + ASSERT_INT_EQUALS(AWS_SOCKS5_STATE_ERROR, connect_context.state); + + aws_byte_buf_clean_up(&buffer); + aws_socks5_context_clean_up(&connect_context); + aws_socks5_proxy_options_clean_up(&connect_options); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(socks5_handshake_error_paths, s_socks5_handshake_error_paths) + +struct socks5_peer_impl { + struct aws_allocator *allocator; + struct aws_mutex mutex; + struct aws_condition_variable condition; + struct aws_byte_buf last_write; + bool has_write; + size_t write_count; + struct aws_channel_slot *slot; +}; + +static bool s_peer_has_write_predicate(void *user_data) { + struct socks5_peer_impl *impl = user_data; + return impl->has_write; +} + +static int s_peer_wait_for_write(struct socks5_peer_impl *impl, struct aws_byte_buf *out_buf) { + int result = AWS_OP_SUCCESS; + + + /* Wait until the peer handler has written data */ + aws_mutex_lock(&impl->mutex); + if (aws_condition_variable_wait_pred(&impl->condition, &impl->mutex, s_peer_has_write_predicate, impl)) { + result = AWS_OP_ERR; + goto done; + } + + result = aws_byte_buf_init_copy_from_cursor( + out_buf, + impl->allocator, + aws_byte_cursor_from_buf(&impl->last_write)); + impl->has_write = false; + aws_byte_buf_clean_up(&impl->last_write); + AWS_ZERO_STRUCT(impl->last_write); + +done: + aws_mutex_unlock(&impl->mutex); + return result; +} + +static int s_peer_send(struct socks5_peer_impl *impl, const uint8_t *data, size_t len) { + struct aws_channel_slot *slot = NULL; + + + /* Send data to the peer's slot (simulates network input) */ + aws_mutex_lock(&impl->mutex); + slot = impl->slot; + aws_mutex_unlock(&impl->mutex); + + if (!slot) { + return aws_raise_error(AWS_ERROR_INVALID_STATE); + } + + return s_channel_send_bytes(impl->allocator, slot, AWS_CHANNEL_DIR_READ, data, len); +} + +static int s_peer_process_write_message( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_io_message *message) { + + struct socks5_peer_impl *impl = handler->impl; + struct aws_byte_cursor payload = aws_byte_cursor_from_buf(&message->message_data); + + /* Store the written message so the test can inspect it */ + aws_mutex_lock(&impl->mutex); + + if (impl->last_write.buffer) { + aws_byte_buf_clean_up(&impl->last_write); + AWS_ZERO_STRUCT(impl->last_write); + } + + if (aws_byte_buf_init_copy_from_cursor(&impl->last_write, impl->allocator, payload)) { + aws_mutex_unlock(&impl->mutex); + aws_mem_release(message->allocator, message); + return AWS_OP_ERR; + } + + impl->has_write = true; + impl->write_count++; + impl->slot = slot; + aws_condition_variable_notify_one(&impl->condition); + + aws_mutex_unlock(&impl->mutex); + + aws_mem_release(message->allocator, message); + return AWS_OP_SUCCESS; +} + +static int s_peer_process_read_message( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + struct aws_io_message *message) { + (void)handler; + (void)slot; + + aws_mem_release(message->allocator, message); + return AWS_OP_SUCCESS; +} + +static int s_peer_increment_read_window( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + size_t size) { + (void)handler; + (void)slot; + (void)size; + return AWS_OP_SUCCESS; +} + +static int s_peer_shutdown( + struct aws_channel_handler *handler, + struct aws_channel_slot *slot, + enum aws_channel_direction dir, + int error_code, + bool abort_immediately) { + (void)handler; + + return aws_channel_slot_on_handler_shutdown_complete(slot, dir, error_code, abort_immediately); +} + +static size_t s_peer_initial_window_size(struct aws_channel_handler *handler) { + (void)handler; + return SIZE_MAX; +} + +static size_t s_peer_message_overhead(struct aws_channel_handler *handler) { + (void)handler; + return 0; +} + +static void s_peer_destroy(struct aws_channel_handler *handler) { + struct socks5_peer_impl *impl = handler->impl; + if (!impl) { + return; + } + + aws_byte_buf_clean_up(&impl->last_write); + aws_condition_variable_clean_up(&impl->condition); + aws_mutex_clean_up(&impl->mutex); + aws_mem_release(handler->alloc, impl); + aws_mem_release(handler->alloc, handler); +} + +static struct aws_channel_handler_vtable s_peer_handler_vtable = { + .process_read_message = s_peer_process_read_message, + .process_write_message = s_peer_process_write_message, + .increment_read_window = s_peer_increment_read_window, + .shutdown = s_peer_shutdown, + .initial_window_size = s_peer_initial_window_size, + .message_overhead = s_peer_message_overhead, + .destroy = s_peer_destroy, +}; + +static struct aws_channel_handler *s_peer_handler_new( + struct aws_allocator *allocator, + struct socks5_peer_impl **out_impl) { + struct aws_channel_handler *handler = aws_mem_calloc(allocator, 1, sizeof(struct aws_channel_handler)); + if (!handler) { + return NULL; + } + + struct socks5_peer_impl *impl = aws_mem_calloc(allocator, 1, sizeof(struct socks5_peer_impl)); + if (!impl) { + aws_mem_release(allocator, handler); + return NULL; + } + + impl->allocator = allocator; + if (aws_mutex_init(&impl->mutex)) { + aws_mem_release(allocator, impl); + aws_mem_release(allocator, handler); + return NULL; + } + if (aws_condition_variable_init(&impl->condition)) { + aws_mutex_clean_up(&impl->mutex); + aws_mem_release(allocator, impl); + aws_mem_release(allocator, handler); + return NULL; + } + + handler->alloc = allocator; + handler->impl = impl; + handler->vtable = &s_peer_handler_vtable; + + *out_impl = impl; + return handler; +} + +struct socks5_channel_fixture { + struct aws_mutex mutex; + struct aws_condition_variable condition; + bool setup_completed; + int setup_error; + bool shutdown_completed; + int shutdown_error; +}; + +static void s_socks5_channel_on_setup_completed(struct aws_channel *channel, int error_code, void *user_data) { + (void)channel; + struct socks5_channel_fixture *fixture = user_data; + + aws_mutex_lock(&fixture->mutex); + fixture->setup_completed = true; + fixture->setup_error = error_code; + aws_condition_variable_notify_one(&fixture->condition); + aws_mutex_unlock(&fixture->mutex); +} + +static void s_socks5_channel_on_shutdown_completed(struct aws_channel *channel, int error_code, void *user_data) { + (void)channel; + struct socks5_channel_fixture *fixture = user_data; + + aws_mutex_lock(&fixture->mutex); + fixture->shutdown_completed = true; + fixture->shutdown_error = error_code; + aws_condition_variable_notify_one(&fixture->condition); + aws_mutex_unlock(&fixture->mutex); +} + +static bool s_socks5_channel_setup_predicate(void *user_data) { + struct socks5_channel_fixture *fixture = user_data; + return fixture->setup_completed; +} + +static bool s_socks5_channel_shutdown_predicate(void *user_data) { + struct socks5_channel_fixture *fixture = user_data; + return fixture->shutdown_completed; +} + +struct socks5_handler_context { + struct aws_mutex mutex; + struct aws_condition_variable condition; + bool invoked; + int error_code; +}; + +static void s_socks5_handler_on_setup_completed(struct aws_channel *channel, int error_code, void *user_data) { + (void)channel; + struct socks5_handler_context *context = user_data; + + aws_mutex_lock(&context->mutex); + context->invoked = true; + context->error_code = error_code; + aws_condition_variable_notify_one(&context->condition); + aws_mutex_unlock(&context->mutex); +} + +static bool s_socks5_handler_invoked_predicate(void *user_data) { + struct socks5_handler_context *context = user_data; + return context->invoked; +} + +static int s_socks5_channel_handler_happy_path(struct aws_allocator *allocator, void *ctx) { + + /* Test full channel handler flow and data forwarding */ + (void)ctx; + + /* Set up event loop and channel for handler integration test */ + struct aws_event_loop *event_loop = aws_event_loop_new_default(allocator, aws_high_res_clock_get_ticks); + ASSERT_NOT_NULL(event_loop); + ASSERT_SUCCESS(aws_event_loop_run(event_loop)); + + struct socks5_channel_fixture channel_fixture = { + .mutex = AWS_MUTEX_INIT, + .condition = AWS_CONDITION_VARIABLE_INIT, + .setup_completed = false, + .setup_error = AWS_ERROR_SUCCESS, + .shutdown_completed = false, + .shutdown_error = AWS_ERROR_SUCCESS, + }; + + struct aws_channel_options channel_options = { + .on_setup_completed = s_socks5_channel_on_setup_completed, + .setup_user_data = &channel_fixture, + .on_shutdown_completed = s_socks5_channel_on_shutdown_completed, + .shutdown_user_data = &channel_fixture, + .event_loop = event_loop, + }; + + struct aws_channel *channel = aws_channel_new(allocator, &channel_options); + ASSERT_NOT_NULL(channel); + + aws_mutex_lock(&channel_fixture.mutex); + ASSERT_SUCCESS(aws_condition_variable_wait_pred( + &channel_fixture.condition, &channel_fixture.mutex, s_socks5_channel_setup_predicate, &channel_fixture)); + aws_mutex_unlock(&channel_fixture.mutex); + ASSERT_INT_EQUALS(0, channel_fixture.setup_error); + + /* Install a peer handler to simulate the remote SOCKS5 server */ + struct socks5_peer_impl *peer_impl = NULL; + struct aws_channel_handler *peer_handler = s_peer_handler_new(allocator, &peer_impl); + ASSERT_NOT_NULL(peer_handler); + + struct aws_channel_slot *peer_slot = NULL; + ASSERT_SUCCESS(s_install_handler(channel, peer_handler, &peer_slot)); + aws_mutex_lock(&peer_impl->mutex); + peer_impl->slot = peer_slot; + aws_mutex_unlock(&peer_impl->mutex); + + struct aws_channel_slot *socks5_slot = NULL; + + struct aws_socks5_proxy_options proxy_options; + ASSERT_SUCCESS(aws_socks5_proxy_options_init( + &proxy_options, allocator, aws_byte_cursor_from_c_str("proxy.example.com"), 1080)); + ASSERT_SUCCESS(aws_socks5_proxy_options_set_auth( + &proxy_options, allocator, aws_byte_cursor_from_c_str("user"), aws_byte_cursor_from_c_str("pass"))); + + struct socks5_handler_context handler_context = { + .mutex = AWS_MUTEX_INIT, + .condition = AWS_CONDITION_VARIABLE_INIT, + .invoked = false, + .error_code = AWS_ERROR_SUCCESS, + }; + + struct aws_channel_handler *socks5_handler = aws_socks5_channel_handler_new( + allocator, + &proxy_options, + aws_byte_cursor_from_c_str("destination.example.com"), + 443, + AWS_SOCKS5_ATYP_DOMAIN, + s_socks5_handler_on_setup_completed, + &handler_context); + ASSERT_NOT_NULL(socks5_handler); + ASSERT_SUCCESS(s_install_handler(channel, socks5_handler, &socks5_slot)); + + ASSERT_SUCCESS(s_channel_run_on_thread(channel, s_start_handshake_on_thread, socks5_handler)); + + struct aws_byte_buf greeting; + ASSERT_SUCCESS(s_peer_wait_for_write(peer_impl, &greeting)); + ASSERT_TRUE(greeting.len >= AWS_SOCKS5_GREETING_MIN_SIZE); + ASSERT_UINT_EQUALS(AWS_SOCKS5_VERSION, greeting.buffer[0]); + ASSERT_UINT_EQUALS(2, greeting.buffer[1]); + aws_byte_buf_clean_up(&greeting); + + uint8_t greeting_response[] = {AWS_SOCKS5_VERSION, AWS_SOCKS5_AUTH_USERNAME_PASSWORD}; + ASSERT_SUCCESS(s_peer_send(peer_impl, greeting_response, sizeof(greeting_response))); + + struct aws_byte_buf auth_request; + ASSERT_SUCCESS(s_peer_wait_for_write(peer_impl, &auth_request)); + ASSERT_UINT_EQUALS(3 + 4 + 4, auth_request.len); + aws_byte_buf_clean_up(&auth_request); + + uint8_t auth_response[] = {AWS_SOCKS5_AUTH_VERSION, 0}; + ASSERT_SUCCESS(s_peer_send(peer_impl, auth_response, sizeof(auth_response))); + + struct aws_byte_buf connect_request; + ASSERT_SUCCESS(s_peer_wait_for_write(peer_impl, &connect_request)); + ASSERT_TRUE(connect_request.len > AWS_SOCKS5_CONN_REQ_MIN_SIZE); + aws_byte_buf_clean_up(&connect_request); + + uint8_t connect_success[] = { + AWS_SOCKS5_VERSION, + AWS_SOCKS5_STATUS_SUCCESS, + AWS_SOCKS5_RESERVED, + AWS_SOCKS5_ATYP_IPV4, + 1, + 1, + 1, + 1, + 0, + 80}; + ASSERT_SUCCESS(s_peer_send(peer_impl, connect_success, sizeof(connect_success))); + + aws_mutex_lock(&handler_context.mutex); + ASSERT_SUCCESS(aws_condition_variable_wait_pred( + &handler_context.condition, &handler_context.mutex, s_socks5_handler_invoked_predicate, &handler_context)); + int handshake_error = handler_context.error_code; + aws_mutex_unlock(&handler_context.mutex); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, handshake_error); + + struct aws_byte_cursor ping_cur = aws_byte_cursor_from_c_str("ping"); + ASSERT_SUCCESS(s_channel_send_cursor(allocator, socks5_slot, AWS_CHANNEL_DIR_WRITE, ping_cur)); + + struct aws_byte_buf forwarded_ping; + ASSERT_SUCCESS(s_peer_wait_for_write(peer_impl, &forwarded_ping)); + ASSERT_UINT_EQUALS(4, forwarded_ping.len); + ASSERT_BIN_ARRAYS_EQUALS("ping", 4, forwarded_ping.buffer, forwarded_ping.len); + aws_byte_buf_clean_up(&forwarded_ping); + + struct aws_byte_cursor pong_cur = aws_byte_cursor_from_c_str("pong"); + ASSERT_SUCCESS(s_channel_send_cursor(allocator, socks5_slot, AWS_CHANNEL_DIR_WRITE, pong_cur)); + + struct aws_byte_buf forwarded_pong; + ASSERT_SUCCESS(s_peer_wait_for_write(peer_impl, &forwarded_pong)); + ASSERT_UINT_EQUALS(4, forwarded_pong.len); + ASSERT_BIN_ARRAYS_EQUALS("pong", 4, forwarded_pong.buffer, forwarded_pong.len); + aws_byte_buf_clean_up(&forwarded_pong); + + aws_channel_shutdown(channel, AWS_OP_SUCCESS); + aws_mutex_lock(&channel_fixture.mutex); + ASSERT_SUCCESS(aws_condition_variable_wait_pred( + &channel_fixture.condition, &channel_fixture.mutex, s_socks5_channel_shutdown_predicate, &channel_fixture)); + aws_mutex_unlock(&channel_fixture.mutex); + if (channel_fixture.shutdown_error != AWS_ERROR_SUCCESS) { + ASSERT_INT_EQUALS(AWS_IO_SOCKET_CLOSED, channel_fixture.shutdown_error); + } + + aws_channel_destroy(channel); + aws_event_loop_destroy(event_loop); + aws_mutex_clean_up(&handler_context.mutex); + aws_condition_variable_clean_up(&handler_context.condition); + aws_mutex_clean_up(&channel_fixture.mutex); + aws_condition_variable_clean_up(&channel_fixture.condition); + aws_socks5_proxy_options_clean_up(&proxy_options); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(socks5_channel_handler_happy_path, s_socks5_channel_handler_happy_path) + +static int s_socks5_channel_handler_greeting_failure(struct aws_allocator *allocator, void *ctx) { + + /* Test handler behavior on malformed greeting response */ + (void)ctx; + + struct aws_event_loop *event_loop = aws_event_loop_new_default(allocator, aws_high_res_clock_get_ticks); + ASSERT_NOT_NULL(event_loop); + ASSERT_SUCCESS(aws_event_loop_run(event_loop)); + + struct socks5_channel_fixture channel_fixture = { + .mutex = AWS_MUTEX_INIT, + .condition = AWS_CONDITION_VARIABLE_INIT, + .setup_completed = false, + .setup_error = AWS_ERROR_SUCCESS, + .shutdown_completed = false, + .shutdown_error = AWS_ERROR_SUCCESS, + }; + + struct aws_channel_options channel_options = { + .on_setup_completed = s_socks5_channel_on_setup_completed, + .setup_user_data = &channel_fixture, + .on_shutdown_completed = s_socks5_channel_on_shutdown_completed, + .shutdown_user_data = &channel_fixture, + .event_loop = event_loop, + }; + + struct aws_channel *channel = aws_channel_new(allocator, &channel_options); + ASSERT_NOT_NULL(channel); + + aws_mutex_lock(&channel_fixture.mutex); + ASSERT_SUCCESS(aws_condition_variable_wait_pred( + &channel_fixture.condition, &channel_fixture.mutex, s_socks5_channel_setup_predicate, &channel_fixture)); + aws_mutex_unlock(&channel_fixture.mutex); + ASSERT_INT_EQUALS(0, channel_fixture.setup_error); + + struct socks5_peer_impl *peer_impl = NULL; + struct aws_channel_handler *peer_handler = s_peer_handler_new(allocator, &peer_impl); + ASSERT_NOT_NULL(peer_handler); + struct aws_channel_slot *peer_slot = NULL; + ASSERT_SUCCESS(s_install_handler(channel, peer_handler, &peer_slot)); + aws_mutex_lock(&peer_impl->mutex); + peer_impl->slot = peer_slot; + aws_mutex_unlock(&peer_impl->mutex); + + struct aws_channel_slot *socks5_slot = NULL; + + struct aws_socks5_proxy_options proxy_options; + ASSERT_SUCCESS(aws_socks5_proxy_options_init( + &proxy_options, allocator, aws_byte_cursor_from_c_str("proxy.example.com"), 1080)); + + struct socks5_handler_context handler_context = { + .mutex = AWS_MUTEX_INIT, + .condition = AWS_CONDITION_VARIABLE_INIT, + .invoked = false, + .error_code = AWS_ERROR_SUCCESS, + }; + + struct aws_channel_handler *socks5_handler = aws_socks5_channel_handler_new( + allocator, + &proxy_options, + aws_byte_cursor_from_c_str("destination.example.com"), + 80, + AWS_SOCKS5_ATYP_DOMAIN, + s_socks5_handler_on_setup_completed, + &handler_context); + ASSERT_NOT_NULL(socks5_handler); + ASSERT_SUCCESS(s_install_handler(channel, socks5_handler, &socks5_slot)); + + ASSERT_SUCCESS(s_channel_run_on_thread(channel, s_start_handshake_on_thread, socks5_handler)); + + struct aws_byte_buf greeting; + ASSERT_SUCCESS(s_peer_wait_for_write(peer_impl, &greeting)); + ASSERT_TRUE(greeting.len >= AWS_SOCKS5_GREETING_MIN_SIZE); + aws_byte_buf_clean_up(&greeting); + + uint8_t invalid_greeting[] = {0x04, AWS_SOCKS5_AUTH_NO_ACCEPTABLE}; + ASSERT_SUCCESS(s_peer_send(peer_impl, invalid_greeting, sizeof(invalid_greeting))); + + aws_mutex_lock(&handler_context.mutex); + ASSERT_SUCCESS(aws_condition_variable_wait_pred( + &handler_context.condition, &handler_context.mutex, s_socks5_handler_invoked_predicate, &handler_context)); + int failure_code = handler_context.error_code; + aws_mutex_unlock(&handler_context.mutex); + ASSERT_INT_EQUALS(AWS_IO_SOCKS5_PROXY_ERROR_MALFORMED_RESPONSE, failure_code); + ASSERT_INT_EQUALS(1, (int)peer_impl->write_count); + + aws_channel_shutdown(channel, AWS_OP_SUCCESS); + aws_mutex_lock(&channel_fixture.mutex); + ASSERT_SUCCESS(aws_condition_variable_wait_pred( + &channel_fixture.condition, &channel_fixture.mutex, s_socks5_channel_shutdown_predicate, &channel_fixture)); + aws_mutex_unlock(&channel_fixture.mutex); + + aws_channel_destroy(channel); + aws_event_loop_destroy(event_loop); + aws_mutex_clean_up(&handler_context.mutex); + aws_condition_variable_clean_up(&handler_context.condition); + aws_mutex_clean_up(&channel_fixture.mutex); + aws_condition_variable_clean_up(&channel_fixture.condition); + aws_socks5_proxy_options_clean_up(&proxy_options); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(socks5_channel_handler_greeting_failure, s_socks5_channel_handler_greeting_failure) + +struct socks5_bootstrap_stub_context { + bool invoked; + const char *host_name; + uint16_t port; + void *user_data; + const struct aws_tls_connection_options *tls_options; +}; + +static struct socks5_bootstrap_stub_context *s_stub_context = NULL; + +/* Stub function for simulating an error on a new socket channel creation */ +static int s_stub_bootstrap_new_socket_channel(struct aws_socket_channel_bootstrap_options *options) { + if (s_stub_context) { + s_stub_context->invoked = true; + s_stub_context->host_name = options->host_name; + s_stub_context->port = options->port; + s_stub_context->user_data = options->user_data; + s_stub_context->tls_options = options->tls_options; + } + aws_raise_error(AWS_ERROR_UNKNOWN); + return AWS_OP_ERR; +} + +static int s_socks5_bootstrap_system_vtable_failure(struct aws_allocator *allocator, void *ctx) { + + // Test socket creation error via vtable during bootstrap + (void)ctx; + + aws_io_library_init(allocator); + + struct aws_event_loop_group *el_group = aws_event_loop_group_new_default(allocator, 1, NULL); + ASSERT_NOT_NULL(el_group); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = el_group, + .host_resolver = NULL, + }; + struct aws_client_bootstrap *client_bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + ASSERT_NOT_NULL(client_bootstrap); + + struct aws_socket_options socket_options = { + .type = AWS_SOCKET_STREAM, + .domain = AWS_SOCKET_IPV4, + .connect_timeout_ms = 1000, + }; + + struct aws_socket_channel_bootstrap_options channel_options; + AWS_ZERO_STRUCT(channel_options); + channel_options.bootstrap = client_bootstrap; + channel_options.host_name = "target.example.com"; + channel_options.port = 443; + channel_options.socket_options = &socket_options; + + struct aws_socks5_proxy_options proxy_options; + ASSERT_SUCCESS(aws_socks5_proxy_options_init( + &proxy_options, allocator, aws_byte_cursor_from_c_str("proxy.local"), 1080)); + + struct socks5_bootstrap_stub_context stub_context; + AWS_ZERO_STRUCT(stub_context); + s_stub_context = &stub_context; + + struct aws_socks5_system_vtable stub_vtable = { + .aws_client_bootstrap_new_socket_channel = s_stub_bootstrap_new_socket_channel, + }; + aws_socks5_channel_handler_set_system_vtable(&stub_vtable); + + ASSERT_ERROR( + AWS_ERROR_UNKNOWN, + aws_client_bootstrap_new_socket_channel_with_socks5(allocator, &channel_options, &proxy_options)); + + ASSERT_TRUE(stub_context.invoked); + ASSERT_STR_EQUALS("proxy.local", stub_context.host_name); + ASSERT_INT_EQUALS(1080, stub_context.port); + ASSERT_NOT_NULL(stub_context.user_data); + ASSERT_NULL(stub_context.tls_options); + + aws_socks5_channel_handler_set_system_vtable(NULL); + s_stub_context = NULL; + + channel_options.host_name = "target.example.com"; + aws_socks5_proxy_options_clean_up(&proxy_options); + aws_client_bootstrap_release(client_bootstrap); + aws_event_loop_group_release(el_group); + + aws_io_library_clean_up(); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(socks5_bootstrap_system_vtable_failure, s_socks5_bootstrap_system_vtable_failure)