fix(lwip): Clarify and test DHCP conflict detection

This commit is contained in:
David Cermak
2025-09-18 14:00:04 +02:00
parent 8939bdad63
commit 0586066d1b
3 changed files with 105 additions and 1 deletions
+83 -1
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -24,6 +24,11 @@
#include "dhcpserver/dhcpserver.h"
#include "dhcpserver/dhcpserver_options.h"
#include "esp_sntp.h"
#include "lwip/dhcp.h"
#include "lwip/acd.h"
#include "lwip/prot/dhcp.h"
#include "lwip/prot/acd.h"
#include "lwip/prot/etharp.h"
#define ETH_PING_END_BIT BIT(1)
#define ETH_PING_DURATION_MS (5000)
@@ -278,6 +283,82 @@ static void dhcps_test_dns_options(dns_callback_type_t cb_type,
TEST_ASSERT((api.ret_start == ERR_OK) == pass);
}
typedef struct {
EventGroupHandle_t event;
int self_mac_cb_calls;
int other_mac_cb_calls;
} acd_test_ctx_t;
static acd_test_ctx_t g_acd_ctx;
static void acd_test_cb(struct netif *netif, acd_callback_enum_t state)
{
(void)netif;
(void)state;
/* We only need to know that a callback was triggered (decline/restart). */
g_acd_ctx.other_mac_cb_calls++;
}
static void dhcp_acd_arp_check_api(void *arg)
{
acd_test_ctx_t *ctx = (acd_test_ctx_t *)arg;
struct netif *netif = NULL;
NETIF_FOREACH(netif) {
if (netif->name[0] == 'l' && netif->name[1] == 'o') {
break;
}
}
TEST_ASSERT_NOT_NULL(netif);
/* Set our interface MAC to a known value */
const struct eth_addr self = { .addr = { 0x08, 0x3a, 0x8d, 0x41, 0x13, 0x14 } };
netif->hwaddr_len = ETH_HWADDR_LEN;
SMEMCPY(netif->hwaddr, self.addr, ETH_HWADDR_LEN);
/* Attach a DHCP client struct with CHECKING state and PROBING ACD state */
static struct dhcp dhcp;
memset(&dhcp, 0, sizeof(dhcp));
netif_set_client_data(netif, LWIP_NETIF_CLIENT_DATA_INDEX_DHCP, &dhcp);
IP4_ADDR(&dhcp.offered_ip_addr, 192, 168, 88, 4);
dhcp.state = DHCP_STATE_CHECKING;
dhcp.acd.state = ACD_STATE_PROBING;
dhcp.acd.acd_conflict_callback = acd_test_cb;
/* Case 1: ARP reply from our offered IP but with our own MAC -> NO conflict */
struct etharp_hdr hdr = {0};
hdr.opcode = PP_HTONS(ARP_REPLY);
IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T(&hdr.sipaddr, &dhcp.offered_ip_addr);
SMEMCPY(hdr.shwaddr.addr, self.addr, ETH_HWADDR_LEN);
ctx->self_mac_cb_calls = 0;
g_acd_ctx.other_mac_cb_calls = 0;
acd_arp_reply(netif, &hdr);
/* No callback should be invoked for self-MAC */
TEST_ASSERT_EQUAL_INT(0, g_acd_ctx.other_mac_cb_calls);
/* Case 2: ARP reply from offered IP with a different MAC -> conflict expected */
const struct eth_addr other = { .addr = { 0x08, 0x3a, 0x8d, 0x41, 0x13, 0x15 } };
SMEMCPY(hdr.shwaddr.addr, other.addr, ETH_HWADDR_LEN);
g_acd_ctx.other_mac_cb_calls = 0;
acd_arp_reply(netif, &hdr);
/* Our callback should be called (DECLINE/RESTART) at least once */
TEST_ASSERT(g_acd_ctx.other_mac_cb_calls > 0);
xEventGroupSetBits(ctx->event, 1);
}
TEST(lwip, dhcp_arp_probe_self_mac_is_ok)
{
test_case_uses_tcpip();
g_acd_ctx.event = xEventGroupCreate();
TEST_ASSERT_NOT_NULL(g_acd_ctx.event);
g_acd_ctx.self_mac_cb_calls = 0;
g_acd_ctx.other_mac_cb_calls = 0;
tcpip_callback(dhcp_acd_arp_check_api, &g_acd_ctx);
xEventGroupWaitBits(g_acd_ctx.event, 1, true, true, pdMS_TO_TICKS(5000));
vEventGroupDelete(g_acd_ctx.event);
}
TEST(lwip, dhcp_server_dns_options)
{
test_case_uses_tcpip();
@@ -410,6 +491,7 @@ TEST_GROUP_RUNNER(lwip)
RUN_TEST_CASE(lwip, dhcp_server_dns_options)
RUN_TEST_CASE(lwip, sntp_client_time_2015)
RUN_TEST_CASE(lwip, sntp_client_time_2048)
RUN_TEST_CASE(lwip, dhcp_arp_probe_self_mac_is_ok)
}
void app_main(void)
+11
View File
@@ -38,6 +38,17 @@ Some common lwIP app APIs are supported indirectly by ESP-IDF:
- mDNS uses a different implementation to the lwIP default mDNS, see :doc:`/api-reference/protocols/mdns`. But lwIP can look up mDNS hosts using standard APIs such as ``gethostbyname()`` and the convention ``hostname.local``, provided the :ref:`CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES` setting is enabled.
- The PPP implementation in lwIP can be used to create PPPoS (PPP over serial) interface in ESP-IDF. Please refer to the documentation of the :doc:`/api-reference/network/esp_netif` component to create and configure a PPP network interface, by means of the ``ESP_NETIF_DEFAULT_PPP()`` macro defined in :component_file:`esp_netif/include/esp_netif_defaults.h`. Additional runtime settings are provided via :component_file:`esp_netif/include/esp_netif_ppp.h`. PPPoS interfaces are typically used to interact with NBIoT/GSM/LTE modems. More application-level friendly API is supported by the `esp_modem <https://components.espressif.com/component/espressif/esp_modem>`_ library, which uses this PPP lwIP module behind the scenes.
DHCP Address Conflict Detection
-------------------------------
Usually, DHCP servers verify that a selected IPv4 address is unique on the network before offering it. However, some, including the DHCP server in ESP-IDF, do not perform verification by default to simplify operation and speed up address assignment.
If you want to avoid any possible IP address conflicts and connectivity issues, the ESP-IDF DHCP client provides several options to validate the offered IPv4 address before binding it (see :ref:`CONFIG_LWIP_DHCP_CHECKS_OFFERED_ADDRESS`):
- **Simple ARP check, default option** (``CONFIG_LWIP_DHCP_DOES_ARP_CHECK``): Sends two ARP probes and only declines the offer if a reply for the offered IP comes from a different MAC address than the interface MAC. This is fast (about 12 seconds) and avoids false conflicts on networks where the AP echoes the client's MAC in ARP replies. **Use this option if you encounter DHCP DECLINE loops where the ARP reply for the offered IP advertises the interface's own MAC.**
- **Address Conflict Detection (ACD)** (``CONFIG_LWIP_DHCP_DOES_ACD_CHECK``): Uses upstream lwIP ACD per RFC 5227 to probe/announce addresses. Some access points respond to ARP probes with the client's own MAC for the offered IP; upstream behavior treats any matching sender IP during PROBING as a conflict, which can cause repeated DHCP DECLINEs on such networks. **Use this option only if you need full RFC 5227 compliance and are aware of the potential issues with certain access points.**
- **No conflict detection** (``CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP``): Binds the address without additional checks. **Use this option for maximum compatibility when conflict detection is not required.**
BSD Sockets API
---------------
+11
View File
@@ -38,6 +38,17 @@ ESP-IDF 间接支持以下常见的 lwIP 应用程序 API:
- mDNS 与 lwIP 的默认 mDNS 使用不同实现方式,请参阅 :doc:`/api-reference/protocols/mdns`。但启用 :ref:`CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES` 设置项后,lwIP 可以使用 ``gethostbyname()`` 等标准 API 和 ``hostname.local`` 约定查找 mDNS 主机。
- lwIP 中的 PPP 实现可用于在 ESP-IDF 中创建 PPPoS(串行 PPP)接口。请参阅 :doc:`/api-reference/network/esp_netif` 组件文档,使用 :component_file:`esp_netif/include/esp_netif_defaults.h` 中定义的 ``ESP_NETIF_DEFAULT_PPP()`` 宏创建并配置 PPP 网络接口。:component_file:`esp_netif/include/esp_netif_ppp.h` 中提供了其他的运行时设置。PPPoS 接口通常用于与 NBIoT/GSM/LTE 调制解调器交互。`esp_modem <https://components.espressif.com/component/espressif/esp_modem>`_ 仓库还支持更多应用层友好的 API,该仓库内部使用了上述 PPP lwIP 模块。
DHCP 地址冲突检测
-------------------------------
通常情况下,DHCP 服务器在提供选定的 IPv4 地址前,会验证该地址在网络上的唯一性。然而,有些服务器(包括 ESP-IDF 中的 DHCP 服务器)为了简化操作并加快地址分配速度,默认情况下不会进行这种验证。
为避免潜在的 IP 地址冲突与连接问题,ESP-IDF 的 DHCP 客户端提供了多种选项,可在绑定 IPv4 地址前对服务器分配的地址进行验证(参见 :ref:`CONFIG_LWIP_DHCP_CHECKS_OFFERED_ADDRESS`):
- **简单 ARP 检测,默认选项** (``CONFIG_LWIP_DHCP_DOES_ARP_CHECK``):发送两个 ARP 探测,只有当分配的 IP 的回复来自与接口 MAC 不同的 MAC 地址时才拒绝该分配。这种方式速度快约 1-2 秒,可避免 AP 在 ARP 回复中回显客户端 MAC 的网络中出现误判冲突。**如果遇到 DHCP DECLINE 循环,即分配 IP 的 ARP 回复显示接口自身的 MAC 时,请使用此选项。**
- **地址冲突检测 (ACD)** (``CONFIG_LWIP_DHCP_DOES_ACD_CHECK``):使用上游 lwIP ACD (符合 RFC 5227 标准)来探测或宣告地址。某些接入点会使用客户端自身的 MAC 地址来响应 ARP 探测;上游行为会将探测期间收到的任何匹配发送方 IP 视为冲突,这可能导致在此类网络上反复出现 DHCP DECLINE。**只有在需要完全符合 RFC 5227 标准,并了解某些接入点的潜在问题时,才使用此选项。**
- **无冲突检测** (``CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP``):不进行额外检查直接绑定地址。**当不需要冲突检测时,使用此选项可获得最大兼容性。**
BSD 套接字 API
-----------------