diff --git a/examples/network/.build-test-rules.yml b/examples/network/.build-test-rules.yml index 7bbb24e350..2d2e70f24d 100644 --- a/examples/network/.build-test-rules.yml +++ b/examples/network/.build-test-rules.yml @@ -11,3 +11,6 @@ examples/network/simple_sniffer: - if: IDF_TARGET not in ["esp32", "esp32c3", "esp32s3"] temporary: true reason: lack of runners +examples/network/sta_to_eth: + disable: + - if: SOC_WIFI_SUPPORTED != 1 diff --git a/examples/network/sta_to_eth/CMakeLists.txt b/examples/network/sta_to_eth/CMakeLists.txt new file mode 100644 index 0000000000..11459c67f2 --- /dev/null +++ b/examples/network/sta_to_eth/CMakeLists.txt @@ -0,0 +1,10 @@ +# The following 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.16) + +# This example needs a DNS server: let's use the simple DNS server implementation from captive portal example +set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/protocols/http_server/captive_portal/components/dns_server + $ENV{IDF_PATH}/examples/ethernet/basic/components/ethernet_init) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(sta_to_eth) diff --git a/examples/network/sta_to_eth/README.md b/examples/network/sta_to_eth/README.md new file mode 100644 index 0000000000..9eb5ebc228 --- /dev/null +++ b/examples/network/sta_to_eth/README.md @@ -0,0 +1,92 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | + +# WiFi station to "Wired" interface L2 forwarder + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +This example aims to demonstrate 1-1 bridge using WiFi station and one of these interfaces (so called *wired* in this example) +- Ethernet (supported for all targets) +- USB acting as NCM device (supported for ESP32-S2 and ESP32-S3) + +It also allows for reconfiguring WiFi settings using a virtual network in the Ethernet. The reconfiguration mode is initialized if the WiFi settings are not available, connection fails or manually by long pressing the Boot button (GPIO0). +It is possible to configure WiFi settings (SSID and password) in a browser on an address `"wifi.settings"` or using unified provisioning. + +## How to use example + +This example could be used to *bring* wireless connectivity to devices that support only Ethernet (or USB Ethernet implemented as NCM device). +This example also supports runtime configuration of WiFi settings by means of a webpage or unified provisioning. + + +### Hardware Required + +Any board with either Ethernet of USB-OTG supported. + +### Configure the project + +Open the project configuration menu (`idf.py menuconfig`). + +In the `Example Configuration` menu choose the provisioning method: +* `EXAMPLE_WIFI_CONFIGURATION_MANUAL` for manual configuration using a webpage +* `EXAMPLE_WIFI_CONFIGURATION_PROVISIONING` for standard provisioning over the virtual USB network + +To provision the device using IDF provisioning tools (if `EXAMPLE_WIFI_CONFIGURATION_PROVISIONING` is selected) you can use idf provisioning utility with transport set to `softap`: +```bash +esp-idf/tools/esp_prov$ python esp_prov.py --transport httpd ... +``` +Please refer to the provisioning documentation and `esp_prov` script [documentation](../../../tools/esp_prov/README.md) for more details. + +### Build, Flash, and Run + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT build flash monitor +``` + +(Replace PORT with the name of the serial port to use.) + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +After the flashing you should see the output at idf monitor: + +(note that this is the output of USB configuration) +``` +I (1740) example_sta2wired: Wi-Fi STA connected +I (1740) example_sta2wired: WiFi station connected successfully +W (1750) TinyUSB: The device's configuration descriptor is not provided by user, using default. +W (1760) TinyUSB: The device's string descriptor is not provided by user, using default. +W (1770) TinyUSB: The device's device descriptor is not provided by user, using default. +I (1770) wifi:AP's beacon interval = 102400 us, DTIM period = 1 +I (1780) tusb_desc: +┌─────────────────────────────────┐ +│ USB Device Descriptor Summary │ +├───────────────────┬─────────────┤ +│bDeviceClass │ 239 │ +├───────────────────┼─────────────┤ +│bDeviceSubClass │ 2 │ +├───────────────────┼─────────────┤ +│bDeviceProtocol │ 1 │ +├───────────────────┼─────────────┤ +│bMaxPacketSize0 │ 64 │ +├───────────────────┼─────────────┤ +│idVendor │ 0x303a │ +├───────────────────┼─────────────┤ +│idProduct │ 0x4002 │ +├───────────────────┼─────────────┤ +│bcdDevice │ 0x100 │ +├───────────────────┼─────────────┤ +│iManufacturer │ 0x1 │ +├───────────────────┼─────────────┤ +│iProduct │ 0x2 │ +├───────────────────┼─────────────┤ +│iSerialNumber │ 0x3 │ +├───────────────────┼─────────────┤ +│bNumConfigurations │ 0x1 │ +└───────────────────┴─────────────┘ +I (915) TinyUSB: TinyUSB Driver installed +``` diff --git a/examples/network/sta_to_eth/main/CMakeLists.txt b/examples/network/sta_to_eth/main/CMakeLists.txt new file mode 100644 index 0000000000..58873dfd47 --- /dev/null +++ b/examples/network/sta_to_eth/main/CMakeLists.txt @@ -0,0 +1,16 @@ +if(CONFIG_EXAMPLE_WIFI_CONFIGURATION_MANUAL) + set(config_method manual_config.c) +else() + set(config_method provisioning.c scheme_generic_httpd.c) +endif() + +if(CONFIG_EXAMPLE_WIRED_INTERFACE_IS_ETHERNET) + set(wired_iface ethernet_iface.c) +else() + set(wired_iface usb_ncm_iface.c) +endif() + +idf_component_register(SRCS sta_to_eth_main.c + ${wired_iface} + ${config_method} + INCLUDE_DIRS "") diff --git a/examples/network/sta_to_eth/main/Kconfig.projbuild b/examples/network/sta_to_eth/main/Kconfig.projbuild new file mode 100644 index 0000000000..7b85896e38 --- /dev/null +++ b/examples/network/sta_to_eth/main/Kconfig.projbuild @@ -0,0 +1,89 @@ +menu "Example Configuration" + + orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps" + + choice EXAMPLE_WIFI_CONFIGURATION + prompt "WiFi configuration" + default EXAMPLE_WIFI_CONFIGURATION_MANUAL + help + Choose how the WiFi settings should be configured. + + config EXAMPLE_WIFI_CONFIGURATION_MANUAL + bool + prompt "Manual configuration via http server" + config EXAMPLE_WIFI_CONFIGURATION_PROVISIONING + bool + prompt "Using unified provisioning" + endchoice + + choice EXAMPLE_PROV_SECURITY_VERSION + bool "Protocomm security version" + depends on EXAMPLE_WIFI_CONFIGURATION_PROVISIONING + default EXAMPLE_PROV_SECURITY_VERSION_1 + help + Wi-Fi provisioning component offers 3 security versions. + The example offers a choice between security version 1 and 2. + You can also choose version 0, which is recommended only + for testing (not secure, plain text communication) + + config EXAMPLE_PROV_SECURITY_VERSION_1 + bool "Security version 1" + select ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_1 + + config EXAMPLE_PROV_SECURITY_VERSION_2 + bool "Security version 2" + select ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_2 + + config EXAMPLE_PROV_SECURITY_VERSION_0 + bool "Plain text communication -- not secure!" + select ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0 + + endchoice + + choice EXAMPLE_PROV_MODE + bool "Security version 2 mode" + depends on EXAMPLE_PROV_SECURITY_VERSION_2 + default EXAMPLE_PROV_SEC2_DEV_MODE + + config EXAMPLE_PROV_SEC2_DEV_MODE + bool "Security version 2 development mode" + depends on EXAMPLE_PROV_SECURITY_VERSION_2 + help + This enables the development mode for + security version 2. + Please note that this mode is NOT recommended for production purpose. + + config EXAMPLE_PROV_SEC2_PROD_MODE + bool "Security version 2 production mode" + depends on EXAMPLE_PROV_SECURITY_VERSION_2 + help + This enables the production mode for + security version 2. + endchoice + + choice EXAMPLE_WIRED_INTERFACE + prompt "Choose the Wired interface" + default EXAMPLE_WIRED_INTERFACE_IS_ETHERNET + help + Choose the wired interface: Ethernet or USB + + config EXAMPLE_WIRED_INTERFACE_IS_ETHERNET + bool + prompt "Ethernet" + config EXAMPLE_WIRED_INTERFACE_IS_USB + bool + depends on IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 + prompt "USB NCM" + endchoice + + config EXAMPLE_RECONFIGURE_BUTTON + int "Button for switching to reconfigure mode" + range ENV_GPIO_RANGE_MIN ENV_GPIO_IN_RANGE_MAX + default 2 if EXAMPLE_WIRED_INTERFACE_IS_ETHERNET + default 0 + help + The button on this GPIO is used to reset the board to + the reconfiguration mode, i.e. to restart provisioning + or manual configuration of Wi-Fi settings (ssid, password) + +endmenu diff --git a/examples/network/sta_to_eth/main/ethernet_iface.c b/examples/network/sta_to_eth/main/ethernet_iface.c new file mode 100644 index 0000000000..87c99182d3 --- /dev/null +++ b/examples/network/sta_to_eth/main/ethernet_iface.c @@ -0,0 +1,363 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "cc.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_event.h" +#include "wired_iface.h" +#include "dhcpserver/dhcpserver_options.h" +#include "esp_mac.h" +#include "ethernet_init.h" +#include "esp_eth_netif_glue.h" + +/** + * Disable promiscuous mode on Ethernet interface by setting this macro to 0 + * if disabled, we'd have to rewrite MAC addressed in frames with the actual Eth interface MAC address + * - this results in better throughput + * - might cause ARP conflicts if the PC is also connected to the same AP with another NIC + */ +#define ETH_BRIDGE_PROMISCUOUS 0 + +/** + * Set this to 1 to runtime update HW addresses in DHCP messages + * (this is needed if the client uses 61 option and the DHCP server applies strict rules on assigning addresses) + */ +#define MODIFY_DHCP_MSGS 0 + +static const char *TAG = "example_wired_ethernet"; +static esp_eth_handle_t s_eth_handle = NULL; +static bool s_ethernet_is_connected = false; +static uint8_t s_eth_mac[6]; +static wired_rx_cb_t s_rx_cb = NULL; +static wired_free_cb_t s_free_cb = NULL; + +/** + * @brief Event handler for Ethernet events + */ +void eth_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + uint8_t mac_addr[6] = {0}; + /* we can get the ethernet driver handle from event data */ + esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data; + esp_netif_t *netif = (esp_netif_t*)arg; + + switch (event_id) { + case ETHERNET_EVENT_CONNECTED: + ESP_LOGI(TAG, "Ethernet Link Up"); + esp_netif_dhcps_start(netif); + esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr); + ESP_LOGI(TAG, "Ethernet HW Addr %02x:%02x:%02x:%02x:%02x:%02x", + mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); + s_ethernet_is_connected = true; + break; + case ETHERNET_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "Ethernet Link Down"); + esp_netif_dhcps_stop(netif); + s_ethernet_is_connected = false; + break; + case ETHERNET_EVENT_START: + ESP_LOGI(TAG, "Ethernet Started"); + break; + case ETHERNET_EVENT_STOP: + ESP_LOGI(TAG, "Ethernet Stopped"); + break; + default: + ESP_LOGI(TAG, "Default Event"); + break; + } +} + +/** + * In this scenario of WiFi station to Ethernet bridge mode, we have this configuration + * + * (ISP) router ESP32 PC + * [ AP ] <-> [ sta -- eth ] <-> [ eth-NIC ] + * + * From the PC's NIC perspective the L2 forwarding should be transparent and resemble this configuration: + * + * (ISP) router PC + * [ AP ] <----------> [ virtual wifi-NIC ] + * + * In order for the ESP32 to act as L2 bridge it needs to accept all frames on the interface + * - For Ethernet we just enable `PROMISCUOUS` mode + * - For Wifi we could also enable the promiscuous mode, but in that case we'd receive encoded frames + * from 802.11 and we'd have to decode it and process (using wpa-supplicant). + * The easier option (in this scenario of only one client -- eth-NIC) we could simply "pretend" + * that we have the HW mac address of eth-NIC and receive only ethernet frames for "us" from esp_wifi API + * (we could use the same technique for Ethernet and yield better throughput, see ETH_BRIDGE_PROMISCUOUS flag) + * + * This API updates Ethernet frames to swap mac addresses of ESP32 interfaces with those of eth-NIC and AP. + * For that we'd have to parse initial DHCP packets (manually) to record the HW addresses of the AP and eth-NIC + * (note, that it is possible to simply spoof the MAC addresses, but that's not recommended technique) + */ +#define IP_V4 0x40 +#define IP_PROTO_UDP 0x11 +#define DHCP_PORT_IN 0x43 +#define DHCP_PORT_OUT 0x44 +#define DHCP_MACIG_COOKIE_OFFSET (8 + 236) +#define DHCP_HW_ADDRESS_OFFSET (36) +#define MIN_DHCP_PACKET_SIZE (285) +#define IP_HEADER_SIZE (20) +#define DHCP_DISCOVER 1 +#define DHCP_OFFER 2 +#define DHCP_COOKIE_WITH_PKT_TYPE(type) {0x63, 0x82, 0x53, 0x63, 0x35, 1, type}; + +#if MODIFY_DHCP_MSGS +static void update_udp_checksum(uint16_t *udp_header, uint16_t* ip_header) +{ + uint32_t sum = 0; + uint16_t *ptr = udp_header; + ptr[3] = 0; // clear the current checksum + int payload_len = htons(ip_header[1]) - IP_HEADER_SIZE; + // add UDP payload + for (int i = 0; i < payload_len/2; i++) { + sum += htons(*ptr++); + } + // add some IP header data + ptr = ip_header + 6; + for (int i = 0; i < 4; i++) { // IP addresses + sum += htons(*ptr++); + } + sum += IP_PROTO_UDP + payload_len; // protocol + size + do { + sum = (sum & 0xFFFF) + (sum >> 16); + } while (sum & 0xFFFF0000); // process the carry + ptr = udp_header; + ptr[3] = htons(~sum); // update the UDP header with the new checksum +} +#endif // MODIFY_DHCP_MSGS + +void mac_spoof(mac_spoof_direction_t direction, uint8_t *buffer, uint16_t len, uint8_t own_mac[6]) +{ + if (!s_ethernet_is_connected) { + return; + } + static uint8_t eth_nic_mac[6] = {}; + static bool eth_nic_mac_found = false; +#if !ETH_BRIDGE_PROMISCUOUS + static uint8_t ap_mac[6] = {}; + static bool ap_mac_found = false; +#endif + uint8_t *dest_mac = buffer; + uint8_t *src_mac = buffer + 6; + uint8_t *eth_type = buffer + 12; + if (eth_type[0] == 0x08) { // support only IPv4 + // try to find NIC HW address (look for DHCP discovery packet) + if ( (!eth_nic_mac_found || (MODIFY_DHCP_MSGS)) && direction == FROM_WIRED && eth_type[1] == 0x00) { // ETH IP4 + uint8_t *ip_header = eth_type + 2; + if (len > MIN_DHCP_PACKET_SIZE && (ip_header[0] & 0xF0) == IP_V4 && ip_header[9] == IP_PROTO_UDP) { + uint8_t *udp_header = ip_header + IP_HEADER_SIZE; + const uint8_t dhcp_ports[] = {0, DHCP_PORT_OUT, 0, DHCP_PORT_IN}; + if (memcmp(udp_header, dhcp_ports, sizeof(dhcp_ports)) == 0) { + uint8_t *dhcp_magic = udp_header + DHCP_MACIG_COOKIE_OFFSET; + const uint8_t dhcp_type[] = DHCP_COOKIE_WITH_PKT_TYPE(DHCP_DISCOVER); + if (!eth_nic_mac_found && memcmp(dhcp_magic, dhcp_type, sizeof(dhcp_type)) == 0) { + eth_nic_mac_found = true; + memcpy(eth_nic_mac, src_mac, 6); + } +#if MODIFY_DHCP_MSGS + if (eth_nic_mac_found) { + bool update_checksum = false; + // Replace the BOOTP HW address + uint8_t *dhcp_client_hw_addr = udp_header + DHCP_HW_ADDRESS_OFFSET; + if (memcmp(dhcp_client_hw_addr, eth_nic_mac, 6) == 0) { + memcpy(dhcp_client_hw_addr, own_mac, 6); + update_checksum = true; + } + // Replace the HW address in opt-61 + uint8_t *dhcp_opts = dhcp_magic + 4; + while (*dhcp_opts != 0xFF) { + if (dhcp_opts[0] == 61 && dhcp_opts[1] == 7 /* size (type=1 + mac=6) */ && dhcp_opts[2] == 1 /* HW address type*/ && + memcmp(dhcp_opts + 3, eth_nic_mac, 6) == 0) { + update_checksum = true; + memcpy(dhcp_opts + 3, own_mac, 6); + break; + } + dhcp_opts += dhcp_opts[1]+ 2; + if (dhcp_opts - buffer >= len) { + break; + } + } + if (update_checksum) { + update_udp_checksum((uint16_t *) udp_header, (uint16_t *) ip_header); + } + } +#endif // MODIFY_DHCP_MSGS + } // DHCP + } // UDP/IP +#if !ETH_BRIDGE_PROMISCUOUS || MODIFY_DHCP_MSGS + // try to find AP HW address (look for DHCP offer packet) + } else if ( (!ap_mac_found || (MODIFY_DHCP_MSGS)) && direction == TO_WIRED && eth_type[1] == 0x00) { // ETH IP4 + uint8_t *ip_header = eth_type + 2; + if (len > MIN_DHCP_PACKET_SIZE && (ip_header[0] & 0xF0) == IP_V4 && ip_header[9] == IP_PROTO_UDP) { + uint8_t *udp_header = ip_header + IP_HEADER_SIZE; + const uint8_t dhcp_ports[] = {0, DHCP_PORT_IN, 0, DHCP_PORT_OUT}; + if (memcmp(udp_header, dhcp_ports, sizeof(dhcp_ports)) == 0) { + uint8_t *dhcp_magic = udp_header + DHCP_MACIG_COOKIE_OFFSET; +#if MODIFY_DHCP_MSGS + if (eth_nic_mac_found) { + uint8_t *dhcp_client_hw_addr = udp_header + DHCP_HW_ADDRESS_OFFSET; + // Replace BOOTP HW address + if (memcmp(dhcp_client_hw_addr, own_mac, 6) == 0) { + memcpy(dhcp_client_hw_addr, eth_nic_mac, 6); + update_udp_checksum((uint16_t*)udp_header, (uint16_t*)ip_header); + } + } +#endif // MODIFY_DHCP_MSGS + const uint8_t dhcp_type[] = DHCP_COOKIE_WITH_PKT_TYPE(DHCP_OFFER); + if (!ap_mac_found && memcmp(dhcp_magic, dhcp_type, sizeof(dhcp_type)) == 0) { + ap_mac_found = true; + memcpy(ap_mac, src_mac, 6); + } + } // DHCP + } // UDP/IP +#endif // !ETH_BRIDGE_PROMISCUOUS || MODIFY_DHCP_MSGS + } + + // swap addresses in ARP probes + if (eth_type[1] == 0x06) { // ARP + uint8_t *arp = eth_type + 2 + 8; // points to sender's HW address + if (eth_nic_mac_found && direction == FROM_WIRED && memcmp(arp, eth_nic_mac, 6) == 0) { + /* updates senders HW address to our wireless */ + memcpy(arp, own_mac, 6); +#if !ETH_BRIDGE_PROMISCUOUS + } else if (ap_mac_found && direction == TO_WIRED && memcmp(arp, ap_mac, 6) == 0) { + /* updates senders HW address to our wired */ + memcpy(arp, s_eth_mac, 6); +#endif // !ETH_BRIDGE_PROMISCUOUS + } + } + // swap HW addresses in ETH frames +#if !ETH_BRIDGE_PROMISCUOUS + if (ap_mac_found && direction == FROM_WIRED && memcmp(dest_mac, s_eth_mac, 6) == 0) { + memcpy(dest_mac, ap_mac, 6); + } + if (ap_mac_found && direction == TO_WIRED && memcmp(src_mac, ap_mac, 6) == 0) { + memcpy(src_mac, s_eth_mac, 6); + } +#endif // !ETH_BRIDGE_PROMISCUOUS + if (eth_nic_mac_found && direction == FROM_WIRED && memcmp(src_mac, eth_nic_mac, 6) == 0) { + memcpy(src_mac, own_mac, 6); + } + if (eth_nic_mac_found && direction == TO_WIRED && memcmp(dest_mac, own_mac, 6) == 0) { + memcpy(dest_mac, eth_nic_mac, 6); + } + } // IP4 section of eth-type (0x08) both ETH-IP4 and ETHARP +} + +static esp_err_t wired_recv(esp_eth_handle_t eth_handle, uint8_t *buffer, uint32_t len, void *priv) +{ + esp_err_t ret = s_rx_cb(buffer, len, buffer); + free(buffer); + return ret; +} + +esp_err_t wired_bridge_init(wired_rx_cb_t rx_cb, wired_free_cb_t free_cb) +{ + uint8_t eth_port_cnt = 0; + esp_eth_handle_t *eth_handles; + ESP_ERROR_CHECK(example_eth_init(ð_handles, ð_port_cnt)); + + // Check for multiple Ethernet interfaces + if (1 < eth_port_cnt) { + ESP_LOGW(TAG, "Multiple Ethernet Interface detected: Only the first initialized interface is going to be used."); + } + s_eth_handle = eth_handles[0]; + free(eth_handles); + ESP_ERROR_CHECK(esp_eth_update_input_path(s_eth_handle, wired_recv, NULL)); +#if ETH_BRIDGE_PROMISCUOUS + bool eth_promiscuous = true; + ESP_ERROR_CHECK(esp_eth_ioctl(s_eth_handle, ETH_CMD_S_PROMISCUOUS, ð_promiscuous)); +#endif + ESP_ERROR_CHECK(esp_eth_ioctl(s_eth_handle, ETH_CMD_G_MAC_ADDR, &s_eth_mac)); + ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, eth_event_handler, NULL)); + ESP_ERROR_CHECK(esp_eth_start(s_eth_handle)); + s_rx_cb = rx_cb; + s_free_cb = free_cb; + return ESP_OK; +} + +esp_err_t wired_send(void *buffer, uint16_t len, void *buff_free_arg) +{ + if (s_ethernet_is_connected) { + if (esp_eth_transmit(s_eth_handle, buffer, len) != ESP_OK) { + ESP_LOGE(TAG, "Ethernet send packet failed"); + return ESP_FAIL; + } + if (s_free_cb) { + s_free_cb(buff_free_arg, NULL); + } + return ESP_OK; + } + return ESP_ERR_INVALID_STATE; +} + +/** + * In this scenario of configuring WiFi, we setup Ethernet to create a network and run DHCP server, + * so it could assign an IP address to the PC + * + * ESP32 PC + * | eth | <-> [ eth-NIC ] + * | | + * | (wifi-provisioning) | + * + * From the PC's NIC perspective the board acts as a separate network with it's own IP and MAC address + * (this network's MAC address is the native ESP32's Ethernet interface MAC) + */ + +esp_err_t wired_netif_init(void) +{ + uint8_t eth_port_cnt = 0; + esp_eth_handle_t *eth_handles; + ESP_ERROR_CHECK(example_eth_init(ð_handles, ð_port_cnt)); + + // Check or multiple ethernet interface + if (1 < eth_port_cnt) { + ESP_LOGW(TAG, "Multiple Ethernet Interface detected: Only the first initialized interface is going to be used."); + } + s_eth_handle = eth_handles[0]; + free(eth_handles); + + // 1) Derive the base config (very similar to IDF's default WiFi AP with DHCP server) + esp_netif_inherent_config_t base_cfg = { + .flags = ESP_NETIF_DHCP_SERVER, // Run DHCP server + .ip_info = &_g_esp_netif_soft_ap_ip, // Use the same IP ranges as IDF's soft AP + .if_key = "wired", // Set mame, key, priority + .if_desc = "ethernet config device", + .route_prio = 10 + }; + + // Config the esp-netif with: + // 1) inherent config (behavioural settings of an interface) + // 2) driver's config -- no need, will use the default ethernet-netif glue and attach it to this netif + // 3) stack config -- will use the default ethernet TCP/IP settings + esp_netif_config_t cfg = { + .base = &base_cfg, + .stack = ESP_NETIF_NETSTACK_DEFAULT_ETH + }; + + esp_netif_t *netif = esp_netif_new(&cfg); + if (netif == NULL) { + return ESP_FAIL; + } + + // Now we attach the constructed network interface to IDF's default ethernet glue + esp_eth_netif_glue_handle_t eth_glue = esp_eth_new_netif_glue(s_eth_handle); + ESP_ERROR_CHECK(esp_netif_attach(netif, eth_glue)); + + uint8_t mac[6]; + ESP_ERROR_CHECK(esp_eth_ioctl(s_eth_handle, ETH_CMD_G_MAC_ADDR, &mac)); + esp_netif_set_mac(netif, mac); + + // set the minimum lease time + uint32_t lease_opt = 1; + esp_netif_dhcps_option(netif, ESP_NETIF_OP_SET, IP_ADDRESS_LEASE_TIME, &lease_opt, sizeof(lease_opt)); + ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, eth_event_handler, netif)); + ESP_ERROR_CHECK(esp_eth_start(s_eth_handle)); + return ESP_OK; +} diff --git a/examples/network/sta_to_eth/main/idf_component.yml b/examples/network/sta_to_eth/main/idf_component.yml new file mode 100644 index 0000000000..bff20c669a --- /dev/null +++ b/examples/network/sta_to_eth/main/idf_component.yml @@ -0,0 +1,9 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/esp_tinyusb: + version: "^1.3.0" + rules: + - if: "idf_version >=4.4" + - if: "target in [esp32s2, esp32s3]" + + idf: "^5.0" diff --git a/examples/network/sta_to_eth/main/manual_config.c b/examples/network/sta_to_eth/main/manual_config.c new file mode 100644 index 0000000000..e95d041822 --- /dev/null +++ b/examples/network/sta_to_eth/main/manual_config.c @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "esp_wifi.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_http_server.h" +#include "dns_server.h" + +static const char *TAG = "NCM_configuration"; +static httpd_handle_t s_web_server = NULL; +static EventGroupHandle_t *s_flags = NULL; +static int s_success_bit; + +bool is_provisioned(void) +{ + wifi_config_t wifi_cfg; + if (esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + return false; + } + + if (strlen((const char *) wifi_cfg.sta.ssid)) { + return true; + } + + return false; +} + +static esp_err_t http_get_handler(httpd_req_t *req) +{ + const char page[] = "


\n" + "SSID:

\n" + "Password:

\n" + " " + "
"; + char *buf = NULL; + size_t buf_len; + buf_len = httpd_req_get_url_query_len(req) + 1; + if (buf_len > 1) { + buf = malloc(buf_len); + if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) { + ESP_LOGI(TAG, "Found URL query => %s", buf); + char param[32]; + wifi_config_t wifi_cfg = {}; + + if (httpd_query_key_value(buf, "ssid", param, sizeof(param)) == ESP_OK) { + ESP_LOGI(TAG, "ssid=%s", param); + strncpy((char *)wifi_cfg.sta.ssid, param, sizeof(wifi_cfg.sta.ssid)); + } + if (httpd_query_key_value(buf, "password", param, sizeof(param)) == ESP_OK) { + ESP_LOGI(TAG, "password=%s", param); + strncpy((char *)wifi_cfg.sta.password, param, sizeof(wifi_cfg.sta.password)); + } + + if (strlen((char *)wifi_cfg.sta.ssid) > 0 && strlen((char *)wifi_cfg.sta.password)) { + httpd_resp_set_type(req, "text/html"); + esp_wifi_set_mode(WIFI_MODE_STA); + if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) == ESP_OK && + esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg) == ESP_OK) { + const char wifi_configured[] = "

Connecting...

"; + ESP_LOGI(TAG, "WiFi settings accepted!"); + httpd_resp_send(req, wifi_configured, strlen(wifi_configured)); + } else { + const char wifi_config_failed[] = "

Failed to configure WiFi settings

"; + ESP_LOGE(TAG, "Failed to set WiFi config to flash"); + httpd_resp_send(req, wifi_config_failed, strlen(wifi_config_failed)); + } + + free(buf); + if (s_flags) { + xEventGroupSetBits(*s_flags, s_success_bit); + } + return ESP_OK; + } + } + free(buf); + } + + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, page, sizeof(page)); + + return ESP_OK; +} + +static const httpd_uri_t root = { + .uri = "/", + .method = HTTP_GET, + .handler = http_get_handler, +}; + +static void start_webserver(void) +{ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.max_open_sockets = 3; + config.lru_purge_enable = true; + + // Start the httpd server + ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); + if (httpd_start(&s_web_server, &config) == ESP_OK) { + // Set URI handlers + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(s_web_server, &root); + } +} + +esp_err_t start_provisioning(EventGroupHandle_t *flags, int success_bit, int fail_bit) +{ + start_webserver(); + // Start the DNS server that will reply to "wifi.settings" with "usb" network interface address + dns_server_config_t config = DNS_SERVER_CONFIG_SINGLE("wifi.settings" /* name */, "wired" /* USB netif ID */); + start_dns_server(&config); + + s_flags = flags; + s_success_bit = success_bit; + return ESP_OK; +} diff --git a/examples/network/sta_to_eth/main/provisioning.c b/examples/network/sta_to_eth/main/provisioning.c new file mode 100644 index 0000000000..2a08088323 --- /dev/null +++ b/examples/network/sta_to_eth/main/provisioning.c @@ -0,0 +1,204 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "esp_log.h" +#include "esp_mac.h" +#include "esp_event.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "dns_server.h" + +static const char *TAG = "NCM_provisioning"; + +#if CONFIG_EXAMPLE_PROV_SECURITY_VERSION_2 +#if CONFIG_EXAMPLE_PROV_SEC2_DEV_MODE +#define EXAMPLE_PROV_SEC2_USERNAME "wifiprov" +#define EXAMPLE_PROV_SEC2_PWD "abcd1234" + +/* This salt,verifier has been generated for username = "wifiprov" and password = "abcd1234" + * IMPORTANT NOTE: For production cases, this must be unique to every device + * and should come from device manufacturing partition.*/ +static const char sec2_salt[] = { + 0x03, 0x6e, 0xe0, 0xc7, 0xbc, 0xb9, 0xed, 0xa8, 0x4c, 0x9e, 0xac, 0x97, 0xd9, 0x3d, 0xec, 0xf4 +}; + +static const char sec2_verifier[] = { + 0x7c, 0x7c, 0x85, 0x47, 0x65, 0x08, 0x94, 0x6d, 0xd6, 0x36, 0xaf, 0x37, 0xd7, 0xe8, 0x91, 0x43, + 0x78, 0xcf, 0xfd, 0x61, 0x6c, 0x59, 0xd2, 0xf8, 0x39, 0x08, 0x12, 0x72, 0x38, 0xde, 0x9e, 0x24, + 0xa4, 0x70, 0x26, 0x1c, 0xdf, 0xa9, 0x03, 0xc2, 0xb2, 0x70, 0xe7, 0xb1, 0x32, 0x24, 0xda, 0x11, + 0x1d, 0x97, 0x18, 0xdc, 0x60, 0x72, 0x08, 0xcc, 0x9a, 0xc9, 0x0c, 0x48, 0x27, 0xe2, 0xae, 0x89, + 0xaa, 0x16, 0x25, 0xb8, 0x04, 0xd2, 0x1a, 0x9b, 0x3a, 0x8f, 0x37, 0xf6, 0xe4, 0x3a, 0x71, 0x2e, + 0xe1, 0x27, 0x86, 0x6e, 0xad, 0xce, 0x28, 0xff, 0x54, 0x46, 0x60, 0x1f, 0xb9, 0x96, 0x87, 0xdc, + 0x57, 0x40, 0xa7, 0xd4, 0x6c, 0xc9, 0x77, 0x54, 0xdc, 0x16, 0x82, 0xf0, 0xed, 0x35, 0x6a, 0xc4, + 0x70, 0xad, 0x3d, 0x90, 0xb5, 0x81, 0x94, 0x70, 0xd7, 0xbc, 0x65, 0xb2, 0xd5, 0x18, 0xe0, 0x2e, + 0xc3, 0xa5, 0xf9, 0x68, 0xdd, 0x64, 0x7b, 0xb8, 0xb7, 0x3c, 0x9c, 0xfc, 0x00, 0xd8, 0x71, 0x7e, + 0xb7, 0x9a, 0x7c, 0xb1, 0xb7, 0xc2, 0xc3, 0x18, 0x34, 0x29, 0x32, 0x43, 0x3e, 0x00, 0x99, 0xe9, + 0x82, 0x94, 0xe3, 0xd8, 0x2a, 0xb0, 0x96, 0x29, 0xb7, 0xdf, 0x0e, 0x5f, 0x08, 0x33, 0x40, 0x76, + 0x52, 0x91, 0x32, 0x00, 0x9f, 0x97, 0x2c, 0x89, 0x6c, 0x39, 0x1e, 0xc8, 0x28, 0x05, 0x44, 0x17, + 0x3f, 0x68, 0x02, 0x8a, 0x9f, 0x44, 0x61, 0xd1, 0xf5, 0xa1, 0x7e, 0x5a, 0x70, 0xd2, 0xc7, 0x23, + 0x81, 0xcb, 0x38, 0x68, 0xe4, 0x2c, 0x20, 0xbc, 0x40, 0x57, 0x76, 0x17, 0xbd, 0x08, 0xb8, 0x96, + 0xbc, 0x26, 0xeb, 0x32, 0x46, 0x69, 0x35, 0x05, 0x8c, 0x15, 0x70, 0xd9, 0x1b, 0xe9, 0xbe, 0xcc, + 0xa9, 0x38, 0xa6, 0x67, 0xf0, 0xad, 0x50, 0x13, 0x19, 0x72, 0x64, 0xbf, 0x52, 0xc2, 0x34, 0xe2, + 0x1b, 0x11, 0x79, 0x74, 0x72, 0xbd, 0x34, 0x5b, 0xb1, 0xe2, 0xfd, 0x66, 0x73, 0xfe, 0x71, 0x64, + 0x74, 0xd0, 0x4e, 0xbc, 0x51, 0x24, 0x19, 0x40, 0x87, 0x0e, 0x92, 0x40, 0xe6, 0x21, 0xe7, 0x2d, + 0x4e, 0x37, 0x76, 0x2f, 0x2e, 0xe2, 0x68, 0xc7, 0x89, 0xe8, 0x32, 0x13, 0x42, 0x06, 0x84, 0x84, + 0x53, 0x4a, 0xb3, 0x0c, 0x1b, 0x4c, 0x8d, 0x1c, 0x51, 0x97, 0x19, 0xab, 0xae, 0x77, 0xff, 0xdb, + 0xec, 0xf0, 0x10, 0x95, 0x34, 0x33, 0x6b, 0xcb, 0x3e, 0x84, 0x0f, 0xb9, 0xd8, 0x5f, 0xb8, 0xa0, + 0xb8, 0x55, 0x53, 0x3e, 0x70, 0xf7, 0x18, 0xf5, 0xce, 0x7b, 0x4e, 0xbf, 0x27, 0xce, 0xce, 0xa8, + 0xb3, 0xbe, 0x40, 0xc5, 0xc5, 0x32, 0x29, 0x3e, 0x71, 0x64, 0x9e, 0xde, 0x8c, 0xf6, 0x75, 0xa1, + 0xe6, 0xf6, 0x53, 0xc8, 0x31, 0xa8, 0x78, 0xde, 0x50, 0x40, 0xf7, 0x62, 0xde, 0x36, 0xb2, 0xba +}; +#endif + +static esp_err_t example_get_sec2_salt(const char **salt, uint16_t *salt_len) +{ +#if CONFIG_EXAMPLE_PROV_SEC2_DEV_MODE + ESP_LOGI(TAG, "Development mode: using hard coded salt"); + *salt = sec2_salt; + *salt_len = sizeof(sec2_salt); + return ESP_OK; +#elif CONFIG_EXAMPLE_PROV_SEC2_PROD_MODE + ESP_LOGE(TAG, "Not implemented!"); + return ESP_FAIL; +#endif +} + +static esp_err_t example_get_sec2_verifier(const char **verifier, uint16_t *verifier_len) +{ +#if CONFIG_EXAMPLE_PROV_SEC2_DEV_MODE + ESP_LOGI(TAG, "Development mode: using hard coded verifier"); + *verifier = sec2_verifier; + *verifier_len = sizeof(sec2_verifier); + return ESP_OK; +#elif CONFIG_EXAMPLE_PROV_SEC2_PROD_MODE + /* This code needs to be updated with appropriate implementation to provide verifier */ + ESP_LOGE(TAG, "Not implemented!"); + return ESP_FAIL; +#endif +} +#endif // CONFIG_EXAMPLE_PROV_SECURITY_VERSION_2 + +struct events { + EventGroupHandle_t *flags; + int success_bit; + int fail_bit; + bool success; +}; + +static void event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + struct events *handler_args = arg; + switch (event_id) { + case WIFI_PROV_START: + ESP_LOGI(TAG, "Provisioning started"); + break; + case WIFI_PROV_CRED_RECV: { + wifi_sta_config_t *wifi_sta_cfg = (wifi_sta_config_t *) event_data; + ESP_LOGI(TAG, "Received Wi-Fi credentials" + "\n\tSSID : %s\n\tPassword : %s", + (const char *) wifi_sta_cfg->ssid, + (const char *) wifi_sta_cfg->password); + break; + } + case WIFI_PROV_CRED_FAIL: { + wifi_prov_sta_fail_reason_t *reason = (wifi_prov_sta_fail_reason_t *) event_data; + ESP_LOGE(TAG, "Provisioning failed!\n\tReason : %s" + "\n\tPlease reset to factory and retry provisioning", + (*reason == WIFI_PROV_STA_AUTH_ERROR) ? + "Wi-Fi station authentication failed" : "Wi-Fi access-point not found"); + handler_args->success = false; + + break; + } + case WIFI_PROV_CRED_SUCCESS: + ESP_LOGI(TAG, "Provisioning successful"); + handler_args->success = true; + break; + case WIFI_PROV_END: + /* De-initialize manager once provisioning is finished */ + wifi_prov_mgr_deinit(); + xEventGroupSetBits(*handler_args->flags, handler_args->success ? handler_args->success_bit : handler_args->fail_bit); + free(handler_args); + break; + default: + break; + } +} + +extern const wifi_prov_scheme_t wifi_prov_scheme_httpd; + +esp_err_t start_provisioning(EventGroupHandle_t *flags, int success_bit, int fail_bit) +{ + // Start the DNS server that will reply to "wifi.settings" with "usb" network interface address + dns_server_config_t dns_config = DNS_SERVER_CONFIG_SINGLE("wifi.settings" /* name */, "wired" /* wired netif ID */); + start_dns_server(&dns_config); + struct events *handler_args = malloc(sizeof(struct events)); + handler_args->flags = flags; + handler_args->success_bit = success_bit; + handler_args->fail_bit = fail_bit; + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_PROV_EVENT, ESP_EVENT_ANY_ID, event_handler, handler_args)); + /* Configuration for the provisioning manager */ + wifi_prov_mgr_config_t config = { + .scheme = wifi_prov_scheme_httpd, + }; + + /* Initialize provisioning manager with the + * configuration parameters set above */ + ESP_ERROR_CHECK(wifi_prov_mgr_init(config)); + + /* What is the security level that we want (0, 1, 2): + * - WIFI_PROV_SECURITY_0 is simply plain text communication. + * - WIFI_PROV_SECURITY_1 is secure communication which consists of secure handshake + * - WIFI_PROV_SECURITY_2 SRP6a based authentication and key exchange + * Please check unified provisioning documentation for more details + */ + +#ifdef CONFIG_EXAMPLE_PROV_SECURITY_VERSION_0 + wifi_prov_security_t security = WIFI_PROV_SECURITY_0; +#elif CONFIG_EXAMPLE_PROV_SECURITY_VERSION_1 + wifi_prov_security_t security = WIFI_PROV_SECURITY_1; + const char *pop = "abcd1234"; /* Proof of possession */ + wifi_prov_security1_params_t *sec_params = pop; + +#elif CONFIG_EXAMPLE_PROV_SECURITY_VERSION_2 + wifi_prov_security_t security = WIFI_PROV_SECURITY_2; + /* The username must be the same one, which has been used in the generation of salt and verifier */ + +#if CONFIG_EXAMPLE_PROV_SEC2_DEV_MODE + /* This pop field represents the password that will be used to generate salt and verifier. + * The field is present here in order to generate the QR code containing password. + * In production this password field shall not be stored on the device */ + const char *username = EXAMPLE_PROV_SEC2_USERNAME; + const char *pop = EXAMPLE_PROV_SEC2_PWD; +#elif CONFIG_EXAMPLE_PROV_SEC2_PROD_MODE + /* The username and password shall not be embedded in the firmware */ + const char *username = NULL; + const char *pop = NULL; +#endif + /* This is the structure for passing security parameters + * for the protocomm security 2. + * If dynamically allocated, sec2_params pointer and its content + * must be valid till WIFI_PROV_END event is triggered. + */ + wifi_prov_security2_params_t sec2_params = {}; + + ESP_ERROR_CHECK(example_get_sec2_salt(&sec2_params.salt, &sec2_params.salt_len)); + ESP_ERROR_CHECK(example_get_sec2_verifier(&sec2_params.verifier, &sec2_params.verifier_len)); + + wifi_prov_security2_params_t *sec_params = &sec2_params; +#endif // CONFIG_EXAMPLE_PROV_SECURITY_VERSION_0 (VERSION_1, VERSION_2) + + ESP_ERROR_CHECK(wifi_prov_mgr_start_provisioning(security, (const void *) sec_params, NULL, NULL)); // service name and key could be NULL + return ESP_OK; +} + +bool is_provisioned(void) +{ + bool provisioned = false; + ESP_ERROR_CHECK(wifi_prov_mgr_is_provisioned(&provisioned)); + return provisioned; +} diff --git a/examples/network/sta_to_eth/main/provisioning.h b/examples/network/sta_to_eth/main/provisioning.h new file mode 100644 index 0000000000..a17d98469b --- /dev/null +++ b/examples/network/sta_to_eth/main/provisioning.h @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +/** + * @brief Checks if the device has been provisioned + * @return true if WiFi is provisioned + */ +bool is_provisioned(void); + +/** + * @brief Initiate provisioning + * @param flags Event flags to indicate status of provisioning + * @param success_bit bits set in the event flags on success + * @param fail_bit bits set in the event flags on failure + * @return ESP_OK if provisioning started + */ +esp_err_t start_provisioning(EventGroupHandle_t *flags, int success_bit, int fail_bit); diff --git a/examples/network/sta_to_eth/main/scheme_generic_httpd.c b/examples/network/sta_to_eth/main/scheme_generic_httpd.c new file mode 100644 index 0000000000..e0bd85bbca --- /dev/null +++ b/examples/network/sta_to_eth/main/scheme_generic_httpd.c @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include "sdkconfig.h" +#include +#include +#include +#include +#include + +static const char *TAG = "wifi_prov_scheme_httpd"; + +static esp_err_t prov_start(protocomm_t *pc, void *config) +{ + if (!pc) { + ESP_LOGE(TAG, "Protocomm handle cannot be null"); + return ESP_ERR_INVALID_ARG; + } + + if (!config) { + ESP_LOGE(TAG, "Cannot start with null configuration"); + return ESP_ERR_INVALID_ARG; + } + protocomm_httpd_config_t default_config = { + .data = { + .config = PROTOCOMM_HTTPD_DEFAULT_CONFIG() + } + }; + + /* Start protocomm server on top of HTTP */ + esp_err_t err = protocomm_httpd_start(pc, &default_config); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start protocomm HTTP server"); + return err; + } + + return ESP_OK; +} + +static esp_err_t prov_stop(protocomm_t *pc) +{ + esp_err_t err = protocomm_httpd_stop(pc); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error occurred while stopping protocomm_httpd"); + } + + return err; +} + +/** + * @brief Creates a configuration for this custom provisioning scheme. + * + * We don't need to pass any config option at this moment, so we create + * a dummy configuration since provisioning manager check for non-nullptr. + * If needed we can extend this scheme to provide some options for httpd + * or wifi provisioning. + */ +static void *new_config(void) +{ + return (void *)1; +} + +static void delete_config(void *config) +{ +} + +static esp_err_t set_config_service(void *config, const char *service_name, const char *service_key) +{ + return ESP_OK; +} + +static esp_err_t set_config_endpoint(void *config, const char *endpoint_name, uint16_t uuid) +{ + return ESP_OK; +} + +/** + * @brief Creating a generic HTTPD scheme + */ +const wifi_prov_scheme_t wifi_prov_scheme_httpd = { + .prov_start = prov_start, + .prov_stop = prov_stop, + .new_config = new_config, + .delete_config = delete_config, + .set_config_service = set_config_service, + .set_config_endpoint = set_config_endpoint, + .wifi_mode = WIFI_MODE_STA +}; diff --git a/examples/network/sta_to_eth/main/sta_to_eth_main.c b/examples/network/sta_to_eth/main/sta_to_eth_main.c new file mode 100644 index 0000000000..4729d34382 --- /dev/null +++ b/examples/network/sta_to_eth/main/sta_to_eth_main.c @@ -0,0 +1,212 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" + +#include "esp_log.h" +#include "esp_wifi.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include "esp_event.h" +#include "esp_private/wifi.h" +#include "nvs_flash.h" +#include "driver/gpio.h" +#include "provisioning.h" +#include "wired_iface.h" + +static const char *TAG = "example_sta2wired"; + +static EventGroupHandle_t s_event_flags; +static bool s_wifi_is_connected = false; +static uint8_t s_sta_mac[6]; + +const int CONNECTED_BIT = BIT0; +const int DISCONNECTED_BIT = BIT1; +const int RECONFIGURE_BIT = BIT2; +const int PROV_SUCCESS_BIT = BIT3; +const int PROV_FAIL_BIT = BIT4; + +/** + * WiFi -- Wired packet path + */ +static esp_err_t wired_recv_callback(void *buffer, uint16_t len, void *ctx) +{ + if (s_wifi_is_connected) { + mac_spoof(FROM_WIRED, buffer, len, s_sta_mac); + if (esp_wifi_internal_tx(ESP_IF_WIFI_STA, buffer, len) != ESP_OK) { + ESP_LOGD(TAG, "Failed to send packet to WiFi!"); + } + } + return ESP_OK; +} + +static void wifi_buff_free(void *buffer, void *ctx) +{ + esp_wifi_internal_free_rx_buffer(buffer); +} + +static esp_err_t wifi_recv_callback(void *buffer, uint16_t len, void *eb) +{ + mac_spoof(TO_WIRED, buffer, len, s_sta_mac); + if (wired_send(buffer, len, eb) != ESP_OK) { + esp_wifi_internal_free_rx_buffer(eb); + ESP_LOGD(TAG, "Failed to send packet to USB!"); + } + return ESP_OK; +} + +static void event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + ESP_LOGI(TAG, "Wi-Fi STA disconnected"); + s_wifi_is_connected = false; + esp_wifi_internal_reg_rxcb(ESP_IF_WIFI_STA, NULL); + esp_wifi_connect(); + + xEventGroupClearBits(s_event_flags, CONNECTED_BIT); + xEventGroupSetBits(s_event_flags, DISCONNECTED_BIT); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) { + ESP_LOGI(TAG, "Wi-Fi STA connected"); + esp_wifi_internal_reg_rxcb(ESP_IF_WIFI_STA, wifi_recv_callback); + s_wifi_is_connected = true; + xEventGroupClearBits(s_event_flags, DISCONNECTED_BIT); + xEventGroupSetBits(s_event_flags, CONNECTED_BIT); + } +} + +static esp_err_t connect_wifi(void) +{ + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, event_handler, NULL)); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) ); + ESP_ERROR_CHECK(esp_wifi_start() ); + + wifi_config_t wifi_cfg; + if (esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) { + // configuration not available, report error to restart provisioning + return ESP_FAIL; + } + esp_wifi_connect(); + EventBits_t status = xEventGroupWaitBits(s_event_flags, CONNECTED_BIT, 0, 1, 10000 / portTICK_PERIOD_MS); + if (status & CONNECTED_BIT) { + ESP_LOGI(TAG, "WiFi station connected successfully"); + return ESP_OK; + } + ESP_LOGE(TAG, "WiFi station connected failed"); + return ESP_ERR_TIMEOUT; +} + +/** + * GPIO button functionality + */ +#define GPIO_INPUT CONFIG_EXAMPLE_RECONFIGURE_BUTTON +#define GPIO_LONG_PUSH_US 2000000 /* push for 2 seconds to reconfigure */ + +static void IRAM_ATTR gpio_isr_handler(void *arg) +{ + static int64_t last_pushed = -1; + if (gpio_get_level(GPIO_INPUT) == 0) { + last_pushed = esp_timer_get_time(); + } else { + uint64_t now = esp_timer_get_time(); + if (last_pushed != -1 && now - last_pushed > GPIO_LONG_PUSH_US) { + BaseType_t high_task_wakeup; + xEventGroupSetBitsFromISR(s_event_flags, RECONFIGURE_BIT, &high_task_wakeup); + if (high_task_wakeup) { + portYIELD_FROM_ISR(); + } + } + last_pushed = -1; + } +} + +static void gpio_init(void) +{ + gpio_config_t io_conf = { .intr_type = GPIO_INTR_ANYEDGE, + .pin_bit_mask = (1ULL << GPIO_INPUT), + .mode = GPIO_MODE_INPUT, + .pull_up_en = 1 + }; + gpio_config(&io_conf); + gpio_install_isr_service(0); + //hook isr handler for specific gpio pin + gpio_isr_handler_add(GPIO_INPUT, gpio_isr_handler, NULL); +} + +/** + * Application + */ +void app_main(void) +{ + static __NOINIT_ATTR uint32_t s_reconfigure_requested; + static const uint32_t RECONFIGURE_REQUEST = 0x1C55AA; + + /* Check reset reason and decide if we should re-provision */ + bool do_provision = false; + esp_reset_reason_t reason = esp_reset_reason(); + ESP_LOGD(TAG, "After restart! %d", reason); + if (reason != ESP_RST_SW) { + s_reconfigure_requested = 0; + } else if (s_reconfigure_requested == RECONFIGURE_REQUEST) { + do_provision = true; + } + + /* Initialize NVS and WiFi */ + 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); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + // init the flags and event loop + s_event_flags = xEventGroupCreate(); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* Init the re-provisioning button (long-press with initiate provisioning restart) */ + gpio_init(); + esp_read_mac(s_sta_mac, ESP_MAC_WIFI_STA); + + /* Start the application in configuration mode (to perform provisioning) + * or in a bridge mode (already provisioned) */ + if (do_provision || !is_provisioned()) { + ESP_LOGI(TAG, "Starting provisioning"); + ESP_ERROR_CHECK(esp_netif_init()); + // needed to complete provisioning with getting a valid IP event + esp_netif_create_default_wifi_sta(); + + // starts the wired interface with virtual network used to configure/provision the example + wired_netif_init(); + start_provisioning(&s_event_flags, PROV_SUCCESS_BIT, PROV_FAIL_BIT); + } else { + ESP_LOGI(TAG, "Starting USB-WiFi bridge"); + if (connect_wifi() != ESP_OK) { + // if we cannot connect to WiFi we just try to re-provision + xEventGroupSetBits(s_event_flags, RECONFIGURE_BIT); + } else { + // start the wired interface in the bridge mode + wired_bridge_init(wired_recv_callback, wifi_buff_free); + } + } + + EventBits_t bits = xEventGroupWaitBits(s_event_flags, RECONFIGURE_BIT | PROV_SUCCESS_BIT | PROV_FAIL_BIT, pdTRUE, pdFALSE, portMAX_DELAY); + if (bits & RECONFIGURE_BIT || bits & PROV_FAIL_BIT) { + // retry provisioning if it previously failed or if requested by the button press + s_reconfigure_requested = RECONFIGURE_REQUEST; + } else { + // provisioning successfully finished, restart to the bridge mode + s_reconfigure_requested = 0; + } + + vTaskDelay(pdMS_TO_TICKS(1000)); // to let httpd handle the closure + esp_restart(); +} diff --git a/examples/network/sta_to_eth/main/usb_ncm_iface.c b/examples/network/sta_to_eth/main/usb_ncm_iface.c new file mode 100644 index 0000000000..6f8234977a --- /dev/null +++ b/examples/network/sta_to_eth/main/usb_ncm_iface.c @@ -0,0 +1,185 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* DESCRIPTION: + * This example contains code to make ESP32-S2/S3 as a USB network Device. + */ +#include +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_event.h" +#include "tinyusb.h" +#include "tinyusb_net.h" +#include "wired_iface.h" +#include "dhcpserver/dhcpserver_options.h" +#include "lwip/esp_netif_net_stack.h" +#include "esp_mac.h" + +static const char *TAG = "example_wired_tusb_ncm"; +static esp_netif_t *s_netif = NULL; + +/** + * In this scenario of WiFi station to Ethernet bridge mode, we have this configuration + * + * (ISP) router ESP32 PC + * [ AP ] <-> [ sta -- USB ] <-> [ USB-NCM device acting as eth-NIC ] + * + * From the PC's NIC perspective the L2 forwarding should be transparent and resemble this configuration: + * + * (ISP) router PC + * [ AP ] <----------> [ virtual wifi-NIC ] + * + * In order for the ESP32 to act as L2 bridge it needs to accept the frames for the NCM device, + * which we have fully under control, we can modify it's MAC address, as well as the WiFi station + * MAC address, which need to be the same so the AP would see one device (virtual eth-NIC). + * No need to modify the ethernet frames here, as we can set the station's MAC to the USB NCM device. + */ +void mac_spoof(mac_spoof_direction_t direction, uint8_t *buffer, uint16_t len, uint8_t own_mac[6]) +{ + +} + +esp_err_t wired_bridge_init(wired_rx_cb_t rx_cb, wired_free_cb_t free_cb) +{ + const tinyusb_config_t tusb_cfg = { + .external_phy = false, + }; + ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg)); + + tinyusb_net_config_t net_config = { + .on_recv_callback = rx_cb, + .free_tx_buffer = free_cb, + }; + + esp_read_mac(net_config.mac_addr, ESP_MAC_WIFI_STA); + + esp_err_t ret = tinyusb_net_init(TINYUSB_USBDEV_0, &net_config); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "USB net init but not connect wifi"); + return ret; + } + return ESP_OK; +} + + +esp_err_t wired_send(void *buffer, uint16_t len, void *buff_free_arg) +{ + return tinyusb_net_send_sync(buffer, len, buff_free_arg, pdMS_TO_TICKS(100)); +} + +static void l2_free(void *h, void *buffer) +{ + free(buffer); +} + +static esp_err_t netif_transmit (void *h, void *buffer, size_t len) +{ + if (wired_send(buffer, len, NULL) != ESP_OK) { + ESP_LOGE(TAG, "Failed to send buffer to USB!"); + } + return ESP_OK; +} + +static esp_err_t netif_recv_callback(void *buffer, uint16_t len, void *ctx) +{ + if (s_netif) { + void *buf_copy = malloc(len); + if (!buf_copy) { + return ESP_ERR_NO_MEM; + } + memcpy(buf_copy, buffer, len); + return esp_netif_receive(s_netif, buf_copy, len, NULL); + } + return ESP_OK; +} + +/** + * In this scenario of configuring WiFi, we setup USB-Ethernet to create a virtual network and run DHCP server, + * so it could assign an IP address to the PC + * + * ESP32 PC + * | lwip MAC=...01 | eth NIC MAC=...02 + * | usb | <-> [ USB-NCM device acting as eth-NIC ] + * | | + * | (wifi-provisioning) | + * + * From the PC's NIC perspective the board acts as a separate network with it's own IP and MAC address, + * but the virtual ethernet NIC has also it's own IP and MAC address (configured via tinyusb_net_init()). + * That's why we need to create the virtual network with *different* MAC address. + * Here, we use two different OUI range MAC addresses. + */ +esp_err_t wired_netif_init(void) +{ + const tinyusb_config_t tusb_cfg = { + .external_phy = false, + }; + ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg)); + + const tinyusb_net_config_t net_config = { + // locally administrated address for the ncm device as it's going to be used internally + // for configuration only + .mac_addr = {0x02, 0x02, 0x11, 0x22, 0x33, 0x01}, + .on_recv_callback = netif_recv_callback, + }; + + esp_err_t ret = tinyusb_net_init(TINYUSB_USBDEV_0, &net_config); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Cannot initialize USB Net device"); + return ret; + } + + // with OUI range MAC to create a virtual netif running http server + // this needs to be different to usb_interface_mac (==client) + uint8_t lwip_addr[6] = {0x02, 0x02, 0x11, 0x22, 0x33, 0x02}; + + // Definition of + // 1) Derive the base config (very similar to IDF's default WiFi AP with DHCP server) + esp_netif_inherent_config_t base_cfg = { + .flags = ESP_NETIF_DHCP_SERVER | ESP_NETIF_FLAG_AUTOUP, // Run DHCP server; set the netif "ip" immediately + .ip_info = &_g_esp_netif_soft_ap_ip, // Use the same IP ranges as IDF's soft AP + .if_key = "wired", // Set mame, key, priority + .if_desc = "usb ncm config device", + .route_prio = 10 + }; + // 2) Use static config for driver's config pointing only to static transmit and free functions + esp_netif_driver_ifconfig_t driver_cfg = { + .handle = (void *)1, // not using an instance, USB-NCM is a static singleton (must be != NULL) + .transmit = netif_transmit, // point to static Tx function + .driver_free_rx_buffer = l2_free // point to Free Rx buffer function + }; + + // 3) USB-NCM is an Ethernet netif from lwip perspective, we already have IO definitions for that: + struct esp_netif_netstack_config lwip_netif_config = { + .lwip = { + .init_fn = ethernetif_init, + .input_fn = ethernetif_input + } + }; + + // Config the esp-netif with: + // 1) inherent config (behavioural settings of an interface) + // 2) driver's config (connection to IO functions -- usb) + // 3) stack config (using lwip IO functions -- derive from eth) + esp_netif_config_t cfg = { + .base = &base_cfg, + .driver = &driver_cfg, + .stack = &lwip_netif_config + }; + + s_netif = esp_netif_new(&cfg); + if (s_netif == NULL) { + return ESP_FAIL; + } + esp_netif_set_mac(s_netif, lwip_addr); + + // set the minimum lease time + uint32_t lease_opt = 1; + esp_netif_dhcps_option(s_netif, ESP_NETIF_OP_SET, IP_ADDRESS_LEASE_TIME, &lease_opt, sizeof(lease_opt)); + + // start the interface manually (as the driver has been started already) + esp_netif_action_start(s_netif, 0, 0, 0); + return ESP_OK; +} diff --git a/examples/network/sta_to_eth/main/wired_iface.h b/examples/network/sta_to_eth/main/wired_iface.h new file mode 100644 index 0000000000..d4c3716829 --- /dev/null +++ b/examples/network/sta_to_eth/main/wired_iface.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +typedef esp_err_t (*wired_rx_cb_t)(void *buffer, uint16_t len, void *ctx); + +typedef void (*wired_free_cb_t)(void *buffer, void *ctx); + +typedef enum { + FROM_WIRED, + TO_WIRED +} mac_spoof_direction_t; + +void mac_spoof(mac_spoof_direction_t direction, uint8_t *buffer, uint16_t len, uint8_t own_mac[6]); + +esp_err_t wired_bridge_init(wired_rx_cb_t rx_cb, wired_free_cb_t free_cb); + +esp_err_t wired_send(void *buffer, uint16_t len, void *buff_free_arg); + +esp_err_t wired_netif_init(void); diff --git a/examples/network/sta_to_eth/sdkconfig.defaults b/examples/network/sta_to_eth/sdkconfig.defaults new file mode 100644 index 0000000000..6e41a8774f --- /dev/null +++ b/examples/network/sta_to_eth/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_EXAMPLE_WIRED_INTERFACE_IS_ETHERNET=y diff --git a/examples/network/sta_to_eth/sdkconfig.defaults.esp32s2 b/examples/network/sta_to_eth/sdkconfig.defaults.esp32s2 new file mode 100644 index 0000000000..11a2e1d600 --- /dev/null +++ b/examples/network/sta_to_eth/sdkconfig.defaults.esp32s2 @@ -0,0 +1,4 @@ +# ESP32S2 has USB-OTG, let's prefer virtual Ethernet (USB-NCM device) +CONFIG_EXAMPLE_WIRED_INTERFACE_IS_USB=y +CONFIG_EXAMPLE_WIRED_INTERFACE_IS_ETHERNET=n +CONFIG_TINYUSB_NET_MODE_NCM=y diff --git a/examples/network/sta_to_eth/sdkconfig.defaults.esp32s3 b/examples/network/sta_to_eth/sdkconfig.defaults.esp32s3 new file mode 100644 index 0000000000..e0c0938990 --- /dev/null +++ b/examples/network/sta_to_eth/sdkconfig.defaults.esp32s3 @@ -0,0 +1,11 @@ +# ESP32S3 has USB-OTG, let's prefer virtual Ethernet (USB-NCM device) +CONFIG_EXAMPLE_WIRED_INTERFACE_IS_USB=y +CONFIG_EXAMPLE_WIRED_INTERFACE_IS_ETHERNET=n + +# TinyUSB needs to be initialized and run from one core +# that's why we pin the task to CPU0 and init tusb in the task +# on dual core devices (ESP32S3) +CONFIG_TINYUSB_TASK_AFFINITY_CPU0=y +CONFIG_TINYUSB_INIT_IN_DEFAULT_TASK=y + +CONFIG_TINYUSB_NET_MODE_NCM=y diff --git a/examples/protocols/http_server/captive_portal/components/dns_server/CMakeLists.txt b/examples/protocols/http_server/captive_portal/components/dns_server/CMakeLists.txt new file mode 100644 index 0000000000..423058515c --- /dev/null +++ b/examples/protocols/http_server/captive_portal/components/dns_server/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS dns_server.c + INCLUDE_DIRS include + PRIV_REQUIRES esp_netif) diff --git a/examples/protocols/http_server/captive_portal/main/dns_server.c b/examples/protocols/http_server/captive_portal/components/dns_server/dns_server.c similarity index 73% rename from examples/protocols/http_server/captive_portal/main/dns_server.c rename to examples/protocols/http_server/captive_portal/components/dns_server/dns_server.c index fbe0b74436..13d4098e18 100644 --- a/examples/protocols/http_server/captive_portal/main/dns_server.c +++ b/examples/protocols/http_server/captive_portal/components/dns_server/dns_server.c @@ -1,23 +1,22 @@ -/* Captive Portal 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. -*/ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ #include #include #include "esp_log.h" #include "esp_system.h" +#include "esp_check.h" #include "esp_netif.h" #include "lwip/err.h" #include "lwip/sockets.h" #include "lwip/sys.h" #include "lwip/netdb.h" +#include "dns_server.h" #define DNS_PORT (53) #define DNS_MAX_LEN (256) @@ -57,6 +56,14 @@ typedef struct __attribute__((__packed__)) uint32_t ip_addr; } dns_answer_t; +// DNS server handle +struct dns_server_handle { + bool started; + TaskHandle_t task; + int num_of_entries; + dns_entry_pair_t entry[]; +}; + /* Parse the name from the packet from the DNS name format to a regular .-seperated name returns the pointer to the next part of the packet @@ -90,7 +97,7 @@ static char *parse_dns_name(char *raw_name, char *parsed_name, size_t parsed_nam } // Parses the DNS request and prepares a DNS response with the IP of the softAP -static int parse_dns_request(char *req, size_t req_len, char *dns_reply, size_t dns_reply_max_len) +static int parse_dns_request(char *req, size_t req_len, char *dns_reply, size_t dns_reply_max_len, dns_server_handle_t h) { if (req_len > dns_reply_max_len) { return -1; @@ -126,8 +133,8 @@ static int parse_dns_request(char *req, size_t req_len, char *dns_reply, size_t char *cur_qd_ptr = dns_reply + sizeof(dns_header_t); char name[128]; - // Respond to all questions with the ESP32's IP address - for (int i = 0; i < qd_count; i++) { + // Respond to all questions based on configured rules + for (int qd_i = 0; qd_i < qd_count; qd_i++) { char *name_end_ptr = parse_dns_name(cur_qd_ptr, name, sizeof(name)); if (name_end_ptr == NULL) { ESP_LOGE(TAG, "Failed to parse DNS question: %s", cur_qd_ptr); @@ -141,6 +148,25 @@ static int parse_dns_request(char *req, size_t req_len, char *dns_reply, size_t ESP_LOGD(TAG, "Received type: %d | Class: %d | Question for: %s", qd_type, qd_class, name); if (qd_type == QD_TYPE_A) { + esp_ip4_addr_t ip = { .addr = IPADDR_ANY }; + // Check the configured rules to decide whether to answer this question or not + for (int i = 0; i < h->num_of_entries; ++i) { + // check if the name either corresponds to the entry, or if we should answer to all queries ("*") + if (strcmp(h->entry[i].name, "*") == 0 || strcmp(h->entry[i].name, name) == 0) { + if (h->entry[i].if_key) { + esp_netif_ip_info_t ip_info; + esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey(h->entry[i].if_key), &ip_info); + ip.addr = ip_info.ip.addr; + break; + } else if (h->entry->ip.addr != IPADDR_ANY) { + ip.addr = h->entry[i].ip.addr; + break; + } + } + } + if (ip.addr == IPADDR_ANY) { // no rule applies, continue with another question + continue; + } dns_answer_t *answer = (dns_answer_t *)cur_ans_ptr; answer->ptr_offset = htons(0xC000 | (cur_qd_ptr - dns_reply)); @@ -148,12 +174,10 @@ static int parse_dns_request(char *req, size_t req_len, char *dns_reply, size_t answer->class = htons(qd_class); answer->ttl = htonl(ANS_TTL_SEC); - esp_netif_ip_info_t ip_info; - esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), &ip_info); - ESP_LOGD(TAG, "Answer with PTR offset: 0x%" PRIX16 " and IP 0x%" PRIX32, ntohs(answer->ptr_offset), ip_info.ip.addr); + ESP_LOGD(TAG, "Answer with PTR offset: 0x%" PRIX16 " and IP 0x%" PRIX32, ntohs(answer->ptr_offset), ip.addr); - answer->addr_len = htons(sizeof(ip_info.ip.addr)); - answer->ip_addr = ip_info.ip.addr; + answer->addr_len = htons(sizeof(ip.addr)); + answer->ip_addr = ip.addr; } } return reply_len; @@ -169,8 +193,9 @@ void dns_server_task(void *pvParameters) char addr_str[128]; int addr_family; int ip_protocol; + dns_server_handle_t handle = pvParameters; - while (1) { + while (handle->started) { struct sockaddr_in dest_addr; dest_addr.sin_addr.s_addr = htonl(INADDR_ANY); @@ -193,7 +218,7 @@ void dns_server_task(void *pvParameters) } ESP_LOGI(TAG, "Socket bound, port %d", DNS_PORT); - while (1) { + while (handle->started) { ESP_LOGI(TAG, "Waiting for data"); struct sockaddr_in6 source_addr; // Large enough for both IPv4 or IPv6 socklen_t socklen = sizeof(source_addr); @@ -218,7 +243,7 @@ void dns_server_task(void *pvParameters) rx_buffer[len] = 0; char reply[DNS_MAX_LEN]; - int reply_len = parse_dns_request(rx_buffer, len, reply, DNS_MAX_LEN); + int reply_len = parse_dns_request(rx_buffer, len, reply, DNS_MAX_LEN, handle); ESP_LOGI(TAG, "Received %d bytes from %s | DNS reply with len: %d", len, addr_str, reply_len); if (reply_len <= 0) { @@ -242,7 +267,24 @@ void dns_server_task(void *pvParameters) vTaskDelete(NULL); } -void start_dns_server(void) +dns_server_handle_t start_dns_server(dns_server_config_t *config) { - xTaskCreate(dns_server_task, "dns_server", 4096, NULL, 5, NULL); + dns_server_handle_t handle = calloc(1, sizeof(struct dns_server_handle) + config->num_of_entries * sizeof(dns_entry_pair_t)); + ESP_RETURN_ON_FALSE(handle, NULL, TAG, "Failed to allocate dns server handle"); + + handle->started = true; + handle->num_of_entries = config->num_of_entries; + memcpy(handle->entry, config->item, config->num_of_entries * sizeof(dns_entry_pair_t)); + + xTaskCreate(dns_server_task, "dns_server", 4096, handle, 5, &handle->task); + return handle; +} + +void stop_dns_server(dns_server_handle_t handle) +{ + if (handle) { + handle->started = false; + vTaskDelete(handle->task); + free(handle); + } } diff --git a/examples/protocols/http_server/captive_portal/components/dns_server/include/dns_server.h b/examples/protocols/http_server/captive_portal/components/dns_server/include/dns_server.h new file mode 100644 index 0000000000..ae954e9e94 --- /dev/null +++ b/examples/protocols/http_server/captive_portal/components/dns_server/include/dns_server.h @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef DNS_SERVER_MAX_ITEMS +#define DNS_SERVER_MAX_ITEMS 1 +#endif + +#define DNS_SERVER_CONFIG_SINGLE(queried_name, netif_key) { \ + .num_of_entries = 1, \ + .item = { { .name = queried_name, .if_key = netif_key } } \ + } + +/** + * @brief Definition of one DNS entry: NAME - IP (or the netif whose IP to answer) + * + * @note Please use string literals (or ensure they are valid during dns_server lifetime) as names, since + * we don't take copies of the config values `name` and `if_key` + */ +typedef struct dns_entry_pair { + const char* name; /** * `console` - for debugging via console-based provisioning * The client->device commands are printed to STDOUT and device->client messages are accepted via STDIN. * This is to be used when the device is accepting provisioning commands on UART console. + * `httpd` - the script works the same as for `softap`. This could be used on any other network interface than WiFi soft AP, e.g. Ethernet or USB. * `--service_name ` (Optional) - When transport mode is `ble`, this specifies the BLE device name to which connection is to be established for provisioned. If not provided, BLE scanning is initiated and a list of nearby devices, as seen by the host, is displayed, of which the target device can be chosen. - - When transport mode is `softap`, this specifies the HTTP server hostname / IP which is running the provisioning service, on the SoftAP network of the device which is to be provisioned. This defaults to `192.168.4.1:80` if not specified + - When transport mode is `softap` or `httpd`, this specifies the HTTP server hostname / IP which is running the provisioning service, on the SoftAP network (or any other interface for `httpd` mode) of the device which is to be provisioned. This defaults to `192.168.4.1:80` if not specified * `--ssid ` (Optional) - For specifying the SSID of the Wi-Fi AP to which the device is to connect after provisioning. diff --git a/tools/esp_prov/esp_prov.py b/tools/esp_prov/esp_prov.py index 567ac16719..5b1600f0a0 100644 --- a/tools/esp_prov/esp_prov.py +++ b/tools/esp_prov/esp_prov.py @@ -51,7 +51,7 @@ def get_security(secver, username, password, pop='', verbose=False): async def get_transport(sel_transport, service_name): try: tp = None - if (sel_transport == 'softap'): + if (sel_transport in ['softap', 'httpd']): if service_name is None: service_name = '192.168.4.1:80' tp = transport.Transport_HTTP(service_name) @@ -188,8 +188,8 @@ async def scan_wifi_APs(sel_transport, tp, sec): APs = [] group_channels = 0 readlen = 100 - if sel_transport == 'softap': - # In case of softAP we must perform the scan on individual channels, one by one, + if sel_transport in ['softap', 'httpd']: + # In case of softAP/httpd we must perform the scan on individual channels, one by one, # so that the Wi-Fi controller gets ample time to send out beacons (necessary to # maintain connectivity with authenticated stations. As scanning one channel at a # time will be slow, we can group more than one channels to be scanned in quick @@ -329,14 +329,14 @@ async def main(): parser.add_argument('--transport', required=True, dest='mode', type=str, help=desc_format( 'Mode of transport over which provisioning is to be performed.', - 'This should be one of "softap", "ble" or "console"')) + 'This should be one of "softap", "ble", "console" (or "httpd" which is an alias of softap)')) parser.add_argument('--service_name', dest='name', type=str, help=desc_format( 'This specifies the name of the provisioning service to connect to, ' 'depending upon the mode of transport :', - '\t- transport "ble" : The BLE Device Name', - '\t- transport "softap" : HTTP Server hostname or IP', + '\t- transport "ble" : The BLE Device Name', + '\t- transport "softap/httpd" : HTTP Server hostname or IP', '\t (default "192.168.4.1:80")')) parser.add_argument('--proto_ver', dest='version', type=str, default='',