Skip to content

OpenThread TREL: a border router that (re)starts after its peers never discovers them (mDNS browse queries only once) (IDFGH-18196) #19017

Description

@Tarik2142

Answers checklist.

  • I have read the documentation ESP-IDF Programming Guide and the issue is not addressed there.
  • I have updated my IDF branch (master or release) to the latest version and checked that the issue is present there.
  • I have searched the issue tracker for a similar issue and not found a similar issue.

IDF version.

v5.5

Espressif SoC revision.

ESP32-S3

Operating System used.

Windows

How did you build your project?

VS Code IDE

If you are using Windows, please specify command line type.

CMD

Development Kit.

SMLIGHT SLZB-MR5U/Ultima3

Power Supply used.

External 5V

What is the expected behavior?

A border router should reliably (re)discover its TREL peers regardless of boot order and of the loss of a single multicast packet — i.e. the port should re-query _trel._udp periodically (at least while it has no peers), like every other DNS-SD client does.

What is the actual behavior?

Asymmetric discovery, captured with a small periodic dump calling otTrelGetNumberOfPeers() / otTrelGetNextPeer() / otThreadGetPartitionId() under the OpenThread lock:

BR-A (up for a long time):
TREL on port=12390 self xa=bae14cad3d9ee0ed role=leader partition=0x4feeb940 peers=1
  TREL peer xa=3a88fdd6534a6061 xp=aea4a7cacb750643 [fdae:a4a7:cacb:643:58e6:c5ff:fe45:70ac]:12390

BR-B (restarted while BR-A was already up):
TREL on port=12390 self xa=3a88fdd6534a6061 role=leader partition=0x1f7cdd96 peers=0
TREL on port=12390 self xa=3a88fdd6534a6061 role=leader partition=0x1f7cdd96 peers=0   (30 s later)
TREL on port=12390 self xa=3a88fdd6534a6061 role=leader partition=0x1f7cdd96 peers=0   (60 s later)
... stays like this indefinitely

At the same time both _trel._udp instances (with SRV, TXT xa/xp and AAAA records) are resolvable from a PC on the same network, so the responders on both devices work; only the browse on BR-B never delivered a result.

When both BRs are booted at roughly the same time (so each one receives the other's service announcement), discovery is symmetric and the partitions merge within ~30 s — which is what makes the issue easy to miss.

Steps to reproduce.

  1. Two ESP32-S3 border routers with the same Active Operational Dataset, TREL enabled, on the same L2 network, placed outside each other's 802.15.4 range (so that TREL is the only link between them).
  2. Boot BR-A, wait until it is leader and has registered _trel._udp in mDNS.
  3. Boot BR-B a few minutes later on a lossy backbone (Wi-Fi). Repeat a few times if needed (the loss of the single PTR query/answer is probabilistic).
  4. Observe otTrelGetNumberOfPeers() on both devices (and/or ot trel peers if the CLI is enabled).

Debug Logs.


Diagnostic report archive.

No response

More Information.

This report and patch are AI generated.
Patch tested on real OTBR devices in real network setup.

  • ESP-IDF: release/v5.5 (also reproducible with current master, the code in components/openthread/src/port/esp_openthread_trel.c is identical)
  • Target: ESP32-S3
  • OpenThread configuration: CONFIG_OPENTHREAD_BORDER_ROUTER=y, CONFIG_OPENTHREAD_RADIO_SPINEL_UART=y (external 802.15.4 RCP), CONFIG_OPENTHREAD_RADIO_TREL=y (multi-radio: 802.15.4 + TREL)
  • mDNS component: espressif/mdns 1.11.3
  • Backbone: Ethernet + on-link ULA present on both devices

Summary

esp_openthread_trel.c discovers TREL peers exclusively through mdns_browse_new("_trel", "_udp", ...). The ESP mDNS browse sends its PTR query once (from browse_add(); mdns_browser.c) and afterwards only updates its results from unsolicited announcements/answers received on the wire. It never re-queries periodically and does not re-query on TTL expiry.

As a consequence, a border router that starts (or restarts) while its TREL peers are already running learns about them only if that single PTR query and the peer's answer both make it through. If either multicast packet is lost (very common on Wi-Fi), the restarted BR ends up with otTrelGetNumberOfPeers() == 0 permanently: the already-running peers do not re-announce _trel._udp, and the BR never asks again.

Because OpenThread sends all TREL traffic (including the MLE broadcast emulation used for partition merging: Advertisements, Parent Request) as unicast to known peers, a BR with zero peers cannot merge into the other partition even though the other side sees it and sends to it. The result is two Thread partitions that never merge, with both devices staying leader indefinitely, although _trel._udp records of both are perfectly visible to any other host on the LAN (dns-sd -B _trel._udp, avahi-browse).

Root cause

  • components/openthread/src/port/esp_openthread_trel.c, otPlatTrelEnable(): calls mdns_browse_new() once and never re-queries.
  • components/mdns/mdns_browser.c (espressif/mdns): browse_add() calls browse_send() for every interface/protocol exactly once; afterwards results are only updated from received packets (mdns_priv_browse_result_add_*). There is no timer-based re-query and no TTL-driven refresh (browse_sync() only removes results whose ttl == 0, i.e. goodbye packets).

Proposed fix (tested)

Add a periodic, non-blocking active PTR search for _trel._udp to the TREL port, driven from the OpenThread mainloop (esp_openthread_trel_process()), and feed its results into the existing trel_browse_notifier():

  • mdns_query_async_new(NULL, "_trel", "_udp", MDNS_TYPE_PTR, 3000 ms, 8, NULL) every 15 s while otTrelGetNumberOfPeers() == 0, every 60 s otherwise;
  • poll with mdns_query_async_get_results(search, 0, ...) (timeout 0, no blocking of the OT task); on completion pass the results to trel_browse_notifier(), then mdns_query_results_free() / mdns_query_async_delete();
  • mDNS calls are wrapped in esp_openthread_task_switching_lock_release() / ..._acquire() following the existing convention of the port;
  • the pending search is cancelled in otPlatTrelDisable();
  • the existing mdns_browse_new() is kept for instant announcements and goodbye (TTL 0) handling.

With this patch a restarted BR reaches peers=1 within ~15–20 s even when the initial query/answer is lost, and the partitions merge as expected (child within ~30 s, router after the usual router-selection jitter).

Patch (against release/v5.5, applies to master as well):

diff --git a/components/openthread/src/port/esp_openthread_trel.c b/components/openthread/src/port/esp_openthread_trel.c
--- a/components/openthread/src/port/esp_openthread_trel.c
+++ b/components/openthread/src/port/esp_openthread_trel.c
@@ -21,6 +21,7 @@
 #include "esp_openthread_common.h"
 #include "esp_openthread_common_macro.h"
 #include "esp_openthread_lock.h"
+#include "esp_timer.h"
 #include "esp_openthread_platform.h"
 #include "esp_openthread_radio.h"
 #include "esp_vfs_eventfd.h"
@@ -61,6 +62,21 @@ static esp_openthread_circular_queue_info_t s_recv_queue = {.head = 0, .tail = 0
 static const char *s_trel_workflow = "trel";
 static int s_trel_event_fd = -1;
 
+// Periodic active re-query of `_trel._udp`.
+// The ESP mDNS browse sends its PTR query only once (from mdns_browse_new()) and afterwards relies on
+// the peers announcing themselves. A border router that (re)starts while its peers are already up may
+// therefore never learn about them when that single query or its answer is lost (typical for multicast
+// over Wi-Fi), leaving otTrelGetNumberOfPeers() == 0 forever and making a partition merge impossible.
+// The re-query is a non-blocking async PTR search polled from the OpenThread mainloop; its results are
+// handed to the same handler as the browse results.
+#define TREL_REQUERY_INTERVAL_NO_PEER_MS 15000
+#define TREL_REQUERY_INTERVAL_MS 60000
+#define TREL_REQUERY_TIMEOUT_MS 3000
+#define TREL_REQUERY_MAX_RESULTS 8
+
+static mdns_search_once_t *s_trel_search = NULL;
+static int64_t s_trel_last_query_us = 0;
+
 static void trel_browse_notifier(mdns_result_t *result)
 {
     while (result) {
@@ -160,6 +176,44 @@ void esp_openthread_trel_update(esp_openthread_mainloop_context_t *mainloop)
     }
 }
 
+static void trel_requery_step(otInstance *aInstance)
+{
+    if (s_trel_search != NULL) {
+        mdns_result_t *results = NULL;
+        uint8_t num = 0;
+
+        if (!mdns_query_async_get_results(s_trel_search, 0, &results, &num)) {
+            return; // still searching
+        }
+        if (results != NULL) {
+            trel_browse_notifier(results);
+            mdns_query_results_free(results);
+        }
+        esp_openthread_task_switching_lock_release();
+        mdns_query_async_delete(s_trel_search);
+        esp_openthread_task_switching_lock_acquire(portMAX_DELAY);
+        s_trel_search = NULL;
+        return;
+    }
+
+    const int64_t now_us = esp_timer_get_time();
+    const int64_t interval_us =
+        (int64_t)(otTrelGetNumberOfPeers(aInstance) == 0 ? TREL_REQUERY_INTERVAL_NO_PEER_MS : TREL_REQUERY_INTERVAL_MS) * 1000LL;
+
+    if (s_trel_last_query_us != 0 && (now_us - s_trel_last_query_us) < interval_us) {
+        return;
+    }
+    s_trel_last_query_us = now_us;
+
+    esp_openthread_task_switching_lock_release();
+    s_trel_search = mdns_query_async_new(NULL, TREL_MDNS_TYPE, TREL_MDNS_PROTO, MDNS_TYPE_PTR, TREL_REQUERY_TIMEOUT_MS,
+                                         TREL_REQUERY_MAX_RESULTS, NULL);
+    esp_openthread_task_switching_lock_acquire(portMAX_DELAY);
+    if (s_trel_search == NULL) {
+        ESP_LOGW(OT_PLAT_LOG_TAG, "Failed to start TREL mDNS re-query");
+    }
+}
+
 esp_err_t esp_openthread_trel_process(otInstance *aInstance, const esp_openthread_mainloop_context_t *mainloop)
 {
     uint64_t event_read = 0;
@@ -203,6 +257,8 @@ esp_err_t esp_openthread_trel_process(otInstance *aInstance, const esp_openthrea
         }
     }
 
+    trel_requery_step(aInstance);
+
     return ESP_OK;
 }
 
@@ -353,6 +409,11 @@ void otPlatTrelDisable(otInstance *aInstance)
     mdns_service_remove(TREL_MDNS_TYPE, TREL_MDNS_PROTO);
     s_is_service_registered = false;
     mdns_browse_delete(TREL_MDNS_TYPE, TREL_MDNS_PROTO);
+    if (s_trel_search != NULL) {
+        mdns_query_async_delete(s_trel_search);
+        s_trel_search = NULL;
+    }
+    s_trel_last_query_us = 0;
     esp_openthread_task_switching_lock_acquire(portMAX_DELAY);
     esp_openthread_platform_workflow_unregister(s_trel_workflow);
     free_all_buffer();

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions