Skip to content

Commit ba1dbef

Browse files
committed
revert(mqtt): remove IPv6 dual-stack support — measured heap exhaustion
Reverts merge ae04539 (feat/flex-ipv6, PR #25). Enabling IPv6 joins the device into IPv6 multicast/ND processing; on multicast-heavy LANs (e.g. with a Thread/Matter border router advertising a ULA prefix) inbound bursts land in dynamic WiFi RX buffers in internal heap. Measured on a Station G2 with 4 WSS brokers: min-free floor dropped 36 KB -> 18 KB, largest free block pinned below the 16 KB publish threshold for minutes at a time, 523 dropped publishes in 15 minutes. With IPv6 disabled the floor and max-alloc recovered and publish skips stopped. The feature only fed the wifi.status display line — no transport uses IPv6 (all brokers connect over IPv4), so the fleet risk (network- dependent degradation on unknown home LANs) buys nothing. Revisit after the Arduino core 3.x / IDF 5.x move if IPv6 transport is ever needed; the branch remains at feat/flex-ipv6. Kept from the merge: the wifi.status uptime append now computes the actual remaining space in the 160-byte reply buffer instead of assuming a hardcoded 128, fixing a latent overflow of the snprintf bound.
1 parent ae04539 commit ba1dbef

4 files changed

Lines changed: 5 additions & 86 deletions

File tree

MQTT_IMPLEMENTATION.md

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -249,26 +249,6 @@ The MQTT bridge comes with the following defaults for fresh installs (unless ove
249249
- **Timezone Offset**: 0 (fallback, no offset, unless `MQTT_DEFAULT_TIMEZONE_OFFSET` is set)
250250
- **Repeat (forwarding)**: On (set `repeat off` for receive-only observers)
251251

252-
## IPv6 Support
253-
254-
Observer builds run **dual-stack**: IPv4 continues to work exactly as before, and the node
255-
*additionally* acquires an IPv6 address when the network supports it. This is fully automatic
256-
and requires no configuration.
257-
258-
- **How it works**: once WiFi has an IPv4 address, the node enables IPv6 and obtains a global
259-
address via Router Advertisement / SLAAC. A dual-stack router with RA/SLAAC is required;
260-
on IPv4-only networks the node simply stays IPv4-only (graceful degradation).
261-
- **Visibility**: the global IPv6 address appears in `get wifi.status` once assigned, e.g.
262-
`> connected, IP: 192.168.1.42, IPv6: 2001:db8::abcd, RSSI: -62 dBm, uptime: ...`.
263-
The field is omitted when no global address is present (link-local is not reported). IPv6 is
264-
CLI/serial-visible only — it is not shown on the OLED.
265-
- **Custom broker over IPv6**: use a bracketed literal in a full URI
266-
(`set mqttN.server mqtts://[2001:db8::1]:8883`), or a bare literal
267-
(`set mqttN.server 2001:db8::1`) which is bracketed automatically. Hostname presets need no
268-
changes — DNS resolves AAAA records automatically once the stack is dual-stack.
269-
- **Cost**: none to budget. IPv6 is already compiled into the ESP32 Arduino lwIP that every
270-
build links; enabling it at runtime only adds a couple of address slots.
271-
272252
## CLI Commands
273253

274254
### MQTT Slot Commands

src/helpers/CommonCLI_Observer.cpp

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -780,26 +780,7 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
780780
default: status_str = "unknown"; break;
781781
}
782782
if (status == WL_CONNECTED) {
783-
// reply points at the caller's char[160] command buffer (see main.cpp).
784-
const size_t kReplyBufSize = 160;
785-
sprintf(reply, "> %s, IP: %s", status_str, WiFi.localIP().toString().c_str());
786-
#ifdef WITH_MQTT_BRIDGE
787-
// Group IPv6 directly after IPv4 when a global/ULA address is assigned.
788-
char v6[46];
789-
if (MQTTBridge::getGlobalIPv6(v6, sizeof(v6))) {
790-
size_t v6_len = strlen(reply);
791-
if (v6_len < kReplyBufSize) {
792-
snprintf(reply + v6_len, kReplyBufSize - v6_len, ", IPv6: %s", v6);
793-
}
794-
}
795-
#endif
796-
// RSSI right after the IP addresses.
797-
{
798-
size_t rssi_len = strlen(reply);
799-
if (rssi_len < kReplyBufSize) {
800-
snprintf(reply + rssi_len, kReplyBufSize - rssi_len, ", RSSI: %d dBm", WiFi.RSSI());
801-
}
802-
}
783+
sprintf(reply, "> %s, IP: %s, RSSI: %d dBm", status_str, WiFi.localIP().toString().c_str(), WiFi.RSSI());
803784
#ifdef WITH_MQTT_BRIDGE
804785
unsigned long connect_at = MQTTBridge::getWifiConnectedAtMillis();
805786
if (connect_at != 0) {
@@ -809,6 +790,9 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
809790
unsigned long h = (uptime_sec % 86400) / 3600;
810791
unsigned long m = (uptime_sec % 3600) / 60;
811792
unsigned long s = uptime_sec % 60;
793+
// reply points at the caller's char[160] command buffer (see main.cpp);
794+
// compute the actual remaining space instead of assuming 128.
795+
const size_t kReplyBufSize = 160;
812796
size_t len = strlen(reply);
813797
const size_t reply_remaining = (len < kReplyBufSize) ? (kReplyBufSize - len) : 0;
814798
if (d > 0) {

src/helpers/bridges/MQTTBridge.cpp

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
#ifdef ESP_PLATFORM
1717
#include <esp_wifi.h>
18-
#include <esp_netif.h>
1918
#include <esp_heap_caps.h>
2019
#include <freertos/FreeRTOS.h>
2120
#include <freertos/task.h>
@@ -180,10 +179,6 @@ static unsigned long s_wifi_connected_at = 0;
180179
static uint8_t s_wifi_disconnect_reason = 0;
181180
static unsigned long s_wifi_disconnect_time = 0;
182181

183-
// Most recent global/unique-local IPv6 address (SLAAC), as a string; empty when none.
184-
// Populated from the ARDUINO_EVENT_WIFI_STA_GOT_IP6 handler; cleared on WiFi disconnect.
185-
static char s_global_ipv6[46] = "";
186-
187182
#ifdef MQTT_MEMORY_DEBUG
188183
// #region agent log
189184
static void agentLogHeap(const char* location, const char* message, const char* hypothesisId,
@@ -206,17 +201,6 @@ unsigned long MQTTBridge::getWifiConnectedAtMillis() {
206201
return s_wifi_connected_at;
207202
}
208203

209-
bool MQTTBridge::getGlobalIPv6(char* buf, size_t len) {
210-
if (buf == nullptr || len == 0) return false;
211-
if (s_global_ipv6[0] == '\0') {
212-
buf[0] = '\0';
213-
return false;
214-
}
215-
strncpy(buf, s_global_ipv6, len - 1);
216-
buf[len - 1] = '\0';
217-
return true;
218-
}
219-
220204
void MQTTBridge::formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPrefs* obs) {
221205
if (buf == nullptr || bufsize == 0) return;
222206
const char* msgs = (obs && obs->mqtt_status_enabled) ? "on" : "off";
@@ -845,23 +829,11 @@ void MQTTBridge::initializeWiFiInTask() {
845829
switch(event) {
846830
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
847831
MQTT_DEBUG_PRINTLN("WiFi connected: %s", IPAddress(info.got_ip.ip_info.ip.addr).toString().c_str());
848-
// Kick off IPv6 link-local + RA/SLAAC (additive; IPv4 path unchanged). Idempotent.
849-
WiFi.enableIpV6();
850832
// Set flag to trigger NTP sync from loop() instead of doing it here
851833
if (!_ntp_synced && !_ntp_sync_pending) {
852834
_ntp_sync_pending = true;
853835
}
854836
break;
855-
case ARDUINO_EVENT_WIFI_STA_GOT_IP6: {
856-
// Store only global/unique-local addresses; link-local isn't useful for diagnostics.
857-
esp_ip6_addr_t ip6 = info.got_ip6.ip6_info.ip;
858-
esp_ip6_addr_type_t type = esp_netif_ip6_get_addr_type(&ip6);
859-
if (type == ESP_IP6_ADDR_IS_GLOBAL || type == ESP_IP6_ADDR_IS_UNIQUE_LOCAL) {
860-
snprintf(s_global_ipv6, sizeof(s_global_ipv6), IPV6STR, IPV62STR(ip6));
861-
MQTT_DEBUG_PRINTLN("WiFi IPv6: %s", s_global_ipv6);
862-
}
863-
break;
864-
}
865837
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
866838
s_wifi_disconnect_reason = info.wifi_sta_disconnected.reason;
867839
s_wifi_disconnect_time = millis();
@@ -1353,16 +1325,7 @@ void MQTTBridge::setupSlot(int index) {
13531325
} else if (slot.port == 443) {
13541326
proto = "wss";
13551327
}
1356-
// Bare IPv6 literals (contain ':' but no '.', not already bracketed) must be wrapped
1357-
// in [..] so the ":port" suffix isn't mistaken for part of the address.
1358-
bool bare_ipv6 = (slot.host[0] != '[') &&
1359-
(strchr(slot.host, ':') != nullptr) &&
1360-
(strchr(slot.host, '.') == nullptr);
1361-
if (bare_ipv6) {
1362-
snprintf(slot.broker_uri, sizeof(slot.broker_uri), "%s://[%s]:%d", proto, slot.host, slot.port);
1363-
} else {
1364-
snprintf(slot.broker_uri, sizeof(slot.broker_uri), "%s://%s:%d", proto, slot.host, slot.port);
1365-
}
1328+
snprintf(slot.broker_uri, sizeof(slot.broker_uri), "%s://%s:%d", proto, slot.host, slot.port);
13661329
}
13671330
slot.client->setServer(slot.broker_uri);
13681331
MQTT_DEBUG_PRINTLN("MQTT%d custom broker URI: %s (host='%s', port=%u)",
@@ -2117,9 +2080,6 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
21172080
_wifi_disconnected_time = 0;
21182081
s_wifi_connected_at = now;
21192082
_wifi_reconnect_backoff_attempt = 0;
2120-
// Re-arm IPv6 link-local after a reconnect (covers paths that re-run WiFi.begin
2121-
// and skip the GOT_IP event ordering). Idempotent.
2122-
WiFi.enableIpV6();
21232083
#ifdef ESP_PLATFORM
21242084
wifi_ps_type_t ps_mode;
21252085
uint8_t ps_pref = _obs->wifi_power_save;
@@ -2146,7 +2106,6 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
21462106
if (_last_wifi_status == WL_CONNECTED) {
21472107
_wifi_disconnected_time = now;
21482108
s_wifi_connected_at = 0;
2149-
s_global_ipv6[0] = '\0'; // drop stale IPv6 so get wifi.status doesn't report it
21502109
// Disconnect all slot clients when WiFi drops
21512110
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
21522111
if (_slots[i].client && _slots[i].connected) {

src/helpers/bridges/MQTTBridge.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -430,10 +430,6 @@ class MQTTBridge : public BridgeBase {
430430

431431
static unsigned long getWifiConnectedAtMillis();
432432

433-
// Copies the current global/unique-local IPv6 address (string) into buf.
434-
// Returns false (and writes an empty string) when no global IPv6 is assigned.
435-
static bool getGlobalIPv6(char* buf, size_t len);
436-
437433
/**
438434
* Per-slot outage accessors used by AlertReporter to detect prolonged
439435
* MQTT broker outages. Indices are 0..RUNTIME_MQTT_SLOTS-1.

0 commit comments

Comments
 (0)