From 708d49f05805c9a729fd088f986ee0911a0bfa29 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 2 Oct 2025 12:46:44 +0200 Subject: [PATCH 1/3] fix(mdns): Add fuzzing into mdns CI --- .github/workflows/mdns__host-tests.yml | 40 +++++++++++++++++++ .../mdns/tests/test_afl_fuzz_host/Makefile | 19 +++++++-- .../tests/test_afl_fuzz_host/esp32_mock.h | 9 +++-- .../mdns/tests/test_afl_fuzz_host/test.c | 19 ++------- 4 files changed, 65 insertions(+), 22 deletions(-) diff --git a/.github/workflows/mdns__host-tests.yml b/.github/workflows/mdns__host-tests.yml index 19733621aa..759a743574 100644 --- a/.github/workflows/mdns__host-tests.yml +++ b/.github/workflows/mdns__host-tests.yml @@ -68,3 +68,43 @@ jobs: diff -q $file /tmp/$file || exit 1 echo "OK" done + + fuzz_test: + if: contains(github.event.pull_request.labels.*.name, 'mdns-fuzz') || github.event_name == 'push' + name: Fuzzer tests for mdns lib + strategy: + matrix: + idf_ver: ["latest"] + + runs-on: ubuntu-22.04 + container: aflplusplus/aflplusplus + steps: + - name: Checkout esp-protocols + uses: actions/checkout@v4 + + - name: Checkout ESP-IDF + uses: actions/checkout@v4 + with: + repository: espressif/esp-idf + path: idf + submodules: recursive + + - name: Install Necessary Libs + run: | + apt-get update -y + apt-get install -y libbsd-dev + + - name: Run AFL++ + shell: bash + run: | + export IDF_PATH=$GITHUB_WORKSPACE/idf + cd components/mdns/tests/test_afl_fuzz_host/ + make fuzz + + - name: Upload Crash Artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: fuzz-crashes + path: components/mdns/tests/test_afl_fuzz_host/out/default/crashes.tar.gz + if-no-files-found: ignore diff --git a/components/mdns/tests/test_afl_fuzz_host/Makefile b/components/mdns/tests/test_afl_fuzz_host/Makefile index 0eac9ae83b..c9f08b1066 100644 --- a/components/mdns/tests/test_afl_fuzz_host/Makefile +++ b/components/mdns/tests/test_afl_fuzz_host/Makefile @@ -1,7 +1,9 @@ +#INSTR=off TEST_NAME=test FUZZ=afl-fuzz COMPONENTS_DIR=$(IDF_PATH)/components -COMPILER_ICLUDE_DIR=$(shell echo `which xtensa-esp32-elf-gcc | xargs dirname | xargs dirname`/xtensa-esp32-elf) +# Use ESP32 toolchain include path if available, otherwise fall back to system includes for host-based compilation +COMPILER_INCLUDE_DIR=$(shell if command -v xtensa-esp32-elf-gcc >/dev/null 2>&1; then echo `which xtensa-esp32-elf-gcc | xargs dirname | xargs dirname`/xtensa-esp32-elf; else echo /usr; fi) CFLAGS=-g -Wno-unused-value -Wno-missing-declarations -Wno-pointer-bool-conversion -Wno-macro-redefined -Wno-int-to-void-pointer-cast -DHOOK_MALLOC_FAILED -DESP_EVENT_H_ -D__ESP_LOG_H__ \ -I. -I../.. -I../../include -I../../private_include -I ./build/config \ @@ -35,7 +37,7 @@ CFLAGS=-g -Wno-unused-value -Wno-missing-declarations -Wno-pointer-bool-conversi -I$(COMPONENTS_DIR)/xtensa/include \ -I$(COMPONENTS_DIR)/xtensa/esp32/include \ -I$(COMPONENTS_DIR)/esp_hw_support/etm/include \ - -I$(COMPILER_ICLUDE_DIR)/include + -I$(COMPILER_INCLUDE_DIR)/include MDNS_C_DEPENDENCY_INJECTION=-include mdns_di.h @@ -77,7 +79,18 @@ $(TEST_NAME): $(OBJECTS) @$(LD) $(OBJECTS) -o $@ $(LDLIBS) fuzz: $(TEST_NAME) - @$(FUZZ) -i "in" -o "out" -- ./$(TEST_NAME) + # timeout returns 124 if time limit is reached, original return code otherwise + # pass only if: fuzzing was running smoothly until timeout AND no crash found + @timeout 10m $(FUZZ) -i "in" -o "out" -- ./$(TEST_NAME) || \ + if [ $$? -eq 124 ]; then \ + if [ -n "$$(find out/default/crashes -type f 2>/dev/null)" ]; then \ + echo "Crashes found!"; \ + tar -czf out/default/crashes.tar.gz -C out/default crashes; \ + exit 1; \ + fi \ + else \ + exit 1; \ + fi clean: @rm -rf *.o *.SYM $(TEST_NAME) out diff --git a/components/mdns/tests/test_afl_fuzz_host/esp32_mock.h b/components/mdns/tests/test_afl_fuzz_host/esp32_mock.h index 68a3461058..70a2037ffa 100644 --- a/components/mdns/tests/test_afl_fuzz_host/esp32_mock.h +++ b/components/mdns/tests/test_afl_fuzz_host/esp32_mock.h @@ -55,8 +55,7 @@ #define pdMS_TO_TICKS(a) a #define xSemaphoreTake(s,d) true -#define xTaskDelete(a) -#define vTaskDelete(a) free(a) +#define vTaskDelete(a) free(NULL) #define xSemaphoreGive(s) #define xQueueCreateMutex(s) #define _mdns_pcb_init(a,b) true @@ -66,7 +65,7 @@ #define vSemaphoreDelete(s) free(s) #define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U #define xTaskCreatePinnedToCore(a,b,c,d,e,f,g) *(f) = malloc(1) -#define xTaskCreateStaticPinnedToCore(a,b,c,d,e,f,g,h) true +#define xTaskCreateStaticPinnedToCore(a,b,c,d,e,f,g,h) ((void*)1) #define vTaskDelay(m) usleep((m)*0) #define esp_random() (rand()%UINT32_MAX) @@ -139,4 +138,8 @@ TaskHandle_t xTaskGetCurrentTaskHandle(void); void xTaskNotifyGive(TaskHandle_t task); BaseType_t xTaskNotifyWait(uint32_t bits_entry_clear, uint32_t bits_exit_clear, uint32_t *value, TickType_t wait_time); +static inline void xTaskGetStaticBuffers(void *pvTaskBuffer, void *pvStackBuffer, void *pvTaskTCB) +{ +} + #endif //_ESP32_COMPAT_H_ diff --git a/components/mdns/tests/test_afl_fuzz_host/test.c b/components/mdns/tests/test_afl_fuzz_host/test.c index afbf023843..d753dc9609 100644 --- a/components/mdns/tests/test_afl_fuzz_host/test.c +++ b/components/mdns/tests/test_afl_fuzz_host/test.c @@ -78,30 +78,20 @@ static int mdns_test_service_txt_set(const char *service, const char *proto, ui static int mdns_test_sub_service_add(const char *sub_name, const char *service_name, const char *proto, uint32_t port) { if (mdns_service_add(NULL, service_name, proto, port, NULL, 0)) { - // This is expected failure as the service thread is not running + return ESP_FAIL; } - mdns_action_t *a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); if (mdns_test_mdns_get_service_item(service_name, proto) == NULL) { return ESP_FAIL; } - int ret = mdns_service_subtype_add_for_host(NULL, service_name, proto, NULL, sub_name); - a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); - return ret; + return mdns_service_subtype_add_for_host(NULL, service_name, proto, NULL, sub_name); } static int mdns_test_service_add(const char *service_name, const char *proto, uint32_t port) { if (mdns_service_add(NULL, service_name, proto, port, NULL, 0)) { - // This is expected failure as the service thread is not running + return ESP_FAIL; } - mdns_action_t *a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); if (mdns_test_mdns_get_service_item(service_name, proto) == NULL) { return ESP_FAIL; @@ -266,9 +256,6 @@ int main(int argc, char **argv) } #ifndef MDNS_NO_SERVICES mdns_service_remove_all(); - mdns_action_t *a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); #endif ForceTaskDelete(); mdns_free(); From 7fdc3db504c77d6c9f72b20099f94a11b77bfece Mon Sep 17 00:00:00 2001 From: David Cermak Date: Thu, 2 Oct 2025 15:27:07 +0200 Subject: [PATCH 2/3] fix(mdns): Add test programs --- .../mdns/examples/simple/CMakeLists.txt | 6 + .../mdns/examples/simple/main/CMakeLists.txt | 2 + .../examples/simple/main/Kconfig.projbuild | 40 +++++ .../examples/simple/main/idf_component.yml | 8 + components/mdns/examples/simple/main/simple.c | 167 ++++++++++++++++++ .../simple/main/softap_example_main.c | 117 ++++++++++++ .../mdns/examples/simple/sdkconfig.defaults | 6 + .../tests/test_afl_fuzz_host/send_mdns.py | 83 +++++++++ 8 files changed, 429 insertions(+) create mode 100644 components/mdns/examples/simple/CMakeLists.txt create mode 100644 components/mdns/examples/simple/main/CMakeLists.txt create mode 100644 components/mdns/examples/simple/main/Kconfig.projbuild create mode 100644 components/mdns/examples/simple/main/idf_component.yml create mode 100644 components/mdns/examples/simple/main/simple.c create mode 100644 components/mdns/examples/simple/main/softap_example_main.c create mode 100644 components/mdns/examples/simple/sdkconfig.defaults create mode 100644 components/mdns/tests/test_afl_fuzz_host/send_mdns.py diff --git a/components/mdns/examples/simple/CMakeLists.txt b/components/mdns/examples/simple/CMakeLists.txt new file mode 100644 index 0000000000..17930cbb7c --- /dev/null +++ b/components/mdns/examples/simple/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(simple) diff --git a/components/mdns/examples/simple/main/CMakeLists.txt b/components/mdns/examples/simple/main/CMakeLists.txt new file mode 100644 index 0000000000..bd4c4204ec --- /dev/null +++ b/components/mdns/examples/simple/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "simple.c" "softap_example_main.c" + INCLUDE_DIRS ".") diff --git a/components/mdns/examples/simple/main/Kconfig.projbuild b/components/mdns/examples/simple/main/Kconfig.projbuild new file mode 100644 index 0000000000..5dcaa97c15 --- /dev/null +++ b/components/mdns/examples/simple/main/Kconfig.projbuild @@ -0,0 +1,40 @@ +menu "Example Configuration" + + config ESP_WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + + config ESP_WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the example to use. + config ESP_WIFI_CHANNEL + int "WiFi Channel" + range 1 13 + default 1 + help + WiFi channel (network channel) for the example to use. + + config ESP_MAX_STA_CONN + int "Maximal STA connections" + default 4 + help + Max number of the STA connects to AP. + + config ESP_GTK_REKEYING_ENABLE + bool "Enable GTK Rekeying" + default y + help + Flag to enable GTK rekeying. + + config ESP_GTK_REKEY_INTERVAL + int "GTK rekey interval" + depends on ESP_GTK_REKEYING_ENABLE + range 60 65535 + default 600 + help + GTK rekeying interval in seconds. +endmenu diff --git a/components/mdns/examples/simple/main/idf_component.yml b/components/mdns/examples/simple/main/idf_component.yml new file mode 100644 index 0000000000..e9277dfc99 --- /dev/null +++ b/components/mdns/examples/simple/main/idf_component.yml @@ -0,0 +1,8 @@ +dependencies: + ## Required IDF version + idf: ">=5.0" + espressif/mdns: + version: "^1.0.0" + override_path: "../../../" + protocol_examples_common: + path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/components/mdns/examples/simple/main/simple.c b/components/mdns/examples/simple/main/simple.c new file mode 100644 index 0000000000..cc4f4ef9b9 --- /dev/null +++ b/components/mdns/examples/simple/main/simple.c @@ -0,0 +1,167 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "esp_event.h" +#include "esp_log.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include "esp_netif_ip_addr.h" +#include "mdns.h" +#include "nvs_flash.h" +#include "protocol_examples_common.h" + +static const char *TAG = "mdns-simple"; + +static void initialise_mdns(void) +{ + const char *mdns_hostname = "minifritz"; + const char *mdns_instance = "Hristo's Time Capsule"; + + mdns_txt_item_t arduTxtData[4] = { + {"board", "esp32"}, + {"tcp_check", "no"}, + {"ssh_upload", "no"}, + {"auth_upload", "no"} + }; + + uint8_t mac[6]; + ESP_ERROR_CHECK(esp_read_mac(mac, ESP_MAC_WIFI_STA)); + + char *winstance = NULL; + if (asprintf(&winstance, "%s [%02x:%02x:%02x:%02x:%02x:%02x]", + mdns_hostname, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) < 0) { + abort(); + } + + ESP_ERROR_CHECK(mdns_init()); + ESP_ERROR_CHECK(mdns_hostname_set(mdns_hostname)); + ESP_LOGI(TAG, "mdns hostname set to: [%s]", mdns_hostname); + ESP_ERROR_CHECK(mdns_instance_name_set(mdns_instance)); + + // Delegate host: 17.17.17.17 + mdns_ip_addr_t addr4 = { 0 }; + addr4.addr.type = ESP_IPADDR_TYPE_V4; + esp_netif_str_to_ip4("17.17.17.17", &addr4.addr.u_addr.ip4); + addr4.next = NULL; + ESP_ERROR_CHECK(mdns_delegate_hostname_add(mdns_hostname, &addr4)); + ESP_ERROR_CHECK(mdns_delegate_hostname_add("megafritz", &addr4)); + + // Services and subtypes mirroring the fuzz test setup + ESP_ERROR_CHECK(mdns_service_add(NULL, "_fritz", "_tcp", 22, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_subtype_add_for_host(NULL, "_fritz", "_tcp", NULL, "_server")); + + ESP_ERROR_CHECK(mdns_service_add(NULL, "_telnet", "_tcp", 22, NULL, 0)); + + ESP_ERROR_CHECK(mdns_service_add(NULL, "_workstation", "_tcp", 9, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_instance_name_set("_workstation", "_tcp", winstance)); + + ESP_ERROR_CHECK(mdns_service_add(NULL, "_arduino", "_tcp", 3232, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_txt_set("_arduino", "_tcp", arduTxtData, 4)); + + ESP_ERROR_CHECK(mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_instance_name_set("_http", "_tcp", "ESP WebServer")); + + ESP_ERROR_CHECK(mdns_service_add(NULL, "_afpovertcp", "_tcp", 548, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_rfb", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_smb", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_adisk", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_airport", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_printer", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_airplay", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_raop", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_uscan", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_uscans", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_ippusb", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_scanner", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_ipp", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_ipps", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_pdl-datastream", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_ptp", "_tcp", 885, NULL, 0)); + ESP_ERROR_CHECK(mdns_service_add(NULL, "_sleep-proxy", "_udp", 885, NULL, 0)); + + free(winstance); +} + +void init_softap(void); + +/* these strings match mdns_ip_protocol_t enumeration */ +static const char *ip_protocol_str[] = {"V4", "V6", "MAX"}; + +static void mdns_print_results(mdns_result_t *results) +{ + mdns_result_t *r = results; + mdns_ip_addr_t *a = NULL; + int i = 1, t; + while (r) { + if (r->esp_netif) { + printf("%d: Interface: %s, Type: %s, TTL: %" PRIu32 "\n", i++, esp_netif_get_ifkey(r->esp_netif), + ip_protocol_str[r->ip_protocol], r->ttl); + } + if (r->instance_name) { + printf(" PTR : %s.%s.%s\n", r->instance_name, r->service_type, r->proto); + } + if (r->hostname) { + printf(" SRV : %s.local:%u\n", r->hostname, r->port); + } + if (r->txt_count) { + printf(" TXT : [%zu] ", r->txt_count); + for (t = 0; t < r->txt_count; t++) { + printf("%s=%s(%d); ", r->txt[t].key, r->txt[t].value ? r->txt[t].value : "NULL", r->txt_value_len[t]); + } + printf("\n"); + } + a = r->addr; + while (a) { + if (a->addr.type == ESP_IPADDR_TYPE_V6) { + printf(" AAAA: " IPV6STR "\n", IPV62STR(a->addr.u_addr.ip6)); + } else { + printf(" A : " IPSTR "\n", IP2STR(&(a->addr.u_addr.ip4))); + } + a = a->next; + } + r = r->next; + } +} + + +static void query_mdns_service(const char *service_name, const char *proto) +{ + ESP_LOGI(TAG, "Query PTR: %s.%s.local", service_name, proto); + + mdns_result_t *results = NULL; + esp_err_t err = mdns_query_ptr(service_name, proto, 3000, 20, &results); + if (err) { + ESP_LOGE(TAG, "Query Failed: %s", esp_err_to_name(err)); + return; + } + if (!results) { + ESP_LOGW(TAG, "No results found!"); + return; + } + + mdns_print_results(results); + mdns_query_results_free(results); +} + + +void app_main(void) +{ + // ESP_ERROR_CHECK(nvs_flash_init()); + // ESP_ERROR_CHECK(esp_netif_init()); + // ESP_ERROR_CHECK(esp_event_loop_create_default()); + + ESP_LOGI(TAG, "mDNS Ver: %s", ESP_MDNS_VERSION_NUMBER); + + + // Use the common connection helper to bring up Wi‑Fi/Ethernet + init_softap(); + initialise_mdns(); + while (1) { + query_mdns_service("_fritz", "_tcp"); + + } + // ESP_ERROR_CHECK(example_connect()); +} diff --git a/components/mdns/examples/simple/main/softap_example_main.c b/components/mdns/examples/simple/main/softap_example_main.c new file mode 100644 index 0000000000..50c4f3a6df --- /dev/null +++ b/components/mdns/examples/simple/main/softap_example_main.c @@ -0,0 +1,117 @@ +/* WiFi softAP Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_mac.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_log.h" +#include "nvs_flash.h" + +#include "lwip/err.h" +#include "lwip/sys.h" + +/* The examples use WiFi configuration that you can set via project configuration menu. + + If you'd rather not, just change the below entries to strings with + the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid" +*/ +#define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID +#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD +#define EXAMPLE_ESP_WIFI_CHANNEL CONFIG_ESP_WIFI_CHANNEL +#define EXAMPLE_MAX_STA_CONN CONFIG_ESP_MAX_STA_CONN + +#if CONFIG_ESP_GTK_REKEYING_ENABLE +#define EXAMPLE_GTK_REKEY_INTERVAL CONFIG_ESP_GTK_REKEY_INTERVAL +#else +#define EXAMPLE_GTK_REKEY_INTERVAL 0 +#endif + +static const char *TAG = "wifi softAP"; + +static void wifi_event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + if (event_id == WIFI_EVENT_AP_STACONNECTED) { + wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data; + ESP_LOGI(TAG, "station "MACSTR" join, AID=%d", + MAC2STR(event->mac), event->aid); + } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) { + wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data; + ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d, reason=%d", + MAC2STR(event->mac), event->aid, event->reason); + } +} + +void wifi_init_softap(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_ap(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, + ESP_EVENT_ANY_ID, + &wifi_event_handler, + NULL, + NULL)); + + wifi_config_t wifi_config = { + .ap = { + .ssid = EXAMPLE_ESP_WIFI_SSID, + .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID), + .channel = EXAMPLE_ESP_WIFI_CHANNEL, + .password = EXAMPLE_ESP_WIFI_PASS, + .max_connection = EXAMPLE_MAX_STA_CONN, +#ifdef CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT + .authmode = WIFI_AUTH_WPA3_PSK, + .sae_pwe_h2e = WPA3_SAE_PWE_BOTH, +#else /* CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT */ + .authmode = WIFI_AUTH_WPA2_PSK, +#endif + .pmf_cfg = { + .required = true, + }, +#ifdef CONFIG_ESP_WIFI_BSS_MAX_IDLE_SUPPORT + .bss_max_idle_cfg = { + .period = WIFI_AP_DEFAULT_MAX_IDLE_PERIOD, + .protected_keep_alive = 1, + }, +#endif + .gtk_rekey_interval = EXAMPLE_GTK_REKEY_INTERVAL, + }, + }; + if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) { + wifi_config.ap.authmode = WIFI_AUTH_OPEN; + } + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + + ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s channel:%d", + EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS, EXAMPLE_ESP_WIFI_CHANNEL); +} + +void init_softap(void) +{ + //Initialize NVS + 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_LOGI(TAG, "ESP_WIFI_MODE_AP"); + wifi_init_softap(); +} diff --git a/components/mdns/examples/simple/sdkconfig.defaults b/components/mdns/examples/simple/sdkconfig.defaults new file mode 100644 index 0000000000..7d0fdf4880 --- /dev/null +++ b/components/mdns/examples/simple/sdkconfig.defaults @@ -0,0 +1,6 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) 6.0.0 Project Minimal Configuration +# +CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT=y +CONFIG_MDNS_MAX_SERVICES=40 +CONFIG_MDNS_ENABLE_DEBUG_PRINTS=y diff --git a/components/mdns/tests/test_afl_fuzz_host/send_mdns.py b/components/mdns/tests/test_afl_fuzz_host/send_mdns.py new file mode 100644 index 0000000000..815f8733a9 --- /dev/null +++ b/components/mdns/tests/test_afl_fuzz_host/send_mdns.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +""" +Send a raw mDNS UDP payload (from AFL crash.bin) to the mDNS multicast group. + +Usage: + python tests/test_afl_fuzz_host/send_mdns.py crash.bin \ + [--group 224.0.0.251] [--port 5353] \ + [--src 192.168.1.10] [--bind-iface eth0] \ + [--count 1] [--interval 0.2] + +Notes: +- The input file should contain the UDP payload (DNS message) only, as produced by the fuzzer. +- On Linux, specifying --bind-iface helps ensure the packet uses a specific interface. +""" + +import argparse +import os +import socket +import sys +import time + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Send raw mDNS UDP payload to 224.0.0.251:5353") + p.add_argument("bin", help="Path to UDP payload (e.g., crash.bin)") + p.add_argument("--group", default="224.0.0.251", help="Multicast group (IPv4)") + p.add_argument("--port", type=int, default=5353, help="Destination UDP port") + p.add_argument("--src", help="Source IPv4 address to bind and use for multicast (sets IP_MULTICAST_IF)") + p.add_argument("--bind-iface", help="Interface name to bind (Linux SO_BINDTODEVICE)") + p.add_argument("--count", type=int, default=1, help="Number of times to send the payload") + p.add_argument("--interval", type=float, default=0.2, help="Seconds between sends") + return p.parse_args() + + +def main() -> int: + args = parse_args() + + if not os.path.isfile(args.bin): + print(f"File not found: {args.bin}", file=sys.stderr) + return 2 + + payload = open(args.bin, "rb").read() + if not payload: + print("Empty payload", file=sys.stderr) + return 3 + + dst = (args.group, args.port) + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + # Reuse addr and set TTL/hop limit to 255 as commonly used by mDNS + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, 255) + s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255) + # Avoid receiving our own multicast + s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 0) + + if args.src: + # Bind to source and set multicast interface + s.bind((args.src, 0)) + s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(args.src)) + + if args.bind_iface and hasattr(socket, 'SO_BINDTODEVICE'): + try: + s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, args.bind_iface.encode()) + except OSError as e: + print(f"Warning: SO_BINDTODEVICE failed: {e}", file=sys.stderr) + + for i in range(args.count): + s.sendto(payload, dst) + if i + 1 < args.count: + time.sleep(args.interval) + + print(f"Sent {args.count} packet(s) to {dst[0]}:{dst[1]} ({len(payload)} bytes)") + return 0 + finally: + s.close() + + +if __name__ == "__main__": + raise SystemExit(main()) From 2e433aa6ec859ec34189e21ce551b6cb302dce41 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 6 Oct 2025 13:03:03 +0200 Subject: [PATCH 3/3] fix(mdns): Add tests for delegated answers --- components/mdns/tests/host_test/dnsfixture.py | 52 ++++++++++++++++++- .../mdns/tests/host_test/pytest_mdns.py | 13 ++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/components/mdns/tests/host_test/dnsfixture.py b/components/mdns/tests/host_test/dnsfixture.py index 6dcf0c99f5..c16b061fac 100644 --- a/components/mdns/tests/host_test/dnsfixture.py +++ b/components/mdns/tests/host_test/dnsfixture.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Unlicense OR CC0-1.0 import logging import re @@ -92,10 +92,58 @@ def check_record(self, name, query_type, expected=True, expect=None): if expect is None: expect = name if expected: - assert any(expect in answer for answer in answers), f"Expected record '{expect}' not found in answer section" + assert any(expect in answer for answer in answers), f"Expected record '{expect}' not in answer section" else: assert not any(expect in answer for answer in answers), f"Unexpected record '{expect}' found in answer section" + def parse_section(self, response, section: str, rdtype_text: str): + """Parse a specific response section (answer, authority, additional) for given rdtype. + + Returns list of textual records for that rdtype. + """ + out = [] + if not response: + return out + rrsets = [] + if section == 'answer': + rrsets = response.answer + elif section == 'authority': + rrsets = response.authority + elif section == 'additional': + rrsets = response.additional + else: + raise ValueError('invalid section') + for rr in rrsets: + if dns.rdatatype.to_text(rr.rdtype) != rdtype_text: + continue + for item in rr.items: + full = ( + f'{rr.name} {rr.ttl} ' + f'{dns.rdataclass.to_text(rr.rdclass)} ' + f'{dns.rdatatype.to_text(rr.rdtype)} ' + f'{item.to_text()}' + ) + out.append(full) + return out + + def check_additional(self, response, rdtype_text: str, owner_contains: str, expected: bool = True, expect_substr: str | None = None): + """Check Additional section for an RR of type rdtype_text whose owner includes owner_contains. + + If expect_substr is provided, also require it to appear in the textual RR. + """ + records = self.parse_section(response, 'additional', rdtype_text) + logger.info(f'additional({rdtype_text}): {records}') + + def _matches(line: str) -> bool: + in_owner = owner_contains in line + has_val = (expect_substr in line) if expect_substr else True + return in_owner and has_val + found = any(_matches(r) for r in records) + if expected: + assert found, f"Expected {rdtype_text} for {owner_contains} in Additional not found" + else: + assert not found, f"Unexpected {rdtype_text} for {owner_contains} found in Additional" + if __name__ == '__main__': if len(sys.argv) < 3: diff --git a/components/mdns/tests/host_test/pytest_mdns.py b/components/mdns/tests/host_test/pytest_mdns.py index f8b95f5514..95fefc2eba 100644 --- a/components/mdns/tests/host_test/pytest_mdns.py +++ b/components/mdns/tests/host_test/pytest_mdns.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Unlicense OR CC0-1.0 import logging @@ -65,6 +65,17 @@ def test_add_service(mdns_console, dig_app): dig_app.check_record('_http._tcp.local', query_type='PTR', expected=True) +def test_ptr_additional_records_for_service(dig_app): + # Query PTR for the service type and ensure SRV/TXT are in Additional (RFC 6763 §12.1) + resp = dig_app.run_query('_http._tcp.local', query_type='PTR') + # Answer section should have at least one PTR to the instance + answers = dig_app.parse_answer_section(resp, 'PTR') + assert any('test_service._http._tcp.local' in a for a in answers) + # Additional section should include SRV and TXT for the same instance + dig_app.check_additional(resp, 'SRV', 'test_service._http._tcp.local', expected=True) + dig_app.check_additional(resp, 'TXT', 'test_service._http._tcp.local', expected=True) + + def test_remove_service(mdns_console, dig_app): mdns_console.send_input('mdns_service_remove _http _tcp') mdns_console.send_input('mdns_service_lookup _http _tcp')