diff --git a/.github/workflows/esp_dns__build.yml b/.github/workflows/esp_dns__build.yml index 7e18243434..ea28950cd4 100644 --- a/.github/workflows/esp_dns__build.yml +++ b/.github/workflows/esp_dns__build.yml @@ -13,9 +13,15 @@ jobs: name: Build strategy: matrix: - idf_ver: ["latest", "release-v5.1", "release-v5.2", "release-v5.3", "release-v5.4"] + idf_ver: ["latest", "release-v5.4", "release-v5.5", "release-v6.0"] idf_target: ["esp32"] - test: [ { app: esp_dns_basic, path: "components/esp_dns/examples"}] + test: + - app: esp_dns_basic + path: components/esp_dns/examples + ci_relpath: ../../../ci/build_apps.py + - app: esp_dns_concurrent + path: components/esp_dns/tests/test_apps + ci_relpath: ../../../../ci/build_apps.py include: - idf_ver: "latest" warning: "the choice symbol ETHERNET_PHY_LAN867X\nis deprecated: Please use smi_gpio instead" @@ -31,11 +37,12 @@ jobs: env: EXPECTED_WARNING: ${{ matrix.warning }} shell: bash - working-directory: ${{matrix.test.path}} + working-directory: ${{ matrix.test.path }} run: | - if [[ "${{ matrix.idf_ver }}" == "release-v5.3" || "${{ matrix.idf_ver }}" == "release-v5.4" ]]; then - export EXPECTED_WARNING="unknown kconfig symbol 'LWIP_USE_ESP_GETADDRINFO'" + if [[ "${{ matrix.idf_ver }}" == "release-v5.4" || "${{ matrix.idf_ver }}" == "release-v5.5" ]]; then + export EXPECTED_WARNING="unknown kconfig symbol 'LWIP_USE_ESP_GETADDRINFO' + unknown kconfig symbol 'MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY'" fi . ${IDF_PATH}/export.sh python -m pip install idf-build-apps - python ../../../ci/build_apps.py ./${{ matrix.test.app }} --target ${{ matrix.idf_target }} -vv --preserve-all --pytest-app + python ${{ matrix.test.ci_relpath }} ./${{ matrix.test.app }} --target ${{ matrix.idf_target }} -vv --preserve-all --pytest-app diff --git a/.gitignore b/.gitignore index 4db6959840..9e214de381 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,9 @@ test_multi_heap_host # VS Code Settings .vscode/ +# clangd / LSP cache +.cache/ + # VIM files *.swp *.swo diff --git a/components/esp_dns/Kconfig b/components/esp_dns/Kconfig new file mode 100644 index 0000000000..bcf8fa98da --- /dev/null +++ b/components/esp_dns/Kconfig @@ -0,0 +1,27 @@ +menu "ESP DNS" + + config ESP_DNS_MAX_ANSWER_SCAN + int "Maximum resource records to scan per DNS response" + range 1 128 + default 16 + help + Maximum number of answer-section resource records to walk when parsing + a DNS response. This limit is independent of MAX_ANSWERS (fixed at 1 + until the lwIP external-resolve hook passes addr_cnt). + + Increase this value for long CNAME chains or responses with many + non-address records before the desired A/AAAA records. + + config ESP_DNS_BUFFER_SIZE + int "DNS response/query buffer size in bytes" + range 512 4096 + default 512 + help + Size of stack buffers used for DNS queries and responses in TCP, + DNS-over-TLS, and DNS-over-HTTPS paths. The default of 512 bytes + matches the traditional DNS UDP message size limit from RFC 1035. + + Increase this value if you expect larger responses (for example + with EDNS0), keeping in mind the impact on task stack usage. + +endmenu diff --git a/components/esp_dns/README.md b/components/esp_dns/README.md index b02ada8dcb..9cf655a56a 100644 --- a/components/esp_dns/README.md +++ b/components/esp_dns/README.md @@ -163,6 +163,14 @@ if (ret != 0) { | `server_cert` | SSL server certificate in PEM format | | `alpn_protos` | ALPN protocols for DoH (typically `"h2"`) | +When using the certificate bundle with public DoT/DoH resolvers (e.g. Google, Cloudflare), enable cross-signed chain verification in your project `sdkconfig.defaults`: + +``` +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y +``` + +This is an IDF mbedTLS option (not an esp_dns Kconfig). The bundled example and concurrent test ship with it enabled. Disable it in menuconfig (**Component config → mbedTLS → Certificate Bundle → Support cross-signed certificate verification**) only if you use PEM pinning or a resolver that does not present cross-signed chains. + ### Protocol-Specific Options #### DoH Options @@ -181,8 +189,9 @@ When using secure DNS protocols (DoT and DoH), you have two certificate options: - The UDP DNS protocol implementation relies on the native LWIP DNS resolver. - Transport protocol selection must be configured through `esp_dns_init_xxx()` rather than `getaddrinfo()` parameters due to LWIP resolver hook limitations. -- Maximum response size is limited by the buffer size (default: 512 bytes) for DNS over TLS (DOT) and TCP protocols. +- Maximum response size is limited by `CONFIG_ESP_DNS_BUFFER_SIZE` (default: 512 bytes) for DNS over TLS (DOT) and TCP protocols. - Only one DNS protocol can be active at a time. +- **Multi-IP results (TCP/DoT/DoH):** `dns_resolve_*` accepts `addr_cnt` and clamps it via `esp_dns_clamp_addr_cnt()` to `MAX_ANSWERS` (1). The lwIP hook passes `MAX_ANSWERS` because `LWIP_HOOK_NETCONN_EXTERNAL_RESOLVE` does not yet forward the caller's `addr_cnt`. When lwIP adds that parameter, update `lwip_hook_netconn_external_resolve` to pass it through. - **Resolution Speed**: - UDP DNS is fastest but least secure @@ -213,6 +222,7 @@ Once you add this component to your project, it will replace the default LWIP DN - **Certificate Errors**: - Verify that the correct certificate is provided for secure protocols - For public DNS servers, use the certificate bundle approach + - If you see `No matching trusted root certificate found` when using the certificate bundle (e.g. with `dns.google` or Cloudflare), the server likely uses a cross-signed chain. Add `CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y` to your project `sdkconfig.defaults` (see **TLS Configuration** above). The esp_dns examples enable this by default. - **Timeout Errors**: - Increase the timeout value for slow network connections diff --git a/components/esp_dns/esp_dns_doh.c b/components/esp_dns/esp_dns_doh.c index cc0164fb07..0dfa650501 100644 --- a/components/esp_dns/esp_dns_doh.c +++ b/components/esp_dns/esp_dns_doh.c @@ -99,7 +99,7 @@ esp_err_t esp_dns_http_event_handler(esp_http_client_event_t *evt) { char *temp_buff = NULL; size_t temp_buff_len = 0; - esp_dns_handle_t handle = (esp_dns_handle_t)evt->user_data; + response_buffer_t *response_buffer = (response_buffer_t *)evt->user_data; switch (evt->event_id) { case HTTP_EVENT_ERROR: @@ -117,32 +117,32 @@ esp_err_t esp_dns_http_event_handler(esp_http_client_event_t *evt) case HTTP_EVENT_ON_DATA: ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len); /* Check if buffer is null, if yes, initialize it */ - if (handle->response_buffer.buffer == NULL) { + if (response_buffer->buffer == NULL) { if (evt->data_len == 0) { ESP_LOGW(TAG, "Received empty HTTP data"); return ESP_ERR_INVALID_ARG; } temp_buff = malloc(evt->data_len); if (temp_buff) { - handle->response_buffer.buffer = temp_buff; - handle->response_buffer.length = evt->data_len; - memcpy(handle->response_buffer.buffer, evt->data, evt->data_len); + response_buffer->buffer = temp_buff; + response_buffer->length = evt->data_len; + memcpy(response_buffer->buffer, evt->data, evt->data_len); } else { ESP_LOGE(TAG, "Buffer allocation error"); return ESP_ERR_NO_MEM; } } else { /* Reallocate buffer to hold the new data chunk */ - int new_len = handle->response_buffer.length + evt->data_len; + int new_len = response_buffer->length + evt->data_len; if (new_len == 0) { ESP_LOGW(TAG, "New data length is zero after receiving HTTP data"); return ESP_ERR_INVALID_ARG; } - temp_buff = realloc(handle->response_buffer.buffer, new_len); + temp_buff = realloc(response_buffer->buffer, new_len); if (temp_buff) { - handle->response_buffer.buffer = temp_buff; - memcpy(handle->response_buffer.buffer + handle->response_buffer.length, evt->data, evt->data_len); - handle->response_buffer.length = new_len; + response_buffer->buffer = temp_buff; + memcpy(response_buffer->buffer + response_buffer->length, evt->data, evt->data_len); + response_buffer->length = new_len; } else { ESP_LOGE(TAG, "Buffer allocation error"); return ESP_ERR_NO_MEM; @@ -152,29 +152,30 @@ esp_err_t esp_dns_http_event_handler(esp_http_client_event_t *evt) case HTTP_EVENT_ON_FINISH: ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH"); /* Entire response received, process it here */ - ESP_LOGD(TAG, "Received full response, length: %d", handle->response_buffer.length); + ESP_LOGD(TAG, "Received full response, length: %d", response_buffer->length); /* Check if the buffer indicates an HTTP error response */ if (HttpStatus_Ok == esp_http_client_get_status_code(evt->client)) { - if (handle->response_buffer.length >= sizeof(dns_header_t)) { + if (response_buffer->length >= sizeof(dns_header_t)) { /* Parse the DNS response */ - esp_dns_parse_response((uint8_t *)handle->response_buffer.buffer, - handle->response_buffer.length, - &handle->response_buffer.dns_response); + esp_dns_parse_response((uint8_t *)response_buffer->buffer, + response_buffer->length, + &response_buffer->dns_response, + response_buffer->max_ips); } else { ESP_LOGE(TAG, "DNS response too small"); - handle->response_buffer.dns_response.status_code = ERR_VAL; + response_buffer->dns_response.status_code = ERR_VAL; } } else { ESP_LOGE(TAG, "HTTP Error: %d", esp_http_client_get_status_code(evt->client)); - temp_buff_len = handle->response_buffer.length > ESP_DNS_BUFFER_SIZE ? ESP_DNS_BUFFER_SIZE : handle->response_buffer.length; - ESP_LOG_BUFFER_HEXDUMP(TAG, handle->response_buffer.buffer, temp_buff_len, ESP_LOG_ERROR); - handle->response_buffer.dns_response.status_code = ERR_VAL; /* TBD: Not handled properly yet */ + temp_buff_len = response_buffer->length > ESP_DNS_BUFFER_SIZE ? ESP_DNS_BUFFER_SIZE : response_buffer->length; + ESP_LOG_BUFFER_HEXDUMP(TAG, response_buffer->buffer, temp_buff_len, ESP_LOG_ERROR); + response_buffer->dns_response.status_code = ERR_VAL; /* TBD: Not handled properly yet */ } - free(handle->response_buffer.buffer); - handle->response_buffer.buffer = NULL; - handle->response_buffer.length = 0; + free(response_buffer->buffer); + response_buffer->buffer = NULL; + response_buffer->length = 0; break; case HTTP_EVENT_DISCONNECTED: ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED"); @@ -198,17 +199,24 @@ esp_err_t esp_dns_http_event_handler(esp_http_client_event_t *evt) * @param handle Pointer to the DNS handle * @param name The hostname to resolve * @param addr Pointer to store the resolved IP addresses + * @param addr_cnt Number of address slots in addr (must be > 0) * @param rrtype The address RR type (A or AAAA) * * @return ERR_OK on success, or an error code on failure */ -err_t dns_resolve_doh(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t rrtype) +err_t dns_resolve_doh(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t addr_cnt, u8_t rrtype) { uint8_t buffer_qry[ESP_DNS_BUFFER_SIZE]; /* Initialize error status */ err_t err = ERR_OK; const char *prefix = "https://"; + response_buffer_t response_buffer; + u8_t max_ips = esp_dns_clamp_addr_cnt(addr_cnt); + + if (addr == NULL || max_ips == 0) { + return ERR_ARG; + } /* Set default values for DoH configuration if not specified */ const char *url_path = handle->config.protocol_config.doh_config.url_path ? @@ -238,7 +246,7 @@ err_t dns_resolve_doh(const esp_dns_handle_t handle, const char *name, ip_addr_t .url = dns_server_url, .event_handler = esp_dns_http_event_handler, .method = HTTP_METHOD_POST, - .user_data = handle, + .user_data = &response_buffer, .port = port, }; @@ -249,11 +257,12 @@ err_t dns_resolve_doh(const esp_dns_handle_t handle, const char *name, ip_addr_t config.cert_pem = handle->config.tls_config.cert_pem; /* Use the root certificate for dns.google if needed */ } - /* Clear the response buffer to ensure no residual data remains */ - memset(&handle->response_buffer, 0, sizeof(response_buffer_t)); + /* Keep response state local to this lookup so concurrent queries do not collide. */ + memset(&response_buffer, 0, sizeof(response_buffer_t)); + response_buffer.max_ips = max_ips; /* Create DNS query in wire format */ - size_t query_size = esp_dns_create_query(buffer_qry, sizeof(buffer_qry), name, rrtype, &handle->response_buffer.dns_response.id); + size_t query_size = esp_dns_create_query(buffer_qry, sizeof(buffer_qry), name, rrtype, &response_buffer.dns_response.id); if (query_size == -1) { ESP_LOGE(TAG, "Error: Hostname too big"); err = ERR_MEM; @@ -293,13 +302,13 @@ err_t dns_resolve_doh(const esp_dns_handle_t handle, const char *name, ip_addr_t /* Verify HTTP status code and DNS response status */ if ((HttpStatus_Ok != esp_http_client_get_status_code(client)) || - (handle->response_buffer.dns_response.status_code != ERR_OK)) { + (response_buffer.dns_response.status_code != ERR_OK)) { err = ERR_ARG; goto client_cleanup; } /* Extract IP addresses from DNS response */ - err = esp_dns_extract_ip_addresses_from_response(&handle->response_buffer.dns_response, addr); + err = esp_dns_get_ips_from_response(&response_buffer.dns_response, addr, max_ips); } else { ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(ret)); err = ERR_VAL; diff --git a/components/esp_dns/esp_dns_dot.c b/components/esp_dns/esp_dns_dot.c index 4aaee38948..648ccd649b 100644 --- a/components/esp_dns/esp_dns_dot.c +++ b/components/esp_dns/esp_dns_dot.c @@ -96,7 +96,7 @@ int esp_dns_cleanup_dot(esp_dns_handle_t handle) * * @return ERR_OK on success, or an error code on failure */ -err_t dns_resolve_dot(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t rrtype) +err_t dns_resolve_dot(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t addr_cnt, u8_t rrtype) { int err = ERR_OK; esp_transport_handle_t transport = NULL; @@ -105,8 +105,10 @@ err_t dns_resolve_dot(const esp_dns_handle_t handle, const char *name, ip_addr_t size_t query_size; int timeout_ms; int dot_port; + response_buffer_t response_buffer; + u8_t max_ips = esp_dns_clamp_addr_cnt(addr_cnt); - if (addr == NULL) { + if (addr == NULL || max_ips == 0) { return ERR_ARG; } @@ -114,13 +116,13 @@ err_t dns_resolve_dot(const esp_dns_handle_t handle, const char *name, ip_addr_t timeout_ms = handle->config.timeout_ms ? : ESP_DNS_DEFAULT_TIMEOUT_MS; dot_port = handle->config.port ? : ESP_DNS_DEFAULT_DOT_PORT; - /* Clear the response buffer to ensure no residual data remains */ - memset(&handle->response_buffer, 0, sizeof(response_buffer_t)); + /* Keep response state local to this lookup so concurrent queries do not collide. */ + memset(&response_buffer, 0, sizeof(response_buffer_t)); /* Create DNS query in wire format, leaving 2 bytes at start for length prefix as required by RFC 7858 */ memset(dot_buffer, 0, ESP_DNS_BUFFER_SIZE); query_size = esp_dns_create_query((uint8_t *)(dot_buffer + 2), sizeof(dot_buffer) - 2, - name, rrtype, &handle->response_buffer.dns_response.id); + name, rrtype, &response_buffer.dns_response.id); if (query_size == -1) { ESP_LOGE(TAG, "Error: Hostname too big"); return ERR_MEM; @@ -182,16 +184,17 @@ err_t dns_resolve_dot(const esp_dns_handle_t handle, const char *name, ip_addr_t } /* Skip the 2-byte length field that prepends DNS messages as required by RFC 7858 */ - handle->response_buffer.buffer = dot_buffer + 2; - handle->response_buffer.length = len - 2; + response_buffer.buffer = dot_buffer + 2; + response_buffer.length = len - 2; /* Parse the DNS response */ - esp_dns_parse_response((uint8_t *)handle->response_buffer.buffer, - handle->response_buffer.length, - &handle->response_buffer.dns_response); + esp_dns_parse_response((uint8_t *)response_buffer.buffer, + response_buffer.length, + &response_buffer.dns_response, + max_ips); /* Extract IP addresses from DNS response */ - err = esp_dns_extract_ip_addresses_from_response(&handle->response_buffer.dns_response, addr); + err = esp_dns_get_ips_from_response(&response_buffer.dns_response, addr, max_ips); if (err != ERR_OK) { ESP_LOGE(TAG, "Failed to extract IP address from DNS response"); goto cleanup; diff --git a/components/esp_dns/esp_dns_lwip.c b/components/esp_dns/esp_dns_lwip.c index f46ec8674d..174ad7ef89 100644 --- a/components/esp_dns/esp_dns_lwip.c +++ b/components/esp_dns/esp_dns_lwip.c @@ -37,8 +37,13 @@ extern esp_dns_handle_t g_dns_handle; /** * @brief Custom DNS resolution hook for lwIP network connections * + * LWIP_HOOK_NETCONN_EXTERNAL_RESOLVE passes ip_addr_t *addr but not addr_cnt. + * Callers may provide a single-element buffer (e.g. netconn_gethostbyname with + * addr_cnt == 1) even when CONFIG_LWIP_DNS_MAX_HOST_IP > 1, so dns_resolve_* + * must not write more than MAX_ANSWERS (1) addresses. + * * @param name Hostname to resolve - * @param addr Pointer to store resolved IP address + * @param addr Pointer to store resolved IP address(es) * @param addrtype Type of address to resolve (IPv4/IPv6) * @param err Pointer to store error code * @@ -46,6 +51,7 @@ extern esp_dns_handle_t g_dns_handle; */ int lwip_hook_netconn_external_resolve(const char *name, ip_addr_t *addr, u8_t addrtype, err_t *err) { + const u8_t addr_cnt = MAX_ANSWERS; if (g_dns_handle == NULL) { ESP_LOGD(TAG, "ESP_DNS module not initialized, resolving through native DNS"); *err = ERR_OK; @@ -91,13 +97,13 @@ int lwip_hook_netconn_external_resolve(const char *name, ip_addr_t *addr, u8_t a /* Return zero as lwIP DNS can handle UDP DNS */ return 0; case ESP_DNS_PROTOCOL_TCP: - *err = dns_resolve_tcp(g_dns_handle, name, addr, rrtype); + *err = dns_resolve_tcp(g_dns_handle, name, addr, addr_cnt, rrtype); break; case ESP_DNS_PROTOCOL_DOT: - *err = dns_resolve_dot(g_dns_handle, name, addr, rrtype); + *err = dns_resolve_dot(g_dns_handle, name, addr, addr_cnt, rrtype); break; case ESP_DNS_PROTOCOL_DOH: - *err = dns_resolve_doh(g_dns_handle, name, addr, rrtype); + *err = dns_resolve_doh(g_dns_handle, name, addr, addr_cnt, rrtype); break; default: ESP_LOGE(TAG, "Invalid transport type"); diff --git a/components/esp_dns/esp_dns_priv.h b/components/esp_dns/esp_dns_priv.h index c16d32abcd..fec6fadae7 100644 --- a/components/esp_dns/esp_dns_priv.h +++ b/components/esp_dns/esp_dns_priv.h @@ -42,8 +42,6 @@ struct esp_dns_handle { /* Connection state */ bool initialized; /* Flag indicating successful initialization */ - response_buffer_t response_buffer; /* Buffer for storing DNS response data during processing */ - /* Thread safety */ SemaphoreHandle_t lock; /* Mutex for synchronization */ }; @@ -72,45 +70,49 @@ int esp_dns_cleanup(esp_dns_handle_t handle); * * @param handle DNS module handle * @param name Hostname to resolve - * @param addr Pointer to store resolved IP address + * @param addr Pointer to store resolved IP address(es) + * @param addr_cnt Number of address slots in addr (must be > 0) * @param rrtype Record type (A or AAAA) * * @return err_t Error code */ -err_t dns_resolve_doh(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t rrtype); +err_t dns_resolve_doh(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t addr_cnt, u8_t rrtype); /** * @brief Resolve hostname using DNS over TLS * * @param handle DNS module handle * @param name Hostname to resolve - * @param addr Pointer to store resolved IP address + * @param addr Pointer to store resolved IP address(es) + * @param addr_cnt Number of address slots in addr (must be > 0) * @param rrtype Record type (A or AAAA) * * @return err_t Error code */ -err_t dns_resolve_dot(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t rrtype); +err_t dns_resolve_dot(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t addr_cnt, u8_t rrtype); /** * @brief Resolve hostname using TCP DNS * * @param handle DNS module handle * @param name Hostname to resolve - * @param addr Pointer to store resolved IP address + * @param addr Pointer to store resolved IP address(es) + * @param addr_cnt Number of address slots in addr (must be > 0) * @param rrtype Record type (A or AAAA) * * @return err_t Error code */ -err_t dns_resolve_tcp(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t rrtype); +err_t dns_resolve_tcp(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t addr_cnt, u8_t rrtype); /** * @brief Resolve hostname using UDP DNS * * @param handle DNS module handle * @param name Hostname to resolve - * @param addr Pointer to store resolved IP address + * @param addr Pointer to store resolved IP address(es) + * @param addr_cnt Number of address slots in addr (must be > 0) * @param rrtype Record type (A or AAAA) * * @return err_t Error code */ -err_t dns_resolve_udp(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t rrtype); +err_t dns_resolve_udp(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t addr_cnt, u8_t rrtype); diff --git a/components/esp_dns/esp_dns_tcp.c b/components/esp_dns/esp_dns_tcp.c index 8872d64e2d..4fa2f5955f 100644 --- a/components/esp_dns/esp_dns_tcp.c +++ b/components/esp_dns/esp_dns_tcp.c @@ -88,12 +88,13 @@ int esp_dns_cleanup_tcp(esp_dns_handle_t handle) * * @param handle DNS handle * @param name Hostname to resolve - * @param addr Pointer to store the resolved IP address + * @param addr Pointer to store the resolved IP address(es) + * @param addr_cnt Number of address slots in addr (must be > 0) * @param rrtype DNS record type * * @return ERR_OK on success, error code on failure */ -err_t dns_resolve_tcp(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t rrtype) +err_t dns_resolve_tcp(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t addr_cnt, u8_t rrtype) { int err = ERR_OK; esp_transport_handle_t transport = NULL; @@ -102,8 +103,10 @@ err_t dns_resolve_tcp(const esp_dns_handle_t handle, const char *name, ip_addr_t size_t query_size; int timeout_ms; int tcp_port; + response_buffer_t response_buffer; + u8_t max_ips = esp_dns_clamp_addr_cnt(addr_cnt); - if (addr == NULL) { + if (addr == NULL || max_ips == 0) { return ERR_ARG; } @@ -111,13 +114,13 @@ err_t dns_resolve_tcp(const esp_dns_handle_t handle, const char *name, ip_addr_t timeout_ms = handle->config.timeout_ms ? : ESP_DNS_DEFAULT_TIMEOUT_MS; tcp_port = handle->config.port ? : ESP_DNS_DEFAULT_TCP_PORT; - /* Clear the response buffer to ensure no residual data remains */ - memset(&handle->response_buffer, 0, sizeof(response_buffer_t)); + /* Keep response state local to this lookup so concurrent queries do not collide. */ + memset(&response_buffer, 0, sizeof(response_buffer_t)); /* Create DNS query in wire format, leaving 2 bytes at start for length prefix as required by RFC 7858 */ memset(tcp_buffer, 0, ESP_DNS_BUFFER_SIZE); query_size = esp_dns_create_query((uint8_t *)(tcp_buffer + 2), sizeof(tcp_buffer) - 2, - name, rrtype, &handle->response_buffer.dns_response.id); + name, rrtype, &response_buffer.dns_response.id); if (query_size == -1) { ESP_LOGE(TAG, "Error: Hostname too big"); return ERR_MEM; @@ -165,16 +168,17 @@ err_t dns_resolve_tcp(const esp_dns_handle_t handle, const char *name, ip_addr_t } /* Skip the 2-byte length field that prepends DNS messages as required by RFC 7858 */ - handle->response_buffer.buffer = tcp_buffer + 2; - handle->response_buffer.length = len - 2; + response_buffer.buffer = tcp_buffer + 2; + response_buffer.length = len - 2; /* Parse the DNS response */ - esp_dns_parse_response((uint8_t *)handle->response_buffer.buffer, - handle->response_buffer.length, - &handle->response_buffer.dns_response); + esp_dns_parse_response((uint8_t *)response_buffer.buffer, + response_buffer.length, + &response_buffer.dns_response, + max_ips); /* Extract IP addresses from DNS response */ - err = esp_dns_extract_ip_addresses_from_response(&handle->response_buffer.dns_response, addr); + err = esp_dns_get_ips_from_response(&response_buffer.dns_response, addr, max_ips); if (err != ERR_OK) { ESP_LOGE(TAG, "Failed to extract IP address from DNS response"); goto cleanup; diff --git a/components/esp_dns/esp_dns_udp.c b/components/esp_dns/esp_dns_udp.c index 2eb9f247b5..ed4ca3a816 100644 --- a/components/esp_dns/esp_dns_udp.c +++ b/components/esp_dns/esp_dns_udp.c @@ -94,12 +94,13 @@ int esp_dns_cleanup_udp(esp_dns_handle_t handle) * * @param handle DNS handle * @param name Hostname to resolve - * @param addr Pointer to store the resolved IP address + * @param addr Pointer to store the resolved IP address(es) + * @param addr_cnt Number of address slots in addr (must be > 0) * @param rrtype DNS record type * * @return ERR_OK on success, error code on failure */ -err_t dns_resolve_udp(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t rrtype) +err_t dns_resolve_udp(const esp_dns_handle_t handle, const char *name, ip_addr_t *addr, u8_t addr_cnt, u8_t rrtype) { // TBD: Implement UDP DNS resolution if (addr == NULL) { diff --git a/components/esp_dns/esp_dns_utils.c b/components/esp_dns/esp_dns_utils.c index 021d5d5dc5..8550c48e0e 100644 --- a/components/esp_dns/esp_dns_utils.c +++ b/components/esp_dns/esp_dns_utils.c @@ -137,8 +137,9 @@ static uint8_t *skip_dns_name(uint8_t *ptr, size_t remaining_bytes) * @param response_size Size of the response buffer * * @param dns_response Structure to store parsed response + * @param max_ips Maximum number of IP addresses to collect (must be > 0) */ -void esp_dns_parse_response(uint8_t *buffer, size_t response_size, dns_response_t *dns_response) +void esp_dns_parse_response(uint8_t *buffer, size_t response_size, dns_response_t *dns_response, u8_t max_ips) { /* Validate input buffer and minimum size */ assert(buffer != NULL); @@ -150,6 +151,11 @@ void esp_dns_parse_response(uint8_t *buffer, size_t response_size, dns_response_ return; } + if (max_ips == 0) { + dns_response->status_code = ERR_ARG; + return; + } + uint8_t *buffer_end = buffer + response_size; dns_header_t *header = (dns_header_t *)buffer; @@ -161,8 +167,9 @@ void esp_dns_parse_response(uint8_t *buffer, size_t response_size, dns_response_ return; } - /* Ensure only MAX_ANSWERS are processed */ - dns_response->num_answers = (answer_count < MAX_ANSWERS ? answer_count : MAX_ANSWERS); + /* Ensure we scan up to ESP_DNS_MAX_ANSWER_SCAN answers */ + dns_response->num_answers = 0; + int scan_count = (answer_count < ESP_DNS_MAX_ANSWER_SCAN ? answer_count : ESP_DNS_MAX_ANSWER_SCAN); /* Skip the header and question section */ uint8_t *ptr = buffer + sizeof(dns_header_t); @@ -186,7 +193,10 @@ void esp_dns_parse_response(uint8_t *buffer, size_t response_size, dns_response_ ptr += sizeof(dns_question_t); /* Parse each answer record */ - for (int i = 0; i < dns_response->num_answers; i++) { + for (int i = 0; i < scan_count; i++) { + if (dns_response->num_answers >= max_ips) { + break; + } /* Answer fields */ if (ptr > buffer_end) { @@ -220,24 +230,20 @@ void esp_dns_parse_response(uint8_t *buffer, size_t response_size, dns_response_ /* Validate RR class and ttl */ if ((class != DNS_RRCLASS_IN) || (ttl > DNS_MAX_TTL)) { - dns_response->answers[i].status = ERR_VAL; goto next_answer; } - /* Initialize status for this answer */ - dns_response->answers[i].status = ERR_OK; - /* Check the type of answer */ if (type == DNS_RRTYPE_A && data_len == 4) { /* IPv4 Address (A record) */ - memcpy(&dns_response->answers[i].ip, ptr, sizeof(struct in_addr)); - IP_SET_TYPE(&dns_response->answers[i].ip, IPADDR_TYPE_V4); + memcpy(&dns_response->answers[dns_response->num_answers], ptr, sizeof(struct in_addr)); + IP_SET_TYPE(&dns_response->answers[dns_response->num_answers], IPADDR_TYPE_V4); + dns_response->num_answers++; } else if (type == DNS_RRTYPE_AAAA && data_len == 16) { /* IPv6 Address (AAAA record) */ - memcpy(&dns_response->answers[i].ip, ptr, sizeof(struct in6_addr)); - IP_SET_TYPE(&dns_response->answers[i].ip, IPADDR_TYPE_V6); - } else { - dns_response->answers[i].status = ERR_VAL; + memcpy(&dns_response->answers[dns_response->num_answers], ptr, sizeof(struct in6_addr)); + IP_SET_TYPE(&dns_response->answers[dns_response->num_answers], IPADDR_TYPE_V6); + dns_response->num_answers++; } next_answer: @@ -246,43 +252,55 @@ void esp_dns_parse_response(uint8_t *buffer, size_t response_size, dns_response_ } } +u8_t esp_dns_clamp_addr_cnt(u8_t addr_cnt) +{ + if (addr_cnt == 0) { + return 0; + } + if (addr_cnt > MAX_ANSWERS) { + return MAX_ANSWERS; + } + return addr_cnt; +} + /** - * @brief Converts a dns_response_t to an array of IP addresses. + * @brief Copies parsed IP addresses from a DNS response to an array. * - * This function iterates over the DNS response and extracts valid - * IPv4 and IPv6 addresses, storing them in the provided array. + * This function retrieves the valid IPv4 and IPv6 addresses that were + * previously parsed and stored in the DNS response structure, copying + * them into the provided array. * - * @param response The DNS response to process - * @param ipaddr Array to store the extracted IP addresses + * @param response The parsed DNS response + * @param ipaddr Array to store the copied IP addresses + * @param max_entries Number of slots available in ipaddr (must be > 0) * - * @return err_t Status of DNS response parsing + * @return err_t ERR_OK on success, ERR_ARG if ipaddr is NULL or max_entries is 0, + * or an error code from the response */ -err_t esp_dns_extract_ip_addresses_from_response(const dns_response_t *response, ip_addr_t ipaddr[]) +err_t esp_dns_get_ips_from_response(const dns_response_t *response, ip_addr_t ipaddr[], u8_t max_entries) { - int count = 0; - memset(ipaddr, 0, DNS_MAX_HOST_IP * sizeof(ip_addr_t)); + if (ipaddr == NULL || max_entries == 0) { + return ERR_ARG; + } + + memset(ipaddr, 0, max_entries * sizeof(ip_addr_t)); if (response->status_code != ERR_OK) { return response->status_code; } - /* Iterate over the DNS answers */ - for (int i = 0; i < response->num_answers && count < DNS_MAX_HOST_IP; i++) { - const dns_answer_storage_t *answer = &response->answers[i]; - - /* Check if the answer is valid */ - if (answer->status != ERR_OK) { - continue; - } + if (response->num_answers == 0) { + return ERR_VAL; + } - ipaddr[count] = answer->ip; - count++; + u8_t copy_count = (u8_t)response->num_answers; + if (copy_count > max_entries) { + copy_count = max_entries; } - if (count == 0) { - return ERR_VAL; + for (u8_t i = 0; i < copy_count; i++) { + ipaddr[i] = response->answers[i]; } - /* Store the number of valid IP addresses */ return ERR_OK; } diff --git a/components/esp_dns/esp_dns_utils.h b/components/esp_dns/esp_dns_utils.h index 1884ee4f3c..3af9418047 100644 --- a/components/esp_dns/esp_dns_utils.h +++ b/components/esp_dns/esp_dns_utils.h @@ -63,22 +63,19 @@ typedef struct { /** Maximum TTL value for DNS resource records (one week) */ #define DNS_MAX_TTL 604800 -#ifndef CONFIG_LWIP_DNS_MAX_HOST_IP -#define CONFIG_LWIP_DNS_MAX_HOST_IP 1 -#endif - -/** Maximum number of answers that can be stored */ -#define MAX_ANSWERS (CONFIG_LWIP_DNS_MAX_HOST_IP) - -#define ESP_DNS_BUFFER_SIZE 512 - /** - * @brief Structure to store a single DNS answer + * Maximum IP addresses stored and returned per lookup. + * + * Fixed at 1 because LWIP_HOOK_NETCONN_EXTERNAL_RESOLVE passes ip_addr_t *addr + * without addr_cnt; callers may only provide a single-element buffer. */ -typedef struct { - err_t status; /* Status of the answer */ - ip_addr_t ip; /* IP address from the answer */ -} dns_answer_storage_t; +#define MAX_ANSWERS 1 + +/** Maximum number of resource records to scan in a response (CONFIG_ESP_DNS_MAX_ANSWER_SCAN) */ +#define ESP_DNS_MAX_ANSWER_SCAN CONFIG_ESP_DNS_MAX_ANSWER_SCAN + +/** DNS response/query buffer size in bytes (CONFIG_ESP_DNS_BUFFER_SIZE) */ +#define ESP_DNS_BUFFER_SIZE CONFIG_ESP_DNS_BUFFER_SIZE /** * @brief Structure to store a complete DNS response @@ -87,7 +84,7 @@ typedef struct { err_t status_code; /* Overall status of the DNS response */ uint16_t id; /* Transaction ID */ int num_answers; /* Number of valid answers */ - dns_answer_storage_t answers[MAX_ANSWERS]; /* Array of answers */ + ip_addr_t answers[MAX_ANSWERS]; /* Array of IP addresses */ } dns_response_t; /** @@ -96,6 +93,7 @@ typedef struct { typedef struct { char *buffer; /* Pointer to response data buffer */ int length; /* Current length of data in buffer */ + u8_t max_ips; /* Max addresses to collect/return for this lookup */ dns_response_t dns_response; /* Parsed DNS response information */ } response_buffer_t; @@ -118,21 +116,33 @@ size_t esp_dns_create_query(uint8_t *buffer, size_t buffer_size, const char *hos * @param buffer Buffer containing the DNS response * @param response_size Size of the response * @param dns_response Structure to store parsed response + * @param max_ips Maximum number of IP addresses to collect (must be > 0) + */ +void esp_dns_parse_response(uint8_t *buffer, size_t response_size, dns_response_t *dns_response, u8_t max_ips); + +/** + * @brief Clamps addr_cnt to a valid value for esp_dns resolution + * + * @param addr_cnt Number of address slots requested by the caller + * @return Clamped count in range 1..MAX_ANSWERS, or 0 if addr_cnt is 0 */ -void esp_dns_parse_response(uint8_t *buffer, size_t response_size, dns_response_t *dns_response); +u8_t esp_dns_clamp_addr_cnt(u8_t addr_cnt); /** - * @brief Converts a dns_response_t to an array of IP addresses. + * @brief Copies parsed IP addresses from a DNS response to an array. * - * This function iterates over the DNS response and extracts valid - * IPv4 and IPv6 addresses, storing them in the provided array. + * This function retrieves the valid IPv4 and IPv6 addresses that were + * previously parsed and stored in the DNS response structure, copying + * them into the provided array. * - * @param response The DNS response to process. - * @param ipaddr An array to store the extracted IP addresses. + * @param response The parsed DNS response + * @param ipaddr Array to store the copied IP addresses + * @param max_entries Number of slots available in ipaddr (must be > 0) * - * @return err Status of dns response parsing + * @return err_t ERR_OK on success, ERR_ARG if ipaddr is NULL or max_entries is 0, + * or an error code from the response */ -err_t esp_dns_extract_ip_addresses_from_response(const dns_response_t *response, ip_addr_t ipaddr[]); +err_t esp_dns_get_ips_from_response(const dns_response_t *response, ip_addr_t ipaddr[], u8_t max_entries); #ifdef __cplusplus } diff --git a/components/esp_dns/examples/esp_dns_basic/README.md b/components/esp_dns/examples/esp_dns_basic/README.md index 5f2acca32c..60ad3a092a 100644 --- a/components/esp_dns/examples/esp_dns_basic/README.md +++ b/components/esp_dns/examples/esp_dns_basic/README.md @@ -63,10 +63,11 @@ See the Getting Started Guide for full steps to configure and use ESP-IDF to bui Ensure that the network connection details are accurate. For example, verify the Wi-Fi SSID and password or check that the Ethernet connection is secure and not faulty. * **Memory Issues**: - If you encounter memory-related errors, check the system information output which displays free heap and stack high water mark. You may need to increase task stack sizes for more complex DNS operations. + If you encounter memory-related errors, check the system information output which displays free heap and stack high water mark. You may need to increase task stack sizes for more complex DNS operations. The `getaddrinfo()` worker stack is configurable via menuconfig (**Example Configuration → getaddrinfo worker task stack size**); default is 8192 bytes. * **Certificate Issues**: For DoT and DoH protocols, ensure that the certificates are valid for the DNS server you're using. The example includes Google DNS certificates, but these may need to be updated if they expire. + Cross-signed chain verification is enabled by default in this example's `sdkconfig.defaults` (`CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y`). If you still see `No matching trusted root certificate found` with the certificate bundle, confirm that option is present in your active `sdkconfig`. To opt out, remove it from `sdkconfig.defaults` or disable it in menuconfig (**Component config → mbedTLS → Certificate Bundle → Support cross-signed certificate verification**). ## Example Output diff --git a/components/esp_dns/examples/esp_dns_basic/main/Kconfig.projbuild b/components/esp_dns/examples/esp_dns_basic/main/Kconfig.projbuild new file mode 100644 index 0000000000..eef13b3509 --- /dev/null +++ b/components/esp_dns/examples/esp_dns_basic/main/Kconfig.projbuild @@ -0,0 +1,11 @@ +menu "Example Configuration" + + config EXAMPLE_DNS_ADDRINFO_TASK_STACK + int "getaddrinfo worker task stack size (bytes)" + range 4096 32768 + default 8192 + help + Stack size for the FreeRTOS task that runs getaddrinfo() in this example. + DoT/DoH resolution pulls in mbedTLS; 4 KiB is too small on typical builds. + +endmenu diff --git a/components/esp_dns/examples/esp_dns_basic/main/esp_dns_example.c b/components/esp_dns/examples/esp_dns_basic/main/esp_dns_example.c index abfdcbee39..566ab4afb7 100644 --- a/components/esp_dns/examples/esp_dns_basic/main/esp_dns_example.c +++ b/components/esp_dns/examples/esp_dns_basic/main/esp_dns_example.c @@ -17,8 +17,9 @@ #include "nvs_flash.h" #include "esp_event.h" #include "esp_timer.h" +#include "sdkconfig.h" #include "lwip/opt.h" -#include "protocol_examples_common.h" +#include "net_connect.h" #include "esp_dns.h" #if defined(CONFIG_MBEDTLS_CERTIFICATE_BUNDLE) #include "esp_crt_bundle.h" @@ -31,9 +32,6 @@ #define TAG "example_esp_dns" -/* DoH runs TLS + esp_http_client on the caller task; 4 KB overflows on IDF 6.x */ -#define ADDR_INFO_TASK_STACK_SIZE (8 * 1024) - extern const char server_root_cert_pem_start[] asm("_binary_cert_google_root_pem_start"); extern const char server_root_cert_pem_end[] asm("_binary_cert_google_root_pem_end"); @@ -140,7 +138,7 @@ static void run_dns_query_task(void) { TaskHandle_t task_handle = NULL; TaskHandle_t parent_handle = xTaskGetCurrentTaskHandle(); - xTaskCreate(addr_info_task, "AddressInfo", ADDR_INFO_TASK_STACK_SIZE, parent_handle, 5, &task_handle); + xTaskCreate(addr_info_task, "AddressInfo", CONFIG_EXAMPLE_DNS_ADDRINFO_TASK_STACK, parent_handle, 5, &task_handle); /* Wait for task to complete */ if (task_handle != NULL) { @@ -315,7 +313,7 @@ void app_main(void) * Read "Establishing Wi-Fi or Ethernet Connection" section in * examples/protocols/README.md for more information about this function. */ - ESP_ERROR_CHECK(example_connect()); + ESP_ERROR_CHECK(net_connect()); /* Test Without ESP_DNS module */ ESP_LOGI(TAG, "Executing DNS without initializing ESP_DNS module"); diff --git a/components/esp_dns/examples/esp_dns_basic/main/idf_component.yml b/components/esp_dns/examples/esp_dns_basic/main/idf_component.yml index 0f902b6b01..20fd191f1c 100644 --- a/components/esp_dns/examples/esp_dns_basic/main/idf_component.yml +++ b/components/esp_dns/examples/esp_dns_basic/main/idf_component.yml @@ -1,8 +1,7 @@ dependencies: idf: - version: ">=5.1" - protocol_examples_common: - path: ${IDF_PATH}/examples/common_components/protocol_examples_common + version: ">=5.4.3" + espressif/net_connect: "^0.1.1" esp_dns: version: "*" override_path: '../../../' diff --git a/components/esp_dns/examples/esp_dns_basic/sdkconfig.defaults b/components/esp_dns/examples/esp_dns_basic/sdkconfig.defaults index 7684c687c3..7e1f6e4666 100644 --- a/components/esp_dns/examples/esp_dns_basic/sdkconfig.defaults +++ b/components/esp_dns/examples/esp_dns_basic/sdkconfig.defaults @@ -5,3 +5,9 @@ CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_LWIP_DNS_MAX_HOST_IP=4 CONFIG_LWIP_USE_ESP_GETADDRINFO=y CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM=y +# Google Public DNS (and many other sites) present cross-signed chains; without this, +# esp_crt_bundle may log "No matching trusted root certificate found" while PEM pinning works. +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y +CONFIG_EXAMPLE_DNS_ADDRINFO_TASK_STACK=8192 diff --git a/components/esp_dns/tests/test_apps/esp_dns_concurrent/CMakeLists.txt b/components/esp_dns/tests/test_apps/esp_dns_concurrent/CMakeLists.txt new file mode 100644 index 0000000000..66041264f1 --- /dev/null +++ b/components/esp_dns/tests/test_apps/esp_dns_concurrent/CMakeLists.txt @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# +# SPDX-License-Identifier: CC0-1.0 +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(esp_dns_concurrent_test) diff --git a/components/esp_dns/tests/test_apps/esp_dns_concurrent/README.md b/components/esp_dns/tests/test_apps/esp_dns_concurrent/README.md new file mode 100644 index 0000000000..ed140194be --- /dev/null +++ b/components/esp_dns/tests/test_apps/esp_dns_concurrent/README.md @@ -0,0 +1,45 @@ +# esp_dns concurrent resolver test + +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- | -------- | + +This test app stress-tests **`getaddrinfo()`** while the **esp_dns** component owns the lwIP **external resolve** hook. It exercises concurrent DoT/DoH lookups to catch regressions in per-query resolver state. + +## What it does + +1. Connects Wi-Fi or Ethernet via the **net_connect** component (`net_connect()`). +2. Initializes **Cloudflare** DNS-over-TLS (`1dot1dot1dot1.cloudflare-dns.com:853`) with the **certificate bundle**. +3. Starts several FreeRTOS tasks that call `getaddrinfo()` on real hostnames at the same time; then prints success/failure counts. +4. Repeats the same pattern for **DNS-over-HTTPS** (`1dot1dot1dot1.cloudflare-dns.com:443`, path `dns-query`). +5. Logs summary lines when lookup failures occur in a batch. + +Tunable defines in `main/esp_dns_concurrent_test.c`: + +- `NUM_WORKERS` — parallel tasks. +- `ITERATIONS_PER_TASK` — lookups per task after synchronized start. +- `WORKER_STACK_WORDS` — FreeRTOS stack depth (words). + +## Requirements + +- `CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM=y` (set in `sdkconfig.defaults`). +- `CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y` for TLS to Cloudflare without a custom PEM. + +## Build and flash + +```bash +cd /path/to/esp-protocols/components/esp_dns/tests/test_apps/esp_dns_concurrent +idf.py set-target esp32c6 +idf.py -p PORT flash monitor +``` + +## Expected output + +- Many successful hostname lines under concurrent DoT/DoH. +- On failure batches, component logs may include **`ESP_DNS_DOT`** / **`ESP_DNS_DOH`** errors (e.g. failed to extract IP). + +If you see unexpected failures after the concurrency fix, increase **`NUM_WORKERS`** and **`ITERATIONS_PER_TASK`** and run again. + +## Troubleshooting + +- **Network**: configure Wi-Fi/Ethernet credentials in **net_connect** options in `menuconfig`. +- **Certificate**: ensure certificate bundle options in `sdkconfig.defaults` match your IDF version (`menuconfig` → mbedTLS → certificate bundle). Cross-signed chain verification is enabled by default in this test app's `sdkconfig.defaults`. Disable it in menuconfig only if you do not need cross-signed chain support. diff --git a/components/esp_dns/tests/test_apps/esp_dns_concurrent/main/CMakeLists.txt b/components/esp_dns/tests/test_apps/esp_dns_concurrent/main/CMakeLists.txt new file mode 100644 index 0000000000..916999f89e --- /dev/null +++ b/components/esp_dns/tests/test_apps/esp_dns_concurrent/main/CMakeLists.txt @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# +# SPDX-License-Identifier: CC0-1.0 +idf_component_register(SRCS "esp_dns_concurrent_test.c" + INCLUDE_DIRS ".") diff --git a/components/esp_dns/tests/test_apps/esp_dns_concurrent/main/esp_dns_concurrent_test.c b/components/esp_dns/tests/test_apps/esp_dns_concurrent/main/esp_dns_concurrent_test.c new file mode 100644 index 0000000000..bd58543e45 --- /dev/null +++ b/components/esp_dns/tests/test_apps/esp_dns_concurrent/main/esp_dns_concurrent_test.c @@ -0,0 +1,412 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + * + * Stress-test concurrent getaddrinfo() while esp_dns owns the lwIP resolver hook (DoT / DoH). + * Intended to surface Issue 1 (singleton shared state) and Issue 4 (DoH shared response buffer). + */ +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "freertos/event_groups.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "nvs_flash.h" +#include "esp_event.h" +#include "esp_crt_bundle.h" +#include "net_connect.h" +#include "esp_dns.h" + +#define TAG "esp_dns_concurrent_test" + +/** Number of worker tasks started together (tune up to stress the singleton resolver). */ +#define NUM_WORKERS 3 + +/** Lookups each worker performs after the synchronized start. */ +#define ITERATIONS_PER_TASK 25 + +/** + * ESP-IDF's xTaskCreate() stack depth is in bytes. + * Secure DNS + getaddrinfo can use a large stack due to TLS/HTTP parsing. + */ +#define DNS_TASK_STACK_BYTES 8192 + +#define START_BIT BIT0 + +static const char *s_hostnames[] = { + "www.cloudflare.com", + "www.google.com", + "example.com", + "one.one.one.one", +}; + +#define HOSTNAME_COUNT (sizeof(s_hostnames) / sizeof(s_hostnames[0])) + +typedef struct { + bool is_doh; + int worker_id; +} worker_param_t; + +typedef struct { + bool is_doh; + bool ok; + SemaphoreHandle_t done_sem; +} warmup_param_t; + +static EventGroupHandle_t s_start_event; +static SemaphoreHandle_t s_join_sem; + +static worker_param_t s_worker_params[NUM_WORKERS]; + +static portMUX_TYPE s_stats_mux = portMUX_INITIALIZER_UNLOCKED; +static uint32_t s_attempts; +static uint32_t s_success; +static uint32_t s_failures; +static bool s_have_first_fail; +static int s_first_fail_status; +static const char *s_first_fail_host; + +static void reset_stats(void) +{ + portENTER_CRITICAL(&s_stats_mux); + s_attempts = 0; + s_success = 0; + s_failures = 0; + s_have_first_fail = false; + s_first_fail_status = 0; + s_first_fail_host = NULL; + portEXIT_CRITICAL(&s_stats_mux); +} + +static void record_attempt(void) +{ + portENTER_CRITICAL(&s_stats_mux); + s_attempts++; + portEXIT_CRITICAL(&s_stats_mux); +} + +static void record_success(void) +{ + portENTER_CRITICAL(&s_stats_mux); + s_success++; + portEXIT_CRITICAL(&s_stats_mux); +} + +static void record_failure(int status, const char *host) +{ + portENTER_CRITICAL(&s_stats_mux); + s_failures++; + if (!s_have_first_fail) { + s_have_first_fail = true; + s_first_fail_status = status; + s_first_fail_host = host; + } + portEXIT_CRITICAL(&s_stats_mux); +} + +static void dns_worker(void *arg) +{ + worker_param_t *param = (worker_param_t *)arg; + const char *proto = param->is_doh ? "DoH" : "DoT"; + const int wid = param->worker_id; + + (void)xEventGroupWaitBits(s_start_event, START_BIT, pdFALSE, pdTRUE, portMAX_DELAY); + + for (int n = 0; n < ITERATIONS_PER_TASK; n++) { + const char *host = s_hostnames[(n + wid) % HOSTNAME_COUNT]; + struct addrinfo hints; + struct addrinfo *res = NULL; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + + record_attempt(); + const int st = getaddrinfo(host, NULL, &hints, &res); + if (st != 0) { + record_failure(st, host); + ESP_LOGW(TAG, "[%s worker %d] getaddrinfo(%s) failed: %d", proto, wid, host, st); + } else { + record_success(); + freeaddrinfo(res); + } + } + + (void)xSemaphoreGive(s_join_sem); + vTaskDelete(NULL); +} + +static void print_summary(const char *mode_label, bool is_doh_mode) +{ + uint32_t att; + uint32_t ok; + uint32_t fail; + int first_st; + const char *first_host; + + portENTER_CRITICAL(&s_stats_mux); + att = s_attempts; + ok = s_success; + fail = s_failures; + first_st = s_first_fail_status; + first_host = s_first_fail_host; + portEXIT_CRITICAL(&s_stats_mux); + + ESP_LOGI(TAG, "=== %s summary ===", mode_label); + ESP_LOGI(TAG, "attempts=%lu success=%lu failures=%lu", + (unsigned long)att, (unsigned long)ok, (unsigned long)fail); + if (fail > 0) { + ESP_LOGW(TAG, "First failure: status=%d host=%s", first_st, first_host ? first_host : "?"); + if (!is_doh_mode) { + ESP_LOGW(TAG, "Issue 1 reproduced: concurrent DoT getaddrinfo failures observed (singleton shared state)."); + } else { + ESP_LOGW(TAG, "Issue 4 reproduced: concurrent DoH getaddrinfo failures observed (shared HTTP response buffer)."); + ESP_LOGW(TAG, "Note: DoH also uses singleton handle fields; Issue 1 may contribute."); + } + } else { + ESP_LOGI(TAG, "No failures in this run (try increasing NUM_WORKERS / ITERATIONS_PER_TASK)."); + } +} + +static esp_err_t prepare_sync_objects(void) +{ + if (s_start_event == NULL) { + s_start_event = xEventGroupCreate(); + } + if (s_join_sem == NULL) { + s_join_sem = xSemaphoreCreateCounting(NUM_WORKERS, 0); + } + if (s_start_event == NULL || s_join_sem == NULL) { + ESP_LOGE(TAG, "Failed to create sync objects"); + return ESP_ERR_NO_MEM; + } + (void)xEventGroupClearBits(s_start_event, START_BIT); + return ESP_OK; +} + +static void teardown_sync_objects(void) +{ + if (s_join_sem != NULL) { + vSemaphoreDelete(s_join_sem); + s_join_sem = NULL; + } + if (s_start_event != NULL) { + vEventGroupDelete(s_start_event); + s_start_event = NULL; + } +} + +/** + * @return true if we should wait for @p started tasks on s_join_sem + */ +static bool run_concurrent_workers(bool is_doh) +{ + char task_name[12]; + int started = 0; + + if (prepare_sync_objects() != ESP_OK) { + return false; + } + + for (int i = 0; i < NUM_WORKERS; i++) { + snprintf(task_name, sizeof(task_name), "dns_%d", i); + s_worker_params[i].is_doh = is_doh; + s_worker_params[i].worker_id = i; + BaseType_t ok = xTaskCreate(dns_worker, task_name, DNS_TASK_STACK_BYTES, + &s_worker_params[i], tskIDLE_PRIORITY + 5, NULL); + if (ok != pdPASS) { + ESP_LOGE(TAG, "xTaskCreate failed for worker %d", i); + break; + } + started++; + } + + if (started == 0) { + ESP_LOGE(TAG, "No worker tasks started"); + teardown_sync_objects(); + return false; + } + + /* Let workers block on the start event before we signal. */ + vTaskDelay(pdMS_TO_TICKS(150)); + (void)xEventGroupSetBits(s_start_event, START_BIT); + + for (int i = 0; i < started; i++) { + (void)xSemaphoreTake(s_join_sem, portMAX_DELAY); + } + + teardown_sync_objects(); + return true; +} + +/** + * Ensure the secure resolver path works with a single synchronous lookup before workers start. + */ +static bool dns_warmup_lookup_once(bool is_doh) +{ + const char *proto = is_doh ? "DoH" : "DoT"; + struct addrinfo hints; + struct addrinfo *res = NULL; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + + for (size_t i = 0; i < HOSTNAME_COUNT; i++) { + const char *host = s_hostnames[i]; + const int st = getaddrinfo(host, NULL, &hints, &res); + if (st == 0) { + freeaddrinfo(res); + ESP_LOGI(TAG, "[%s] Warmup OK: getaddrinfo(%s)", proto, host); + return true; + } + ESP_LOGW(TAG, "[%s] Warmup: getaddrinfo(%s) failed: %d", proto, host, st); + } + ESP_LOGE(TAG, "[%s] Warmup failed: no successful DNS query", proto); + return false; +} + +static void dns_warmup_task(void *arg) +{ + warmup_param_t *param = (warmup_param_t *)arg; + + param->ok = dns_warmup_lookup_once(param->is_doh); + (void)xSemaphoreGive(param->done_sem); + vTaskDelete(NULL); +} + +static bool dns_warmup_one_success(bool is_doh) +{ + const char *proto = is_doh ? "DoH" : "DoT"; + SemaphoreHandle_t done_sem = xSemaphoreCreateBinary(); + if (done_sem == NULL) { + ESP_LOGE(TAG, "[%s] Warmup failed: no semaphore", proto); + return false; + } + + warmup_param_t param = { + .is_doh = is_doh, + .ok = false, + .done_sem = done_sem, + }; + + BaseType_t task_ok = xTaskCreate(dns_warmup_task, "dns_warmup", DNS_TASK_STACK_BYTES, + ¶m, tskIDLE_PRIORITY + 5, NULL); + if (task_ok != pdPASS) { + ESP_LOGE(TAG, "[%s] Warmup failed: xTaskCreate failed", proto); + vSemaphoreDelete(done_sem); + return false; + } + + (void)xSemaphoreTake(done_sem, portMAX_DELAY); + vSemaphoreDelete(done_sem); + return param.ok; +} + +static esp_err_t run_mode_dot(void) +{ + esp_dns_config_t cfg = { + .dns_server = "1dot1dot1dot1.cloudflare-dns.com", + .port = ESP_DNS_DEFAULT_DOT_PORT, + .timeout_ms = ESP_DNS_DEFAULT_TIMEOUT_MS, + .tls_config = { + .crt_bundle_attach = esp_crt_bundle_attach, + }, + }; + + esp_dns_handle_t h = esp_dns_init_dot(&cfg); + if (h == NULL) { + ESP_LOGE(TAG, "esp_dns_init_dot failed"); + return ESP_FAIL; + } + + if (!dns_warmup_one_success(false)) { + esp_dns_cleanup_dot(h); + return ESP_FAIL; + } + + reset_stats(); + ESP_LOGI(TAG, "Starting DoT concurrent stress (%d tasks x %d lookups)...", NUM_WORKERS, ITERATIONS_PER_TASK); + + if (!run_concurrent_workers(false)) { + esp_dns_cleanup_dot(h); + return ESP_FAIL; + } + + print_summary("DoT concurrency", false); + + esp_dns_cleanup_dot(h); + return ESP_OK; +} + +static esp_err_t run_mode_doh(void) +{ + esp_dns_config_t cfg = { + .dns_server = "1dot1dot1dot1.cloudflare-dns.com", + .port = ESP_DNS_DEFAULT_DOH_PORT, + .timeout_ms = ESP_DNS_DEFAULT_TIMEOUT_MS, + .protocol_config.doh_config.url_path = "dns-query", + .tls_config = { + .crt_bundle_attach = esp_crt_bundle_attach, + }, + }; + + esp_dns_handle_t h = esp_dns_init_doh(&cfg); + if (h == NULL) { + ESP_LOGE(TAG, "esp_dns_init_doh failed"); + return ESP_FAIL; + } + + if (!dns_warmup_one_success(true)) { + esp_dns_cleanup_doh(h); + return ESP_FAIL; + } + + reset_stats(); + ESP_LOGI(TAG, "Starting DoH concurrent stress (%d tasks x %d lookups)...", NUM_WORKERS, ITERATIONS_PER_TASK); + + if (!run_concurrent_workers(true)) { + esp_dns_cleanup_doh(h); + return ESP_FAIL; + } + + print_summary("DoH concurrency", true); + + esp_dns_cleanup_doh(h); + return ESP_OK; +} + +void app_main(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ESP_ERROR_CHECK(net_connect()); + + ESP_LOGI(TAG, "Network ready. Cloudflare DoT/DoH + concurrent getaddrinfo stress test."); + + if (run_mode_dot() != ESP_OK) { + ESP_LOGE(TAG, "DoT mode failed to complete"); + } + + vTaskDelay(pdMS_TO_TICKS(500)); + + if (run_mode_doh() != ESP_OK) { + ESP_LOGE(TAG, "DoH mode failed to complete"); + } + + ESP_LOGI(TAG, "Done. Check logs above for failure counts and Issue 1 / Issue 4 lines."); +} diff --git a/components/esp_dns/tests/test_apps/esp_dns_concurrent/main/idf_component.yml b/components/esp_dns/tests/test_apps/esp_dns_concurrent/main/idf_component.yml new file mode 100644 index 0000000000..8b400861fa --- /dev/null +++ b/components/esp_dns/tests/test_apps/esp_dns_concurrent/main/idf_component.yml @@ -0,0 +1,7 @@ +dependencies: + idf: + version: ">=5.4.3" + espressif/net_connect: "^0.1.1" + esp_dns: + version: "*" + override_path: '../../../../' diff --git a/components/esp_dns/tests/test_apps/esp_dns_concurrent/sdkconfig.defaults b/components/esp_dns/tests/test_apps/esp_dns_concurrent/sdkconfig.defaults new file mode 100644 index 0000000000..b94b5b8fee --- /dev/null +++ b/components/esp_dns/tests/test_apps/esp_dns_concurrent/sdkconfig.defaults @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# +# SPDX-License-Identifier: CC0-1.0 +# +# Minimal options so esp_dns lwIP hook and getaddrinfo stress test work. +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y +CONFIG_LWIP_DNS_MAX_HOST_IP=4 +CONFIG_LWIP_USE_ESP_GETADDRINFO=y +CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y