diff --git a/.github/workflows/mdns__build-target-test.yml b/.github/workflows/mdns__build-target-test.yml deleted file mode 100644 index d222307c79..0000000000 --- a/.github/workflows/mdns__build-target-test.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: "mdns: build/target-tests" - -on: - push: - branches: - - master - pull_request: - types: [opened, synchronize, reopened, labeled] - -jobs: - build_mdns: - if: contains(github.event.pull_request.labels.*.name, 'mdns') || github.event_name == 'push' - name: Build - strategy: - matrix: - idf_ver: ["latest", "release-v5.0", "release-v5.2", "release-v5.3"] - test: [ { app: example, path: "examples/query_advertise" }, { app: unit_test, path: "tests/unit_test" }, { app: test_app, path: "tests/test_apps" } ] - runs-on: ubuntu-22.04 - container: espressif/idf:${{ matrix.idf_ver }} - steps: - - name: Checkout esp-protocols - uses: actions/checkout@v4 - - name: Build ${{ matrix.test.app }} with IDF-${{ matrix.idf_ver }} - shell: bash - run: | - . ${IDF_PATH}/export.sh - python -m pip install idf-build-apps - # Build default configs for all targets - python ./ci/build_apps.py components/mdns/${{ matrix.test.path }} -r default -d - # Build specific configs for test targets - python ./ci/build_apps.py components/mdns/${{ matrix.test.path }} - cd components/mdns/${{ matrix.test.path }} - for dir in `ls -d build_esp32_*`; do - $GITHUB_WORKSPACE/ci/clean_build_artifacts.sh `pwd`/$dir - zip -qur artifacts.zip $dir - done - - uses: actions/upload-artifact@v4 - with: - name: mdns_bin_esp32_${{ matrix.idf_ver }}_${{ matrix.test.app }} - path: components/mdns/${{ matrix.test.path }}/artifacts.zip - if-no-files-found: error - - target_tests_mdns: - # Skip running on forks since it won't have access to secrets - if: | - github.repository == 'espressif/esp-protocols' && - ( contains(github.event.pull_request.labels.*.name, 'mdns') || github.event_name == 'push' ) - name: Target Example and Unit tests - strategy: - matrix: - idf_ver: ["latest"] - idf_target: ["esp32"] - test: [ { app: example, path: "examples/query_advertise" }, { app: unit_test, path: "tests/unit_test" }, { app: test_app, path: "tests/test_apps" } ] - needs: build_mdns - runs-on: - - self-hosted - - ESP32-ETHERNET-KIT - steps: - - name: Clear repository - run: sudo rm -fr $GITHUB_WORKSPACE && mkdir $GITHUB_WORKSPACE - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: mdns_bin_${{ matrix.idf_target }}_${{ matrix.idf_ver }}_${{ matrix.test.app }} - path: components/mdns/${{ matrix.test.path }}/ci/ - - name: Install Python packages - env: - PIP_EXTRA_INDEX_URL: "https://www.piwheels.org/simple" - run: | - sudo apt-get install -y dnsutils - - name: Run ${{ matrix.test.app }} application on ${{ matrix.idf_target }} - working-directory: components/mdns/${{ matrix.test.path }} - run: | - unzip ci/artifacts.zip -d ci - for dir in `ls -d ci/build_*`; do - rm -rf build sdkconfig.defaults - mv $dir build - python -m pytest --log-cli-level DEBUG --junit-xml=./results_${{ matrix.test.app }}_${{ matrix.idf_target }}_${{ matrix.idf_ver }}_${dir#"ci/build_"}.xml --target=${{ matrix.idf_target }} - done - - uses: actions/upload-artifact@v4 - if: always() - with: - name: results_${{ matrix.test.app }}_${{ matrix.idf_target }}_${{ matrix.idf_ver }}.xml - path: components/mdns/${{ matrix.test.path }}/*.xml diff --git a/.github/workflows/mdns__host-tests.yml b/.github/workflows/mdns__host-tests.yml deleted file mode 100644 index 5ef6c8adee..0000000000 --- a/.github/workflows/mdns__host-tests.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: "mdns: host-tests" - -on: - push: - branches: - - master - pull_request: - types: [opened, synchronize, reopened, labeled] - -jobs: - host_test_mdns: - if: contains(github.event.pull_request.labels.*.name, 'mdns') || github.event_name == 'push' - name: Host test build - runs-on: ubuntu-22.04 - container: espressif/idf:release-v5.3 - - steps: - - name: Checkout esp-protocols - uses: actions/checkout@v4 - with: - path: protocols - - - name: Build and Test - shell: bash - run: | - . ${IDF_PATH}/export.sh - python -m pip install idf-build-apps dnspython pytest pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf - cd $GITHUB_WORKSPACE/protocols - # Build host tests app (with all configs and targets supported) - python ./ci/build_apps.py components/mdns/tests/host_test/ - cd components/mdns/tests/host_test - # First run the linux_app and send a quick A query and a reverse query - ./build_linux_app/mdns_host.elf & - python dnsfixture.py A myesp.local --ip_only | xargs python dnsfixture.py X - # Next we run the pytest (using the console app) - pytest - - build_afl_host_test_mdns: - if: contains(github.event.pull_request.labels.*.name, 'mdns') || github.event_name == 'push' - name: Build AFL host test - strategy: - matrix: - idf_ver: ["latest"] - idf_target: ["esp32"] - - runs-on: ubuntu-22.04 - container: espressif/idf:${{ matrix.idf_ver }} - steps: - - name: Checkout esp-protocols - uses: actions/checkout@v4 - with: - path: esp-protocols - - name: Install Necessary Libs - run: | - apt-get update -y - apt-get install -y libbsd-dev - - name: Build ${{ matrix.example }} with IDF-${{ matrix.idf_ver }} for ${{ matrix.idf_target }} - env: - IDF_TARGET: ${{ matrix.idf_target }} - shell: bash - run: | - . ${IDF_PATH}/export.sh - cd $GITHUB_WORKSPACE/esp-protocols/components/mdns/tests/test_afl_fuzz_host/ - make INSTR=off diff --git a/.github/workflows/mdns__rust.yml b/.github/workflows/mdns__rust.yml new file mode 100644 index 0000000000..3124c5c96a --- /dev/null +++ b/.github/workflows/mdns__rust.yml @@ -0,0 +1,37 @@ +name: "mdns: rust-tests" + +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened, labeled] + +jobs: + host_test_mdns: + if: contains(github.event.pull_request.labels.*.name, 'mdns') || github.event_name == 'push' + name: Host test build + runs-on: ubuntu-22.04 + container: espressif/idf:latest + + steps: + - name: Checkout esp-protocols + uses: actions/checkout@v4 + + - name: Build and Test + shell: bash + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + # Add Rust to the current PATH + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + . "$HOME/.cargo/env" + rustc --version + cargo --version + . ${IDF_PATH}/export.sh + cd components/mdns/examples/simple_query/ + idf.py build + cd ../.. + # FFI build + COMPILE_COMMANDS_DIR=examples/simple_query/build/ cargo run --example usage + # Default build + cargo run --example usage diff --git a/components/mdns/CMakeLists.txt b/components/mdns/CMakeLists.txt index e0b2a4d9e0..60a13fd709 100644 --- a/components/mdns/CMakeLists.txt +++ b/components/mdns/CMakeLists.txt @@ -14,7 +14,8 @@ idf_build_get_property(target IDF_TARGET) if(${target} STREQUAL "linux") set(dependencies esp_netif_linux esp_event) set(private_dependencies esp_timer console esp_system) - set(srcs "mdns.c" ${MDNS_NETWORKING} ${MDNS_CONSOLE}) + set(srcs "mdns_stub.c" ${MDNS_NETWORKING} ${MDNS_CONSOLE}) + # set(srcs "mdns.c" ${MDNS_NETWORKING} ${MDNS_CONSOLE}) else() set(dependencies lwip console esp_netif) set(private_dependencies esp_timer esp_wifi) diff --git a/components/mdns/Cargo.toml b/components/mdns/Cargo.toml new file mode 100644 index 0000000000..420704dca5 --- /dev/null +++ b/components/mdns/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "mdns" +version = "0.1.0" +edition = "2021" + +[dependencies] +libc = "0.2" +dns-parser = "0.8" +socket2 = "*" +nix = "0.26" +lazy_static = "*" + +[build-dependencies] +cc = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[features] +ffi = [] diff --git a/components/mdns/build.rs b/components/mdns/build.rs new file mode 100644 index 0000000000..32c6e2893e --- /dev/null +++ b/components/mdns/build.rs @@ -0,0 +1,117 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct CompileCommand { + directory: String, + command: String, + file: String, +} + +fn main() { + + println!("cargo:rerun-if-env-changed=COMPILE_COMMANDS_DIR"); + // Get the directory for compile_commands.json from an environment variable + let compile_commands_dir = match env::var("COMPILE_COMMANDS_DIR") { + Ok(dir) => dir, + Err(_) => { + // If the environment variable is not defined, return early + println!("COMPILE_COMMANDS_DIR not set, skipping custom build."); + // this is a native build + // println!("cargo:rustc-cfg=native"); + return; + } + }; + + // building with FFI of mdns_networking + println!("COMPILE_COMMANDS_DIR set, enabling FFI feature."); + println!("cargo:rustc-cfg=feature=\"ffi\""); + // Construct the path to the compile_commands.json file + let compile_commands_path = Path::new(&compile_commands_dir).join("compile_commands.json"); + + // Parse compile_commands.json + let compile_commands: Vec = { + let data = fs::read_to_string(&compile_commands_path) + .expect("Failed to read compile_commands.json"); + serde_json::from_str(&data) + .expect("Failed to parse compile_commands.json") + }; + + // Directory of compile_commands.json, used to resolve relative paths + let base_dir = compile_commands_path + .parent() + .expect("Failed to get base directory of compile_commands.json"); + + // List of C files to compile (only base names) + let files_to_compile = vec![ + "mdns_networking_socket.c", + "log_write.c", + "log_timestamp.c", + "esp_netif_linux.c", + "freertos_linux.c", + "tag_log_level.c", + "log_linked_list.c", + "log_lock.c", + "log_level.c", + "log_binary_heap.c", + "esp_system_linux2.c", + "heap_caps_linux.c", + "mdns_stub.c", + "log_buffers.c", + "util.c" + ]; + + // Initialize the build + let mut build = cc::Build::new(); + +for file in &files_to_compile { + // Extract the base name from `file` for comparison + let target_base_name = Path::new(file) + .file_name() + .expect("Failed to extract base name from target file") + .to_str() + .expect("Target file name is not valid UTF-8"); + + // Find the entry in compile_commands.json by matching the base name + let cmd = compile_commands.iter() + .find(|entry| { + let full_path = Path::new(&entry.directory).join(&entry.file); // Resolve relative paths + if let Some(base_name) = full_path.file_name().and_then(|name| name.to_str()) { +// println!("Checking file: {} against {}", base_name, target_base_name); // Debug information + base_name == target_base_name + } else { + false + } + }) + .unwrap_or_else(|| panic!("{} not found in compile_commands.json", target_base_name)); + + // Add the file to the build + build.file(&cmd.file); + + // Parse flags and include paths from the command + for part in cmd.command.split_whitespace() { + if part.starts_with("-I") { + // Handle include directories + let include_path = &part[2..]; + let full_include_path = if Path::new(include_path).is_relative() { + base_dir.join(include_path).canonicalize() + .expect("Failed to resolve relative include path") + } else { + PathBuf::from(include_path) + }; + build.include(full_include_path); + } else if part.starts_with("-D") || part.starts_with("-std") { + // Add other compilation flags + build.flag(part); + } + } +} + + + + + // Compile with the gathered information + build.compile("mdns"); +} diff --git a/components/mdns/examples/simple_query/CMakeLists.txt b/components/mdns/examples/simple_query/CMakeLists.txt new file mode 100644 index 0000000000..a244a7cabb --- /dev/null +++ b/components/mdns/examples/simple_query/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.5) + + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +if(${IDF_TARGET} STREQUAL "linux") + set(EXTRA_COMPONENT_DIRS "../../../../common_components/linux_compat" "../../tests/host_test/components/") + set(COMPONENTS main) +endif() + +project(mdns_host) + +# Enable sanitizers only without console (we'd see some leaks on argtable when console exits) +if(NOT CONFIG_TEST_CONSOLE AND CONFIG_IDF_TARGET_LINUX) +idf_component_get_property(mdns mdns COMPONENT_LIB) +target_link_options(${mdns} INTERFACE -fsanitize=address -fsanitize=undefined) +endif() diff --git a/components/mdns/examples/simple_query/main/CMakeLists.txt b/components/mdns/examples/simple_query/main/CMakeLists.txt new file mode 100644 index 0000000000..31eb27ed13 --- /dev/null +++ b/components/mdns/examples/simple_query/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "main.c" "esp_system_linux2.c" + INCLUDE_DIRS + "." + REQUIRES mdns console nvs_flash) diff --git a/components/mdns/examples/simple_query/main/Kconfig.projbuild b/components/mdns/examples/simple_query/main/Kconfig.projbuild new file mode 100644 index 0000000000..9bf4bba235 --- /dev/null +++ b/components/mdns/examples/simple_query/main/Kconfig.projbuild @@ -0,0 +1,21 @@ +menu "Test Configuration" + + config TEST_HOSTNAME + string "mDNS Hostname" + default "esp32-mdns" + help + mDNS Hostname for example to use + + config TEST_NETIF_NAME + string "Network interface name" + default "eth2" + help + Name/ID if the network interface on which we run the mDNS host test + + config TEST_CONSOLE + bool "Start console" + default n + help + Test uses esp_console for interactive testing. + +endmenu diff --git a/components/mdns/examples/simple_query/main/esp_system_linux2.c b/components/mdns/examples/simple_query/main/esp_system_linux2.c new file mode 100644 index 0000000000..25ee652382 --- /dev/null +++ b/components/mdns/examples/simple_query/main/esp_system_linux2.c @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * All functions presented here are stubs for the POSIX/Linux implementation of FReeRTOS. + * They are meant to allow to compile, but they DO NOT return any meaningful value. + */ + +#include +#include +#include "esp_private/system_internal.h" +#include "esp_heap_caps.h" + +// dummy, we should never get here on Linux +void esp_restart_noos_dig(void) +{ + abort(); +} + +uint32_t esp_get_free_heap_size(void) +{ + return heap_caps_get_free_size(MALLOC_CAP_DEFAULT); +} + +uint32_t esp_get_free_internal_heap_size(void) +{ + return heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); +} + +uint32_t esp_get_minimum_free_heap_size(void) +{ + return heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT); +} + +const char *esp_get_idf_version(void) +{ + return "IDF_VER"; +} + +void __attribute__((noreturn)) esp_system_abort(const char *details) +{ + exit(1); +} diff --git a/components/mdns/examples/simple_query/main/idf_component.yml b/components/mdns/examples/simple_query/main/idf_component.yml new file mode 100644 index 0000000000..e2d4fe9ba2 --- /dev/null +++ b/components/mdns/examples/simple_query/main/idf_component.yml @@ -0,0 +1,7 @@ +dependencies: + 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_query/main/main.c b/components/mdns/examples/simple_query/main/main.c new file mode 100644 index 0000000000..1678678eef --- /dev/null +++ b/components/mdns/examples/simple_query/main/main.c @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_console.h" +#include "mdns.h" + +static const char *TAG = "mdns-test"; + +static esp_netif_t *s_netif; + +static void mdns_test_app(esp_netif_t *interface); + +#ifndef CONFIG_IDF_TARGET_LINUX +#include "protocol_examples_common.h" +#include "esp_event.h" +#include "nvs_flash.h" + +/** + * @brief This is an entry point for the real target device, + * need to init few components and connect to a network interface + */ +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_ERROR_CHECK(example_connect()); + + mdns_test_app(EXAMPLE_INTERFACE); + + ESP_ERROR_CHECK(example_disconnect()); +} +#else + +/** + * @brief This is an entry point for the linux target (simulator on host) + * need to create a dummy WiFi station and use it as mdns network interface + */ +int main(int argc, char *argv[]) +{ + setvbuf(stdout, NULL, _IONBF, 0); + const esp_netif_inherent_config_t base_cg = { .if_key = "WIFI_STA_DEF", .if_desc = CONFIG_TEST_NETIF_NAME }; + esp_netif_config_t cfg = { .base = &base_cg }; + s_netif = esp_netif_new(&cfg); + + mdns_test_app(s_netif); + + esp_netif_destroy(s_netif); + return 0; +} +#endif + +typedef size_t mdns_if_t; + +typedef struct { + mdns_if_t tcpip_if; + mdns_ip_protocol_t ip_protocol; + struct pbuf *pb; + esp_ip_addr_t src; + esp_ip_addr_t dest; + uint16_t src_port; + uint8_t multicast; +} mdns_rx_packet_t; + +struct pbuf { + struct pbuf *next; + void *payload; + size_t tot_len; + size_t len; +}; + +esp_err_t _mdns_pcb_init(mdns_if_t tcpip_if, mdns_ip_protocol_t ip_protocol); +size_t _mdns_udp_pcb_write(mdns_if_t tcpip_if, mdns_ip_protocol_t ip_protocol, const esp_ip_addr_t *ip, uint16_t port, uint8_t *data, size_t len); +esp_err_t _mdns_pcb_deinit(mdns_if_t tcpip_if, mdns_ip_protocol_t ip_protocol); +void _mdns_packet_free(mdns_rx_packet_t *packet); + + +static void mdns_test_app(esp_netif_t *interface) +{ + uint8_t query_packet[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x64, 0x61, 0x76, 0x69, 0x64, 0x2d, 0x77, 0x6f, 0x72, 0x6b, 0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x00, 0x00, 0x01, 0x00, 0x01}; + esp_ip_addr_t ip = ESP_IP4ADDR_INIT(224, 0, 0, 251); + +// ESP_ERROR_CHECK(mdns_init()); +// ESP_ERROR_CHECK(mdns_register_netif(interface)); +// ESP_ERROR_CHECK(mdns_netif_action(interface, MDNS_EVENT_ENABLE_IP4)); + esp_err_t err = _mdns_pcb_init(0, MDNS_IP_PROTOCOL_V4); + ESP_LOGI(TAG, "err = %d", err); + size_t len = _mdns_udp_pcb_write(0, MDNS_IP_PROTOCOL_V4, &ip, 5353, query_packet, sizeof(query_packet)); + ESP_LOGI(TAG, "len = %d", (int)len); +// query_mdns_host("david-work"); + vTaskDelay(pdMS_TO_TICKS(1000)); +// mdns_free(); + _mdns_pcb_deinit(0, MDNS_IP_PROTOCOL_V4); + ESP_LOGI(TAG, "Exit"); +} diff --git a/components/mdns/examples/simple_query/sdkconfig.defaults b/components/mdns/examples/simple_query/sdkconfig.defaults new file mode 100644 index 0000000000..dc79fbcad8 --- /dev/null +++ b/components/mdns/examples/simple_query/sdkconfig.defaults @@ -0,0 +1,7 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_TEST_NETIF_NAME="eth0" +CONFIG_ESP_EVENT_POST_FROM_ISR=n +CONFIG_MDNS_NETWORKING_SOCKET=y +CONFIG_MDNS_SKIP_SUPPRESSING_OWN_QUERIES=y +CONFIG_MDNS_PREDEF_NETIF_STA=n +CONFIG_MDNS_PREDEF_NETIF_AP=n diff --git a/components/mdns/examples/usage.rs b/components/mdns/examples/usage.rs new file mode 100644 index 0000000000..366daed45c --- /dev/null +++ b/components/mdns/examples/usage.rs @@ -0,0 +1,17 @@ +// examples/basic_usage.rs + +// use std::process::Termination; +use mdns::*; +use std::thread; +use std::time::Duration; +// use libc::__c_anonymous_xsk_tx_metadata_union; + +fn main() { + // Initialize mDNS + mdns_init(); + + mdns_query("david-work.local"); + thread::sleep(Duration::from_millis(1500)); + // Deinitialize mDNS + mdns_deinit(); +} diff --git a/components/mdns/mdns.c b/components/mdns/mdns.c index 113a1e999b..c59648ee76 100644 --- a/components/mdns/mdns.c +++ b/components/mdns/mdns.c @@ -1544,6 +1544,11 @@ static void _mdns_dispatch_tx_packet(mdns_tx_packet_t *p) mdns_debug_packet(packet, index); #endif + ESP_LOG_BUFFER_HEXDUMP(TAG, packet, index, ESP_LOG_INFO); + for (int i = 0; i < index; ++i) { + printf("0x%02x, ", packet[i]); + } + printf("\n"); _mdns_udp_pcb_write(p->tcpip_if, p->ip_protocol, &p->dst, p->port, packet, index); } diff --git a/components/mdns/mdns_networking_socket.c b/components/mdns/mdns_networking_socket.c index a99a9cc2d9..d359a353cf 100644 --- a/components/mdns/mdns_networking_socket.c +++ b/components/mdns/mdns_networking_socket.c @@ -206,7 +206,7 @@ size_t _mdns_udp_pcb_write(mdns_if_t tcpip_if, mdns_ip_protocol_t ip_protocol, c ESP_LOGE(TAG, "espaddr_to_inet() failed: Mismatch of IP protocols"); return 0; } - ESP_LOGD(TAG, "[sock=%d]: Sending to IP %s port %d", sock, get_string_address(&in_addr), port); + ESP_LOGI(TAG, "[sock=%d]: Sending to IP %s port %d", sock, get_string_address(&in_addr), port); ssize_t actual_len = sendto(sock, data, len, 0, (struct sockaddr *)&in_addr, ss_size); if (actual_len < 0) { ESP_LOGE(TAG, "[sock=%d]: _mdns_udp_pcb_write sendto() has failed\n errno=%d: %s", sock, errno, strerror(errno)); diff --git a/components/mdns/mdns_stub.c b/components/mdns/mdns_stub.c new file mode 100644 index 0000000000..1bab517f75 --- /dev/null +++ b/components/mdns/mdns_stub.c @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "esp_log.h" +#include "esp_console.h" +#include "mdns.h" + + +static const char *TAG = "mdns-stub"; +typedef size_t mdns_if_t; + +typedef struct { + mdns_if_t tcpip_if; + mdns_ip_protocol_t ip_protocol; + struct pbuf *pb; + esp_ip_addr_t src; + esp_ip_addr_t dest; + uint16_t src_port; + uint8_t multicast; +} mdns_rx_packet_t; + +struct pbuf { + struct pbuf *next; + void *payload; + size_t tot_len; + size_t len; +}; + +esp_err_t _mdns_pcb_init(mdns_if_t tcpip_if, mdns_ip_protocol_t ip_protocol); +size_t _mdns_udp_pcb_write(mdns_if_t tcpip_if, mdns_ip_protocol_t ip_protocol, const esp_ip_addr_t *ip, uint16_t port, uint8_t *data, size_t len); +esp_err_t _mdns_pcb_deinit(mdns_if_t tcpip_if, mdns_ip_protocol_t ip_protocol); +void _mdns_packet_free(mdns_rx_packet_t *packet); + +typedef void (*callback_t)(const uint8_t *, size_t len); + +static callback_t rust_callback = NULL; + + + +void set_callback(callback_t callback) +{ + rust_callback = callback; +} + +void set_callback2() +{ + ESP_LOGI(TAG, "set_callback2!"); +} + + +esp_err_t _mdns_send_rx_action(mdns_rx_packet_t *packet) +{ + ESP_LOGI(TAG, "Received packet!"); + ESP_LOG_BUFFER_HEXDUMP(TAG, packet->pb->payload, packet->pb->tot_len, ESP_LOG_INFO); + if (rust_callback) { + rust_callback(packet->pb->payload, packet->pb->tot_len); + } + _mdns_packet_free(packet); + return ESP_OK; +} + +esp_netif_t *g_netif = NULL; + + +esp_netif_t *_mdns_get_esp_netif(mdns_if_t tcpip_if) +{ + if (g_netif == NULL) { + const esp_netif_inherent_config_t base_cg = { .if_key = "WIFI_STA_DEF", .if_desc = CONFIG_TEST_NETIF_NAME }; + esp_netif_config_t cfg = { .base = &base_cg }; + g_netif = esp_netif_new(&cfg); + } + return g_netif; +} diff --git a/components/mdns/src/lib.rs b/components/mdns/src/lib.rs new file mode 100644 index 0000000000..67c1cdc8f7 --- /dev/null +++ b/components/mdns/src/lib.rs @@ -0,0 +1,161 @@ +mod service; +mod querier; + +use service::{Service, NativeService, CService}; +use querier::Querier; + +use lazy_static::lazy_static; +use std::sync::{Arc, Mutex}; +use dns_parser::{Builder, QueryClass, QueryType, Packet}; +use std::time::Duration; + +lazy_static! { + static ref SERVER: Arc> = Arc::new(Mutex::new(Objects { + service: None, + querier: None, + })); +} + +struct Objects { + service: Option>, + querier: Option, +} + +fn build_info() { + #[cfg(not(feature = "ffi"))] + { + println!("Default build"); + } + #[cfg(feature = "ffi")] + { + println!("FFI build"); + } +} + +fn create_service(cb: fn(&[u8])) -> Box { + #[cfg(not(feature = "ffi"))] + { + NativeService::init(cb) + } + #[cfg(feature = "ffi")] + { + CService::init(cb) + } +} + +fn read_cb(vec: &[u8]) { + if vec.len() == 0 { + let mut service_guard = SERVER.lock().unwrap(); + if let Some(querier) = &mut service_guard.querier { + println!("querier process {:?}", vec); + let packet = querier.process(); + if packet.is_some() { + if let Some(service) = &service_guard.service { + service.send(packet.unwrap()); + } + } + } + } else { + println!("Received {:?}", vec); + let mut service_guard = SERVER.lock().unwrap(); + if let Some(querier) = &mut service_guard.querier { + querier.parse(&vec).expect("Failed to parse.."); + } + // parse_dns_response(vec).unwrap(); + } +} + +fn parse_dns_response(data: &[u8]) -> Result<(), String> { + println!("Parsing DNS response with length 2 : {}", data.len()); + let packet = Packet::parse(data).unwrap(); + for answer in packet.answers { + println!("ANSWER:"); + println!("{:?}", answer); + } + for question in packet.questions { + println!("QUESTION:"); + println!("{:?}", question); + } + Ok(()) +} + +pub fn mdns_init() { + build_info(); + let mut service_guard = SERVER.lock().unwrap(); + if service_guard.service.is_none() { + service_guard.service = Some(create_service(read_cb)); + } + if service_guard.querier.is_none() { + service_guard.querier = Some(Querier::new()); + } + println!("mdns_init called"); +} + +pub fn mdns_deinit() { + let mut service_guard = SERVER.lock().unwrap(); + if let Some(service) = service_guard.service.take() { + service.deinit(); + } + println!("mdns_deinit called"); +} + +/* +pub fn mdns_query(name: &str) { + let mut service_guard = SERVER.lock().unwrap(); + + if let Some(querier) = &mut service_guard.querier { + let timeout = Duration::from_secs(5); + let query_id = querier.add( + name.to_string(), + "".to_string(), + "_http._tcp".to_string(), + QueryType::A, + false, + timeout, + ); + querier.wait(query_id).await.unwrap(); + println!("Query added with ID: {}", query_id); + } + +} + + */ + +pub fn mdns_query(name: &str) { + // Lock the server to access the querier + let query_id; + let querier_cvar; + + { + let mut service_guard = SERVER.lock().unwrap(); + if let Some(querier) = &mut service_guard.querier { + let timeout = Duration::from_secs(1); + query_id = querier.add( + name.to_string(), + "".to_string(), + "_http._tcp".to_string(), + QueryType::A, + false, + timeout, + ); + querier_cvar = querier.completed_queries.clone(); // Clone the Arc pair + } else { + println!("No querier available"); + return; + } + } // Release the SERVER lock here + + // Wait for the query to complete + let (lock, cvar) = &*querier_cvar; + let mut completed = lock.lock().unwrap(); + while !completed.get(&query_id).copied().unwrap_or(false) { + let result = cvar.wait_timeout(completed, Duration::from_secs(5)).unwrap(); + completed = result.0; // Update the lock guard + if result.1.timed_out() { + println!("Query timed out: ID {}", query_id); + return; + } + } + + println!("Query completed!!! ID {}", query_id); +} diff --git a/components/mdns/src/querier.rs b/components/mdns/src/querier.rs new file mode 100644 index 0000000000..6ff3e769a0 --- /dev/null +++ b/components/mdns/src/querier.rs @@ -0,0 +1,148 @@ +use dns_parser::{Builder, QueryClass, QueryType, Packet}; +use std::sync::{Arc, Condvar, Mutex}; +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +#[derive(Debug)] +pub struct Query { + pub name: String, + pub service: String, + pub proto: String, + pub query_type: QueryType, + pub unicast: bool, + pub timeout: Duration, + pub added_at: Instant, + pub packet: Vec, + pub id: usize, +} + +pub struct Querier { + queries: Vec, + pub(crate) completed_queries: Arc<(Mutex>, Condvar)>, // Shared state for query completion +} + +impl Querier { + pub fn new() -> Self { + Self { + queries: Vec::new(), + completed_queries: Arc::new((Mutex::new(HashMap::new()), Condvar::new())), + } + } + + pub fn add( + &mut self, + name: String, + service: String, + proto: String, + query_type: QueryType, + unicast: bool, + timeout: Duration, + ) -> usize { + let id = self.queries.len(); + let query = Query { + name: name.clone(), + service, + proto, + query_type, + unicast, + timeout, + added_at: Instant::now(), + packet: create_a_query(&name), + id: id.clone() + }; + self.queries.push(query); + self.completed_queries.0.lock().unwrap().insert(id, false); // Mark as incomplete + id + } + pub fn parse(&mut self, data: &[u8]) -> Result<(), String> { + println!("Parsing DNS response with length 2 : {}", data.len()); + let packet = Packet::parse(data).unwrap(); + for answer in packet.answers { + println!("ANSWER:"); + println!("{:?}", answer); + let name = answer.name.to_string(); + let mut completed_queries = vec![]; + self.queries.retain(|query| { + if query.name == name { + println!("ANSWER: {:?}", answer.data); + completed_queries.push(query.id); // Track for completion + false + } + else { true } + }); + let (lock, cvar) = &*self.completed_queries; + let mut completed = lock.lock().unwrap(); + for query_id in completed_queries { + if let Some(entry) = completed.get_mut(&query_id) { + *entry = true; + } + } + cvar.notify_all(); + + } + for question in packet.questions { + println!("{:?}", question); + } + Ok(()) + + } + + pub fn process(&mut self) -> Option> { + let now = Instant::now(); + let mut packet_to_send: Option> = None; + + // Collect IDs of timed-out queries to mark them as complete + let mut timed_out_queries = vec![]; + self.queries.retain(|query| { + let elapsed = now.duration_since(query.added_at); + if elapsed > query.timeout { + timed_out_queries.push(query.id); // Track for completion + false // Remove the query + } else { + packet_to_send = Some(query.packet.clone()); + true // Keep the query + } + }); + + // Mark timed-out queries as complete and notify waiting threads + let (lock, cvar) = &*self.completed_queries; + let mut completed = lock.lock().unwrap(); + for query_id in timed_out_queries { + if let Some(entry) = completed.get_mut(&query_id) { + *entry = true; + } + } + cvar.notify_all(); + println!("Processing... query"); + + packet_to_send + } + pub fn wait(&self, id: usize) -> Result<(), &'static str> { + let (lock, cvar) = &*self.completed_queries; + + // Wait until the query is marked as complete or timeout expires + let mut completed = lock.lock().unwrap(); + while !completed.get(&id).copied().unwrap_or(false) { + completed = cvar.wait(completed).unwrap(); + } + Ok(()) + } + + fn mark_query_as_complete(&self, query: &Query) { + let (lock, cvar) = &*self.completed_queries; + let mut completed = lock.lock().unwrap(); + if let Some(entry) = completed.get_mut(&(self.queries.len() - 1)) { + *entry = true; + } + cvar.notify_all(); + } +} + +fn create_a_query(name: &str) -> Vec { + let query_type = QueryType::A; // Type A query for IPv4 address + let query_class = QueryClass::IN; // Class IN (Internet) + + let mut builder = Builder::new_query(0, true); + builder.add_question(name, false, query_type, query_class); + builder.build().unwrap_or_else(|x| x) +} diff --git a/components/mdns/src/service/ffi.rs b/components/mdns/src/service/ffi.rs new file mode 100644 index 0000000000..d9f1390fbc --- /dev/null +++ b/components/mdns/src/service/ffi.rs @@ -0,0 +1,190 @@ +use libc::AT_NULL; + +use super::service::Service; +use std::fmt; + +pub struct CService +{ + callback: fn(&[u8]) +} + +impl CService { + fn new(cb: fn(&[u8])) -> Self { + CService { + callback: cb + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct EspIp4Addr { + pub addr: u32, // IPv4 address as a 32-bit integer +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct EspIp6Addr { + pub addr: [u32; 4], // IPv6 address as an array of 4 32-bit integers + pub zone: u8, // Zone ID +} + +#[repr(C)] +pub union EspIpUnion { + pub ip4: EspIp4Addr, + pub ip6: EspIp6Addr, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct EspIpAddr { + pub u_addr: EspIpUnion, // Union containing IPv4 or IPv6 address + pub addr_type: u8, +} + +// Manual implementation of Debug for the union +impl fmt::Debug for EspIpUnion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Safely access and format the union's members for debugging + unsafe { + write!(f, "EspIpUnion {{ ip4: {:?}, ip6: {:?} }}", self.ip4, self.ip6) + } + } +} + +// Manual implementation of Clone for the union +impl Clone for EspIpUnion { + fn clone(&self) -> Self { + // Safety: Assuming the union contains valid data in either `ip4` or `ip6` + unsafe { + EspIpUnion { + ip4: self.ip4.clone(), + } + } + } +} + +// Manual implementation of Copy for the union +impl Copy for EspIpUnion {} + +// Address type definitions +pub const ESP_IPADDR_TYPE_V4: u8 = 0; +pub const ESP_IPADDR_TYPE_V6: u8 = 6; +pub const ESP_IPADDR_TYPE_ANY: u8 = 46; + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub enum MdnsIf { + Netif0 = 0, + // Add more as needed based on the actual C enum definition +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub enum MdnsIpProtocol { + Ip4 = 0, + Ip6 = 1, +} + +type EspErr = i32; + +extern "C" { + fn _mdns_pcb_init(tcpip_if: MdnsIf, ip_protocol: MdnsIpProtocol) -> EspErr; + fn _mdns_udp_pcb_write( + tcpip_if: MdnsIf, + ip_protocol: MdnsIpProtocol, + ip: *const EspIpAddr, + port: u16, + data: *const u8, + len: usize, + ) -> usize; + fn _mdns_pcb_deinit(tcpip_if: MdnsIf, ip_protocol: MdnsIpProtocol) -> EspErr; + fn set_callback(callback: extern "C" fn(*const u8, usize)); +} + +static mut CALLBACK: Option = None; + +extern "C" fn rust_callback(data: *const u8, len: usize) +{ + println!("Received len: {}", len); + unsafe { + // Ensure that the data pointer is valid + if !data.is_null() { + // Create a Vec from the raw pointer and length + let data_vec = std::slice::from_raw_parts(data, len).to_vec(); + if let Some(cb) = CALLBACK { + cb(&data_vec); + } + // Now call the safe parser function with the Vec + // parse_dns_response(&data_vec); + } + } +} + + +pub fn mdns_udp_pcb_write_rust( + tcpip_if: MdnsIf, + ip_protocol: MdnsIpProtocol, + ip: EspIpAddr, + port: u16, + data: &[u8], +) -> usize { + unsafe { + _mdns_udp_pcb_write( + tcpip_if, + ip_protocol, + &ip as *const EspIpAddr, + port, + data.as_ptr(), + data.len(), + ) + } +} + +impl Service for CService { + + + fn init(cb:fn(&[u8])) -> Box { + println!("FfiHousekeeper: Initializing."); + unsafe { + set_callback(rust_callback); + CALLBACK = Some(cb); + } + let _ = unsafe { _mdns_pcb_init(MdnsIf::Netif0, MdnsIpProtocol::Ip4) }; + + + Box::new(CService::new(cb)) + } + + fn send(&self, packet: Vec) { + let ip4 = EspIpAddr { + u_addr: EspIpUnion { + ip4: EspIp4Addr { + addr: u32::from_le_bytes([224, 0, 0, 251]), + }, + }, + addr_type: ESP_IPADDR_TYPE_V4, + }; + println!("FfiHousekeeper: Sending"); + let len = mdns_udp_pcb_write_rust(MdnsIf::Netif0, MdnsIpProtocol::Ip4, ip4, 5353, &packet); + println!("Bytes sent: {}", len); + } + + fn action1(&self) { + println!("FfiHousekeeper: Performing Action1."); + } + + fn action2(&self) { + println!("FfiHousekeeper: Performing Action2."); + } + + fn action3(&self) { + println!("FfiHousekeeper: Performing Action3."); + } + + fn deinit(self: Box) { + let _ = unsafe { _mdns_pcb_deinit(MdnsIf::Netif0, MdnsIpProtocol::Ip4) }; + println!("FfiHousekeeper: Deinitializing."); + } + +} diff --git a/components/mdns/src/service/mod.rs b/components/mdns/src/service/mod.rs new file mode 100644 index 0000000000..9d64845a64 --- /dev/null +++ b/components/mdns/src/service/mod.rs @@ -0,0 +1,9 @@ +mod service; // Import the trait definition +mod native; // Thread-based implementation +mod ffi; // FFI-based implementation + +pub use service::Service; // Expose the trait +pub use native::NativeService; +pub use ffi::CService; +// pub use thread::ThreadHousekeeper; // Expose the thread-based implementation +// pub use ffi::FfiHousekeeper; // Expose the FFI-based implementation diff --git a/components/mdns/src/service/native.rs b/components/mdns/src/service/native.rs new file mode 100644 index 0000000000..458e024911 --- /dev/null +++ b/components/mdns/src/service/native.rs @@ -0,0 +1,164 @@ +use super::service::Service; +use std::thread; + +use std::net::{UdpSocket, Ipv4Addr}; +use socket2::{Socket, Domain, Type, Protocol}; +use nix::unistd::{pipe, read, write, close}; +use nix::sys::select::{select, FdSet}; +use nix::sys::time::TimeVal; +use std::os::fd::AsRawFd; +use std::ptr::null; + +enum Action { + Action1, + Action2, + Action3, +} + +pub struct NativeService +{ + handle: Option>, + socket: UdpSocket, + write_fd: i32, + callback: fn(&[u8]), +} + +fn create_multicast_socket() -> UdpSocket { + let addr: std::net::SocketAddr = "0.0.0.0:5353".parse().unwrap(); + + let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap(); + socket.set_reuse_address(true).unwrap(); + socket.bind(&addr.into()).unwrap(); + + let multicast_addr = Ipv4Addr::new(224, 0, 0, 251); + let interface = Ipv4Addr::new(0, 0, 0, 0); + socket.join_multicast_v4(&multicast_addr, &interface).unwrap(); + + socket.into() +} + +impl NativeService +{ + fn new(cb: fn(&[u8])) -> Self { + let socket = create_multicast_socket(); + let (read_fd, w_fd) = pipe().expect("Failed to create pipe"); + let local_cb = cb; + let local_socket = socket.try_clone().unwrap(); + + let handle = thread::spawn(move || { + + loop { + let socket_fd = local_socket.as_raw_fd(); + let mut read_fds = FdSet::new(); + read_fds.insert(socket_fd); + read_fds.insert(read_fd); + + + let mut timeout = TimeVal::new(0, 500_000); + + match select(read_fd.max(socket_fd) + 1, Some(&mut read_fds), None, None, Some(&mut timeout)) { + Ok(0) => { + println!("ThreadHousekeeper: Performing housekeeping tasks"); + let buf = vec![]; + local_cb(&buf); + } + Ok(_) => { + if read_fds.contains(socket_fd) { + // let mut buf: [MaybeUninit; 1500] = unsafe { MaybeUninit::uninit().assume_init() }; + // let mut buf: [u8; 1500]; + let mut buf = vec![0u8; 1500]; // Create a buffer using a vector + // let sock = unsafe { Socket::from_raw_fd(socket_fd) }; + match local_socket.recv_from(&mut buf) { + Ok((size, addr)) => { + // Convert the buffer from MaybeUninit to a regular slice of u8 + // let buf = unsafe { + // std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, size) + // }; + // println!("Received {} bytes from {:?}: {:?}", size, addr, buf); + local_cb(&buf[..size]); + } + Err(e) => println!("Error reading from socket: {:?}", e), + } + } + + if read_fds.contains(read_fd) { + let mut buf = [0u8; 10]; + match read(read_fd, &mut buf) { + Ok(size) => { + println!("{}", size); + if size == 0 { + break; + } + } + Err(e) => println!("Error reading from socket: {:?}", e), + } + } + } + Err(e) => { + println!("Error in select(): {:?}", e); + break; + } + } + } + + // close(read_fd).expect("Failed to close read end of pipe"); + }); + + NativeService { + handle: Some(handle), + socket, + write_fd: w_fd, + callback: cb + } + } +} + +impl Service for NativeService { + + fn init(cb: fn(&[u8])) -> Box { + println!("Native: Initializing."); + Box::new(NativeService::new(cb)) + } + + fn send(&self, packet: Vec) { + let destination: std::net::SocketAddr = "224.0.0.251:5353".parse().unwrap(); + self.socket.send_to(&packet, &destination) + .expect("Failed to send mDNS query"); + } + + fn action1(&self) { + let buf: [u8; 1] = [0x01]; + // self.socket.send(&buf).unwrap(); + write(self.write_fd, &buf).unwrap(); + // if let Some(tx) = &self.tx { + // tx.send(Action::Action1).unwrap(); + // } + } + + fn action2(&self) { + // if let Some(tx) = &self.tx { + // tx.send(Action::Action2).unwrap(); + // } + } + + fn action3(&self) { + // if let Some(tx) = &self.tx { + // tx.send(Action::Action3).unwrap(); + // } + } + + fn deinit(self: Box) { + println!("DEINIT called"); + // if let Some(tx) = self.tx { + // drop(tx); + // } + close(self.write_fd).unwrap(); + + if let Some(handle) = self.handle { + handle.join().unwrap(); + println!("NativeService: Deinitialized"); + } + println!("DEINIT done..."); + + } +} diff --git a/components/mdns/src/service/service.rs b/components/mdns/src/service/service.rs new file mode 100644 index 0000000000..8653611a1c --- /dev/null +++ b/components/mdns/src/service/service.rs @@ -0,0 +1,12 @@ +pub trait Service: Send + Sync { + fn init(cb: fn(&[u8])) -> Box + where + Self: Sized; + + fn send(&self, packet: Vec); + fn action1(&self); + fn action2(&self); + fn action3(&self); + + fn deinit(self: Box); +}