Answers checklist.
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.
- 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).
- Boot BR-A, wait until it is leader and has registered
_trel._udp in mDNS.
- 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).
- 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();
Answers checklist.
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._udpperiodically (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:At the same time both
_trel._udpinstances (with SRV, TXTxa/xpand 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.
_trel._udpin mDNS.otTrelGetNumberOfPeers()on both devices (and/orot trel peersif 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.
release/v5.5(also reproducible with currentmaster, the code incomponents/openthread/src/port/esp_openthread_trel.cis identical)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)espressif/mdns1.11.3Summary
esp_openthread_trel.cdiscovers TREL peers exclusively throughmdns_browse_new("_trel", "_udp", ...). The ESP mDNS browse sends its PTR query once (frombrowse_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() == 0permanently: 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._udprecords 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(): callsmdns_browse_new()once and never re-queries.components/mdns/mdns_browser.c(espressif/mdns):browse_add()callsbrowse_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 whosettl == 0, i.e. goodbye packets).Proposed fix (tested)
Add a periodic, non-blocking active PTR search for
_trel._udpto the TREL port, driven from the OpenThread mainloop (esp_openthread_trel_process()), and feed its results into the existingtrel_browse_notifier():mdns_query_async_new(NULL, "_trel", "_udp", MDNS_TYPE_PTR, 3000 ms, 8, NULL)every 15 s whileotTrelGetNumberOfPeers() == 0, every 60 s otherwise;mdns_query_async_get_results(search, 0, ...)(timeout 0, no blocking of the OT task); on completion pass the results totrel_browse_notifier(), thenmdns_query_results_free()/mdns_query_async_delete();esp_openthread_task_switching_lock_release()/..._acquire()following the existing convention of the port;otPlatTrelDisable();mdns_browse_new()is kept for instant announcements and goodbye (TTL 0) handling.With this patch a restarted BR reaches
peers=1within ~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 tomasteras well):