[ethernet] Fix IPv6 SLAAC: link-local on link-up, pass all-nodes multicast (#19752)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
David Coulson
2026-10-02 13:46:16 -05:00
committed by GitHub
co-authored by Claude Opus 5.5 J. Nick Koston
parent a5178a0f3d
commit de0a0b2af4
3 changed files with 70 additions and 7 deletions
@@ -220,6 +220,9 @@ class EthernetComponent final : public Component {
protected:
void start_connect_();
void finish_connect_();
#if LWIP_IPV6
esp_err_t ensure_ip6_linklocal_();
#endif
void dump_connect_params_();
#ifdef USE_ESP32
@@ -10,6 +10,10 @@
#include <lwip/dns.h>
#include <cinttypes>
#include "esp_event.h"
#if USE_NETWORK_IPV6
#include <esp_netif_net_stack.h>
#include <lwip/netif.h>
#endif
#ifdef USE_PSRAM
#include <esp_psram.h>
#endif
@@ -509,6 +513,18 @@ void EthernetComponent::ethernet_lazy_init_() {
}
}
#endif
#if USE_NETWORK_IPV6 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
// Since IDF 5.5 the internal EMAC drops multicast groups that were never added (before,
// it passed all multicast), and lwIP never adds all-nodes, so router advertisements
// were lost and SLAAC never ran.
{
uint8_t all_nodes[6] = {0x33, 0x33, 0x00, 0x00, 0x00, 0x01};
if (esp_err_t filter_err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_ADD_MAC_FILTER, all_nodes);
filter_err != ESP_OK) {
ESP_LOGD(TAG, "IPv6 all-nodes multicast filter not added: %s", esp_err_to_name(filter_err));
}
}
#endif
// Register user defined event handers
err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr);
@@ -755,6 +771,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
global_eth_component->notify_ip_state_listeners_();
}
#endif
#if USE_NETWORK_IPV6
// Start SLAAC on link-up, not after the DHCPv4 lease. This also restores the
// link-local after a link flap, which clears the IPv6 addresses.
if (esp_err_t ll_err = esp_netif_create_ip6_linklocal(global_eth_component->eth_netif_); ll_err != ESP_OK) {
ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed on link-up: %s", esp_err_to_name(ll_err));
}
#endif /* USE_NETWORK_IPV6 */
break;
case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected";
@@ -791,7 +814,10 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
void *event_data) {
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip));
global_eth_component->ipv6_count_ += 1;
// Count the addresses on the interface, not the events: recreating the link-local
// after a link flap fires another event for the same address.
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
global_eth_component->ipv6_count_ = esp_netif_get_all_ip6(global_eth_component->eth_netif_, if_ip6s);
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
@@ -806,6 +832,29 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
}
#endif /* USE_NETWORK_IPV6 */
#if USE_NETWORK_IPV6
// Create the link-local address unless the interface already has one, including one still in
// duplicate address detection: recreating it would restart DAD. esp_netif_get_ip6_linklocal()
// only reports a preferred address, so ask lwIP for the slot state instead.
esp_err_t EthernetComponent::ensure_ip6_linklocal_() {
if (auto *netif = static_cast<struct netif *>(esp_netif_get_netif_impl(this->eth_netif_)); netif != nullptr) {
u8_t state;
{
LwIPLock lock;
state = netif_ip6_addr_state(netif, 0);
}
if (ip6_addr_istentative(state) || ip6_addr_isvalid(state)) {
return ESP_OK;
}
}
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err == ESP_OK) {
ESP_LOGD(TAG, "IPv6 link-local address created");
}
return err;
}
#endif /* USE_NETWORK_IPV6 */
void EthernetComponent::finish_connect_() {
#if USE_NETWORK_IPV6
// Retry IPv6 link-local setup if it failed during initial connect
@@ -816,10 +865,7 @@ void EthernetComponent::finish_connect_() {
// - Cable unplugged/network interruption (#10705)
// We can now retry since we're in CONNECTED state and the interface is definitely up.
if (!this->ipv6_setup_done_) {
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err == ESP_OK) {
ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)");
}
this->ensure_ip6_linklocal_();
// Always set the flag to prevent continuous retries
// If IPv6 setup fails here with the interface up and stable, it's
// likely a persistent issue (IPv6 disabled at router, hardware
@@ -833,7 +879,9 @@ void EthernetComponent::finish_connect_() {
void EthernetComponent::start_connect_() {
global_eth_component->got_ipv4_address_ = false;
#if USE_NETWORK_IPV6
global_eth_component->ipv6_count_ = 0;
// Recount rather than zero: addresses that survive a reconnect are not announced again.
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
global_eth_component->ipv6_count_ = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
this->ipv6_setup_done_ = false;
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
@@ -912,7 +960,7 @@ void EthernetComponent::start_connect_() {
// - At bootup when link isn't ready (#10281)
// - After disconnection/cable unplugged (#10705)
// We'll retry in finish_connect_() if it fails here.
err = esp_netif_create_ip6_linklocal(this->eth_netif_);
err = this->ensure_ip6_linklocal_();
if (err != ESP_OK) {
if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) {
// This is a programming error, not a transient failure
@@ -0,0 +1,12 @@
network:
enable_ipv6: true
ethernet:
type: IP101
mdc_pin: 23
mdio_pin: 18
clk:
pin: 0
mode: CLK_EXT_IN
phy_addr: 1
power_pin: 5