You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Border Router cannot route mesh→LAN traffic to hosts using its own advertised on-link prefix (route table in libopenthread_br never covers the self-advertised PIO) #204
This issue was researched and written by an AI — Claude (Fable 5), not a human. It was produced by analyzing the ESP-IDF/OpenThread/lwIP sources and the symbol tables of the prebuilt libopenthread_br.a on behalf of the reporting user. The human user reviewed the write-up and personally verified the failure and the workaround on real hardware (packet captures, pings, flashing). Please read with the usual scrutiny applied to automated analysis.
esp-thread-lib (components/openthread/lib): commit 2e2d91a ("update thread-lib for upstream a12ff0d0f")
LAN: AVM FRITZ!Box advertising a GUA prefix and its own ULA prefix via RA
Symptom
Thread end devices become unreachable from LAN hosts (e.g. a Matter
controller) whenever the host sources its traffic from the Border Router's
self-advertised on-link prefix. The mesh looks healthy the whole time (BR is
leader, good link quality, devices "connected"), which makes this very hard
to diagnose.
OpenThread's routing manager derives a local on-link prefix from the extended
PAN ID (fd17:9499:2de5:90bf::/64 here) and advertises it as a PIO on the
backbone link whenever it has not discovered a favored on-link prefix from
another router — e.g. transiently at startup or after a Wi-Fi reconnect,
before the infrastructure router's RA has been received/processed. LAN hosts
then autoconfigure fd17:... addresses (1800 s lifetimes) and — by RFC 6724
rule 8 (longest prefix match against the Thread OMR ULA) — prefer them as
source for Thread destinations. Even after the BR later deprecates the
prefix, hosts hold the addresses until the valid lifetime expires, so the
breakage persists long after the triggering window.
Reproduction from a LAN host (ip -6 route get <omr-address> confirmed the
kernel picked the fd17 source):
ping source
result
host GUA (2a00:xxxx:xxxx:xx00::/64)
works, normal RTT
infra router ULA (fd06:1f1e:b316::/64)
works, normal RTT
BR on-link prefix (fd17:9499:2de5:90bf::/64)
100 % loss
Note the infrastructure prefixes are fully favored-eligible per OnLinkPrefix::IsFavoredOver() (rdisc6 capture of the FRITZ!Box RA):
PIO
Valid lifetime
Preferred lifetime
Flags
2a00:xxxx:xxxx:xx00::/64 (GUA)
7200 s
3600 s
on-link + autonomous
fd06:1f1e:b316::/64 (ULA)
7200 s
3600 s
on-link + autonomous
so the local on-link prefix advertisement is the transient/redundancy case
that OpenThread intentionally supports — and a later capture showed the OTBR
no longer sending the fd17 PIO (deprecating), consistent with the
intermittent trigger. RFC 4862 address retention on the hosts turns that
transient into a long-lived outage.
Root cause
The Thread-side routing is correct: RoutePublisher publishes ::/0 / fc00::/7 into network data (and keeps the ULA route published while the
on-link prefix is deprecating, precisely so return traffic still routes to
the BR). Device replies to fd17:... destinations therefore do reach the BR.
The packets are lost inside the BR's LAN-side forwarding.
lwip_hook_ip6_route() / lwip_hook_nd6_get_gw() are implemented in the
closed-source libopenthread_br.a (esp_openthread_route_table.c.obj).
Inspecting the library symbols shows the route table is populated exclusively
from received RAs:
esp_openthread_route_table_add_route_entry is referenced only by esp_openthread_nd6.c.obj (the ICMPv6 ND receive path)
nothing consumes otBorderRoutingGetOnLinkPrefix() to install a route for
the prefix the BR itself advertises
Since the BR never receives its own RA, no route entry for fd17:9499:2de5:90bf::/64 ever exists. Mesh→LAN packets destined to hosts in
that prefix either fail routing or are handed to the default gateway learned
from the infrastructure router, which has no route for the prefix — a silent
blackhole for exactly the addresses the BR told the hosts to use.
Expected behavior
Whenever the routing manager publishes/advertises (or is still deprecating)
its local on-link prefix, the BR must be able to deliver packets to that
prefix on-link on the backbone interface — e.g. by installing a corresponding
on-link route in the route table, or by assigning the BR an address inside
the prefix.
Workaround (verified)
Assigning the BR a static backbone-netif address inside the local on-link
prefix fixes it: lwIP gives statically configured addresses an implied
on-link /64 (RFC 5942), and nd6_get_next_hop_entry() checks on-link status
before the ND6_GET_GW hook, so replies are delivered directly via neighbor
discovery:
(run on OPENTHREAD_EVENT_ATTACHED / OPENTHREAD_EVENT_DATASET_CHANGED,
refreshed if the prefix changes)
Flashed and verified on the affected network: pings to Thread devices sourced
from the fd17:... host addresses now get replies with normal Thread RTTs,
where they previously had 100 % loss.
This resolves the connectivity loss, but the proper fix belongs in libopenthread_br, since every deployment where the BR's own on-link prefix
wins source selection on a host is affected.
Note
This issue was researched and written by an AI — Claude (Fable 5), not a human. It was produced by analyzing the ESP-IDF/OpenThread/lwIP sources and the symbol tables of the prebuilt
libopenthread_br.aon behalf of the reporting user. The human user reviewed the write-up and personally verified the failure and the workaround on real hardware (packet captures, pings, flashing). Please read with the usual scrutiny applied to automated analysis.Environment
examples/basic_thread_border_router(ESP32-S3 + RCP, Wi-Fi backbone)esp-thread-lib(components/openthread/lib): commit 2e2d91a ("update thread-lib for upstream a12ff0d0f")Symptom
Thread end devices become unreachable from LAN hosts (e.g. a Matter
controller) whenever the host sources its traffic from the Border Router's
self-advertised on-link prefix. The mesh looks healthy the whole time (BR is
leader, good link quality, devices "connected"), which makes this very hard
to diagnose.
OpenThread's routing manager derives a local on-link prefix from the extended
PAN ID (
fd17:9499:2de5:90bf::/64here) and advertises it as a PIO on thebackbone link whenever it has not discovered a favored on-link prefix from
another router — e.g. transiently at startup or after a Wi-Fi reconnect,
before the infrastructure router's RA has been received/processed. LAN hosts
then autoconfigure
fd17:...addresses (1800 s lifetimes) and — by RFC 6724rule 8 (longest prefix match against the Thread OMR ULA) — prefer them as
source for Thread destinations. Even after the BR later deprecates the
prefix, hosts hold the addresses until the valid lifetime expires, so the
breakage persists long after the triggering window.
Reproduction from a LAN host (
ip -6 route get <omr-address>confirmed thekernel picked the
fd17source):2a00:xxxx:xxxx:xx00::/64)fd06:1f1e:b316::/64)fd17:9499:2de5:90bf::/64)Note the infrastructure prefixes are fully favored-eligible per
OnLinkPrefix::IsFavoredOver()(rdisc6 capture of the FRITZ!Box RA):so the local on-link prefix advertisement is the transient/redundancy case
that OpenThread intentionally supports — and a later capture showed the OTBR
no longer sending the fd17 PIO (deprecating), consistent with the
intermittent trigger. RFC 4862 address retention on the hosts turns that
transient into a long-lived outage.
Root cause
The Thread-side routing is correct:
RoutePublisherpublishes::/0/fc00::/7into network data (and keeps the ULA route published while theon-link prefix is deprecating, precisely so return traffic still routes to
the BR). Device replies to
fd17:...destinations therefore do reach the BR.The packets are lost inside the BR's LAN-side forwarding.
lwip_hook_ip6_route()/lwip_hook_nd6_get_gw()are implemented in theclosed-source
libopenthread_br.a(esp_openthread_route_table.c.obj).Inspecting the library symbols shows the route table is populated exclusively
from received RAs:
esp_openthread_route_table_add_route_entryis referenced only byesp_openthread_nd6.c.obj(the ICMPv6 ND receive path)otBorderRoutingGetOnLinkPrefix()to install a route forthe prefix the BR itself advertises
Since the BR never receives its own RA, no route entry for
fd17:9499:2de5:90bf::/64ever exists. Mesh→LAN packets destined to hosts inthat prefix either fail routing or are handed to the default gateway learned
from the infrastructure router, which has no route for the prefix — a silent
blackhole for exactly the addresses the BR told the hosts to use.
Expected behavior
Whenever the routing manager publishes/advertises (or is still deprecating)
its local on-link prefix, the BR must be able to deliver packets to that
prefix on-link on the backbone interface — e.g. by installing a corresponding
on-link route in the route table, or by assigning the BR an address inside
the prefix.
Workaround (verified)
Assigning the BR a static backbone-netif address inside the local on-link
prefix fixes it: lwIP gives statically configured addresses an implied
on-link /64 (RFC 5942), and
nd6_get_next_hop_entry()checks on-link statusbefore the
ND6_GET_GWhook, so replies are delivered directly via neighbordiscovery:
(run on
OPENTHREAD_EVENT_ATTACHED/OPENTHREAD_EVENT_DATASET_CHANGED,refreshed if the prefix changes)
Flashed and verified on the affected network: pings to Thread devices sourced
from the
fd17:...host addresses now get replies with normal Thread RTTs,where they previously had 100 % loss.
This resolves the connectivity loss, but the proper fix belongs in
libopenthread_br, since every deployment where the BR's own on-link prefixwins source selection on a host is affected.