From 75e0d6273904336515c555677eeff34746b11854 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Mon, 22 Sep 2025 14:29:49 +0200 Subject: [PATCH] feat(lwip/dhcps): Add support for reporting clients hostname --- .../esp_netif/include/esp_netif_types.h | 22 ++++- components/esp_netif/lwip/esp_netif_lwip.c | 12 +++ components/lwip/Kconfig | 19 ++++ components/lwip/apps/dhcpserver/dhcpserver.c | 86 ++++++++++++++++++- .../lwip/include/apps/dhcpserver/dhcpserver.h | 25 +++++- examples/wifi/softap_sta/main/softap_sta.c | 16 +++- 6 files changed, 173 insertions(+), 7 deletions(-) diff --git a/components/esp_netif/include/esp_netif_types.h b/components/esp_netif/include/esp_netif_types.h index 80ef593825..a60455887a 100644 --- a/components/esp_netif/include/esp_netif_types.h +++ b/components/esp_netif/include/esp_netif_types.h @@ -152,11 +152,31 @@ typedef struct { bool preferred; /*!< The default preference of the address */ } ip_event_add_ip6_t; -/** Event structure for IP_EVENT_ASSIGNED_IP_TO_CLIENT event */ +/** Event structure for IP_EVENT_ASSIGNED_IP_TO_CLIENT event + * + * This event is posted when a local DHCP server (e.g., SoftAP) assigns an IPv4 + * address to a client. The structure carries the assigned IPv4 address and the + * client's MAC address. If DHCP server support is disabled via Kconfig + * (CONFIG_LWIP_DHCPS=n), the \c ip field is not populated. + * + * If enabled by Kconfig (CONFIG_LWIP_DHCPS_REPORT_CLIENT_HOSTNAME=y), the + * optional DHCP client hostname (option 12) is also included in the \c hostname + * field. The hostname is a null-terminated UTF-8 string and may be empty if the + * client did not provide one. Its maximum length (including the terminator) is + * bounded by CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN; longer hostnames are truncated. + * The value is sanitized to contain only letters, digits, dot, dash, and + * underscore. Applications should treat this field as untrusted input. + */ typedef struct { esp_netif_t *esp_netif; /*!< Pointer to the associated netif handle */ esp_ip4_addr_t ip; /*!< IP address which was assigned to the station */ uint8_t mac[6]; /*!< MAC address of the connected client */ + /* Client hostname as provided via DHCP option 12 (if available). */ +#ifndef CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN +#define CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN 64 +#endif +#define ESP_NETIF_HOSTNAME_MAX_LEN CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN + char hostname[ESP_NETIF_HOSTNAME_MAX_LEN]; /*!< Optional DHCP client hostname (may be empty string) */ } ip_event_assigned_ip_to_client_t; /** Compatibility event structure for ip_event_ap_staipassigned_t event diff --git a/components/esp_netif/lwip/esp_netif_lwip.c b/components/esp_netif/lwip/esp_netif_lwip.c index 49e87649c0..0ce0e1b8fe 100644 --- a/components/esp_netif/lwip/esp_netif_lwip.c +++ b/components/esp_netif/lwip/esp_netif_lwip.c @@ -1129,6 +1129,18 @@ static void esp_netif_dhcps_cb(void* arg, uint8_t ip[4], uint8_t mac[6]) ESP_LOGI(TAG, "DHCP server assigned IP to a client, IP is: " IPSTR, IP2STR(&evt.ip)); ESP_LOGD(TAG, "Client's MAC: %x:%x:%x:%x:%x:%x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +#if CONFIG_LWIP_DHCPS_REPORT_CLIENT_HOSTNAME + /* Try to fetch hostname for this MAC if available */ + if (esp_netif && esp_netif->dhcps) { + /* Ensure zero-terminated even if not found */ + if (!dhcps_get_hostname_on_mac(esp_netif->dhcps, mac, evt.hostname, sizeof(evt.hostname))) { + if (sizeof(evt.hostname) > 0) { + evt.hostname[0] = '\0'; + } + } + } +#endif + int ret = esp_event_post(IP_EVENT, IP_EVENT_ASSIGNED_IP_TO_CLIENT, &evt, sizeof(evt), 0); if (ESP_OK != ret) { ESP_LOGE(TAG, "dhcps cb: failed to post IP_EVENT_ASSIGNED_IP_TO_CLIENT (%x)", ret); diff --git a/components/lwip/Kconfig b/components/lwip/Kconfig index e9873d33bd..94f54f23e5 100644 --- a/components/lwip/Kconfig +++ b/components/lwip/Kconfig @@ -405,6 +405,15 @@ menu "LWIP" Enabling this option allows the device to run the DHCP server (to dynamically assign IPv4 addresses to clients). + config LWIP_DHCPS_REPORT_CLIENT_HOSTNAME + bool "DHCPS: Report client hostname in assigned-IP event" + default y + depends on LWIP_DHCPS + help + When enabled, the DHCP server parses client hostname (DHCP option 12) + and makes it available via IP_EVENT_ASSIGNED_IP_TO_CLIENT. This adds + a small amount of RAM usage per lease to store the hostname. + config LWIP_DHCPS_LEASE_UNIT int "Multiplier for lease time, in seconds" range 1 3600 @@ -425,6 +434,16 @@ menu "LWIP" After this number is exceeded, DHCP server removes of the oldest device from it's address pool, without notification. + config LWIP_DHCPS_MAX_HOSTNAME_LEN + int "Maximum client hostname length stored by DHCPS" + range 1 255 + default 64 + depends on LWIP_DHCPS_REPORT_CLIENT_HOSTNAME + help + Maximum number of bytes stored per-client for DHCP option 12 (hostname), + including the terminating null when used in esp-netif events. Longer hostnames + will be truncated. + config LWIP_DHCPS_STATIC_ENTRIES bool "Enable ARP static entries" default y diff --git a/components/lwip/apps/dhcpserver/dhcpserver.c b/components/lwip/apps/dhcpserver/dhcpserver.c index 30a43858ca..727bc5ec35 100644 --- a/components/lwip/apps/dhcpserver/dhcpserver.c +++ b/components/lwip/apps/dhcpserver/dhcpserver.c @@ -50,6 +50,7 @@ #define DHCPRELEASE 7 #define DHCP_OPTION_SUBNET_MASK 1 +#define DHCP_OPTION_HOST_NAME 12 #define DHCP_OPTION_ROUTER 3 #define DHCP_OPTION_DNS_SERVER 6 #define DHCP_OPTION_REQ_IPADDR 50 @@ -142,6 +143,11 @@ struct dhcps_t { struct udp_pcb *dhcps_pcb; dhcps_handle_state state; bool has_declined_ip; +#if CONFIG_LWIP_DHCPS_REPORT_CLIENT_HOSTNAME + /* Temporary storage for option 12 parsed from the current packet */ + char opt_hostname[CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN]; + bool opt_hostname_present; +#endif }; @@ -923,6 +929,33 @@ static u8_t parse_options(dhcps_t *dhcps, u8_t *optptr, s16_t len) type = *(optptr + 2); break; +#if CONFIG_LWIP_DHCPS_REPORT_CLIENT_HOSTNAME + case DHCP_OPTION_HOST_NAME: { + /* option format: code(1) len(1) value(len) */ + u8_t olen = *(optptr + 1); + const u8_t *oval = optptr + 2; + if (olen > 0) { + /* clamp to configured max, keep room for NUL */ + size_t copy_len = olen; + if (copy_len >= CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN) { + copy_len = CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN - 1; + } + size_t j = 0; + for (; j < copy_len; ++j) { + char c = (char)oval[j]; + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '_') { + dhcps->opt_hostname[j] = c; + } else { + dhcps->opt_hostname[j] = '-'; + } + } + dhcps->opt_hostname[j] = '\0'; + dhcps->opt_hostname_present = true; + } + break; + } +#endif + case DHCP_OPTION_REQ_IPADDR://50 if (memcmp((char *) &client.addr, (char *) optptr + 2, 4) == 0) { #if DHCPS_DEBUG @@ -1111,7 +1144,7 @@ POOL_CHECK: return 4; } - s16_t ret = parse_options(dhcps, &m->options[4], len);; + s16_t ret = parse_options(dhcps, &m->options[4], len); if (ret == DHCPS_STATE_RELEASE || ret == DHCPS_STATE_NAK || ret == DHCPS_STATE_DECLINE) { if (pnode != NULL) { @@ -1131,6 +1164,21 @@ POOL_CHECK: memset(&dhcps->client_address, 0x0, sizeof(dhcps->client_address)); } +#if CONFIG_LWIP_DHCPS_REPORT_CLIENT_HOSTNAME + /* If we parsed a hostname from options and we have a lease entry, store it */ + if (pdhcps_pool != NULL) { + if (dhcps->opt_hostname_present) { + size_t n = strnlen(dhcps->opt_hostname, CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN); + if (n > 0) { + memset(pdhcps_pool->hostname, 0, sizeof(pdhcps_pool->hostname)); + memcpy(pdhcps_pool->hostname, dhcps->opt_hostname, n); + } else { + pdhcps_pool->hostname[0] = '\0'; + } + } + } +#endif + #if DHCPS_DEBUG DHCPS_LOG("dhcps: xid changed\n"); DHCPS_LOG("dhcps: client_address.addr = %x\n", dhcps->client_address.addr); @@ -1619,4 +1667,40 @@ err_t dhcps_dns_getserver(dhcps_t *dhcps, ip4_addr_t *dnsserver) return dhcps_dns_getserver_by_type(dhcps, dnsserver, DNS_TYPE_MAIN); } +#if CONFIG_LWIP_DHCPS_REPORT_CLIENT_HOSTNAME +bool dhcps_get_hostname_on_mac(dhcps_t *dhcps, const u8_t *mac, char *out, size_t out_len) +{ + if ((dhcps == NULL) || (mac == NULL) || (out == NULL) || (out_len == 0)) { + return false; + } + list_node *pnode = dhcps->plist; + while (pnode) { + struct dhcps_pool *pool = pnode->pnode; + if (memcmp(pool->mac, mac, sizeof(pool->mac)) == 0) { + size_t maxcpy = (out_len > 0) ? out_len - 1 : 0; + size_t srclen = 0; + /* hostname may be empty string */ + srclen = strnlen(pool->hostname, CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN); + if (maxcpy > 0) { + if (srclen > maxcpy) srclen = maxcpy; + if (srclen) memcpy(out, pool->hostname, srclen); + out[srclen] = '\0'; + } + return true; + } + pnode = pnode->pnext; + } + return false; +} +#else +bool dhcps_get_hostname_on_mac(dhcps_t *dhcps, const u8_t *mac, char *out, size_t out_len) +{ + LWIP_UNUSED_ARG(dhcps); + LWIP_UNUSED_ARG(mac); + if (out && out_len) { + out[0] = '\0'; + } + return false; +} +#endif #endif // ESP_DHCPS diff --git a/components/lwip/include/apps/dhcpserver/dhcpserver.h b/components/lwip/include/apps/dhcpserver/dhcpserver.h index 975f00c73f..370bae099c 100644 --- a/components/lwip/include/apps/dhcpserver/dhcpserver.h +++ b/components/lwip/include/apps/dhcpserver/dhcpserver.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -8,6 +8,7 @@ #include "sdkconfig.h" #include +#include #include "lwip/ip_addr.h" #include "lwip/err.h" @@ -75,9 +76,15 @@ typedef enum #define DHCPS_LEASE_UNIT CONFIG_LWIP_DHCPS_LEASE_UNIT struct dhcps_pool{ - ip4_addr_t ip; - u8_t mac[6]; - u32_t lease_timer; + ip4_addr_t ip; + u8_t mac[6]; + u32_t lease_timer; + /* Optional: client's hostname from DHCP option 12, if provided */ +#if CONFIG_LWIP_DHCPS +#if CONFIG_LWIP_DHCPS_REPORT_CLIENT_HOSTNAME + char hostname[CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN]; +#endif +#endif }; typedef u32_t dhcps_time_t; @@ -167,6 +174,16 @@ err_t dhcps_set_option_info(dhcps_t *dhcps, u8_t op_id, void *opt_info, u32_t op */ bool dhcp_search_ip_on_mac(dhcps_t *dhcps, u8_t *mac, ip4_addr_t *ip); +/** + * @brief Tries to find client hostname corresponding to the supplied MAC + * @param dhcps Pointer to the DHCP handle + * @param mac Supplied MAC address + * @param out Output buffer to receive the hostname (null-terminated) + * @param out_len Size of the output buffer + * @return True if the hostname has been found and copied (may be empty string if not provided by client) + */ +bool dhcps_get_hostname_on_mac(dhcps_t *dhcps, const u8_t *mac, char *out, size_t out_len); + /** * @brief Sets the DNS server address for the DHCP server * @param dhcps Pointer to the DHCP handle diff --git a/examples/wifi/softap_sta/main/softap_sta.c b/examples/wifi/softap_sta/main/softap_sta.c index c6c5b5f688..2b29bcce40 100644 --- a/examples/wifi/softap_sta/main/softap_sta.c +++ b/examples/wifi/softap_sta/main/softap_sta.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -103,6 +103,15 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, ESP_LOGI(TAG_STA, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip)); s_retry_num = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) { + const ip_event_assigned_ip_to_client_t *e = (const ip_event_assigned_ip_to_client_t *)event_data; +#if CONFIG_LWIP_DHCPS_REPORT_CLIENT_HOSTNAME + ESP_LOGI(TAG_AP, "Assigned IP to client: " IPSTR ", MAC=" MACSTR ", hostname='%s'", + IP2STR(&e->ip), MAC2STR(e->mac), e->hostname); +#else + ESP_LOGI(TAG_AP, "Assigned IP to client: " IPSTR ", MAC=" MACSTR, + IP2STR(&e->ip), MAC2STR(e->mac)); +#endif } } @@ -203,6 +212,11 @@ void app_main(void) &wifi_event_handler, NULL, NULL)); + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, + IP_EVENT_ASSIGNED_IP_TO_CLIENT, + &wifi_event_handler, + NULL, + NULL)); /*Initialize WiFi */ wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();