Summary
On NCS v3.3.0, building any application that enables the new Zephyr OpenThread Border Router L2 glue (CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER=y, together with CONFIG_OPENTHREAD_BORDER_ROUTER=y / CONFIG_OPENTHREAD_BORDER_ROUTING=y) fails to link.
zephyr/subsys/net/l2/openthread/openthread_border_router.c calls a set of OpenThread APIs that do not exist in the OpenThread snapshot pinned by the v3.3.0 manifest, and the nrf/modules/openthread/platform/CMakeLists.txt overlay omits all the new platform glue files that the L2 layer expects to link against. Both issues are independent and both are required to make OPENTHREAD_ZEPHYR_BORDER_ROUTER=y work on Nordic targets.
This effectively makes the new "Zephyr Border Router" path unusable as shipped in NCS v3.3.0 on Nordic hardware. NCS v3.2.4 with the older BR config still works (used as the workaround).
Versions / environment
| Component | Revision |
|---|---|
| sdk-nrf | v3.3.0 (ba167d9f3d) |
| Zephyr (Nordic fork) | ncs-v3.3.0 (fd9204a02d5) |
OpenThread (modules/lib/openthread) |
ncs-thread-reference-20250402 (0e2666713) |
| Zephyr SDK | 0.17.4 |
| Toolchain | arm-zephyr-eabi-gcc 12.2.0 |
| Target | nrf5340dk/nrf5340/cpuapp (sysbuild, ipc_radio net core with BT_HCI_IPC + IEEE802154) |
| Host | Linux x86_64 |
Reproduction
A minimal prj.conf that triggers the failure on nrf5340dk_nrf5340_cpuapp (full app context omitted; happens on any OTBR-style config):
CONFIG_NETWORKING=y CONFIG_NET_IPV6=y CONFIG_NET_UDP=y CONFIG_NET_TCP=y CONFIG_NET_SOCKETS=y CONFIG_NET_L2_OPENTHREAD=y CONFIG_NET_L2_ETHERNET=y CONFIG_NET_CONNECTION_MANAGER=y CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER=y CONFIG_OPENTHREAD_THREAD_VERSION_1_4=y CONFIG_OPENTHREAD_EXTERNAL_HEAP=y CONFIG_OPENTHREAD_MANUAL_START=n # This is the switch that breaks the build on v3.3.0: CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER=y CONFIG_OPENTHREAD_BORDER_ROUTER=y CONFIG_OPENTHREAD_BORDER_ROUTING=y CONFIG_OPENTHREAD_BACKBONE_ROUTER=y CONFIG_OPENTHREAD_BACKBONE_ROUTER_MULTICAST_ROUTING=y CONFIG_OPENTHREAD_SLAAC=y CONFIG_OPENTHREAD_NETDATA_PUBLISHER=y CONFIG_OPENTHREAD_DNSSD_SERVER=y CONFIG_OPENTHREAD_SRP_SERVER=y CONFIG_OPENTHREAD_NAT64_TRANSLATOR=n CONFIG_OPENTHREAD_NAT64_BORDER_ROUTING=n
west build -b nrf5340dk/nrf5340/cpuapp --sysbuild
Failure mode 1 - Compile-time: missing OpenThread APIs
zephyr/subsys/net/l2/openthread/openthread_border_router.c (Zephyr v3.3.0, the new Zephyr-side BR glue) calls:
otMdnsSetLocalHostName(...)- line 91, line 247otBorderAgentIsEnabled(...)- line 140otBorderAgentSetEnabled(...)- lines 141, 194
These are emitted as -Wimplicit-function-declaration warnings:
.../openthread_border_router.c:91:13: warning: implicit declaration of function
'otMdnsSetLocalHostName' [-Wimplicit-function-declaration]
.../openthread_border_router.c:140:14: warning: implicit declaration of function
'otBorderAgentIsEnabled'; did you mean 'otBorderAgentSetId'?
.../openthread_border_router.c:141:17: warning: implicit declaration of function
'otBorderAgentSetEnabled'; did you mean 'otBorderRoutingSetEnabled'?
Verification - none of those symbols exist anywhere in the OpenThread tree pinned by the v3.3.0 manifest:
$ git describe --tags
ncs-thread-reference-20250402
$ grep -rn 'otMdnsSetLocalHostName\|otBorderAgentSetEnabled\b\|otBorderAgentIsEnabled\b' include/ src/
(no matches)
By contrast, otBorderRoutingDhcp6PdSetEnabled, which is also called by the same file and from the same upstream PR family, is present:
include/openthread/border_routing.h:562:void otBorderRoutingDhcp6PdSetEnabled(otInstance *aInstance, bool aEnabled);
src/core/api/border_routing_api.cpp:221:void otBorderRoutingDhcp6PdSetEnabled(otInstance *aInstance, bool aEnabled)
So the OpenThread snapshot pinned in v3.3.0 is partially-but-not-fully synchronised with the upstream that the new openthread_border_router.c was written against.
Failure mode 2 - Link-time: NCS platform overlay omits the new BR glue files
zephyr/modules/openthread/platform/CMakeLists.txt (upstream Zephyr) adds these sources when CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER=y:
zephyr_library_sources_ifdef(CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER infra_if.c) zephyr_library_sources_ifdef(CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER udp.c) zephyr_library_sources_ifdef(CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER mdns_socket.c) zephyr_library_sources_ifdef(CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER border_agent.c) zephyr_library_sources_ifdef(CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER trel.c) zephyr_library_sources_ifdef(CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER dhcp6_pd.c) zephyr_library_sources_ifdef(CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER dns_upstream_resolver.c)
The NCS overlay at nrf/modules/openthread/platform/CMakeLists.txt replaces the upstream CMake (same zephyr_library_named(openthread_platform)) and only re-adds a subset:
zephyr_library_sources_ifdef(CONFIG_OPENTHREAD_CRYPTO_PSA crypto_psa.c)
zephyr_library_sources(
${ZEPHYR_BASE}/modules/openthread/platform/alarm.c
${ZEPHYR_BASE}/modules/openthread/platform/entropy.c
${ZEPHYR_BASE}/modules/openthread/platform/misc.c
${ZEPHYR_BASE}/modules/openthread/platform/platform.c
)
# ...radio, spi, ble, diag, uart, memory, messagepool, settings, logging...
# (NO infra_if.c, udp.c, mdns_socket.c, border_agent.c, trel.c, dhcp6_pd.c,
# dns_upstream_resolver.c - none of the new BR glue is added.)
Result - when OPENTHREAD_ZEPHYR_BORDER_ROUTER=y, the L2 layer references symbols that are simply never compiled:
ld.bfd: openthread_border_router.c.obj: in function `ail_ipv6_address_event_handler':
.../openthread_border_router.c:273: undefined reference to `mdns_plat_monitor_interface'
.../openthread_border_router.c:91: undefined reference to `otMdnsSetLocalHostName'
.../openthread_border_router.c:98: undefined reference to `trel_plat_init'
.../openthread_border_router.c:103: undefined reference to `infra_if_init'
.../openthread_border_router.c:107: undefined reference to `udp_plat_init'
.../openthread_border_router.c:111: undefined reference to `mdns_plat_socket_init'
.../openthread_border_router.c:115: undefined reference to `dhcpv6_pd_client_init'
.../openthread_border_router.c:120: undefined reference to `border_agent_init'
.../openthread_border_router.c:140: undefined reference to `otBorderAgentIsEnabled'
.../openthread_border_router.c:141: undefined reference to `otBorderAgentSetEnabled'
.../openthread_border_router.c:148: undefined reference to `otBorderRoutingDhcp6PdSetEnabled'
.../openthread_border_router.c:191: undefined reference to `border_agent_deinit'
.../openthread_border_router.c:192: undefined reference to `infra_if_deinit'
.../openthread_border_router.c:193: undefined reference to `infra_if_stop_icmp6_listener'
.../openthread_border_router.c:194: undefined reference to `otBorderAgentSetEnabled'
.../openthread_border_router.c:195: undefined reference to `udp_plat_deinit'
.../openthread_border_router.c:259: undefined reference to `mdns_plat_monitor_interface'
.../openthread_border_router.c:363: undefined reference to `udp_plat_init_sockfd'
.../openthread_border_router.c:365: undefined reference to `infra_if_start_icmp6_listener'
ld.bfd: libopenthread-ftd.a(infra_if.cpp.obj):
in function `ot::BorderRouter::InfraIf::Send(...)':
.../border_router/infra_if.cpp:93: undefined reference to `otPlatInfraIfSendIcmp6Nd'
collect2: error: ld returned 1 exit status
The two undefined reference categories are:
- OpenThread APIs missing from the pinned OT snapshot (failure mode 1, surfaces at link too because the warnings above are non-errors):
otMdnsSetLocalHostNameotBorderAgentIsEnabledotBorderAgentSetEnabledotPlatInfraIfSendIcmp6Nd
- Platform glue from
zephyr/modules/openthread/platform/not compiled by thenrfoverlay:infra_if_init/infra_if_deinit/infra_if_start_icmp6_listener/infra_if_stop_icmp6_listener(infra_if.c)udp_plat_init/udp_plat_deinit/udp_plat_init_sockfd(udp.c)mdns_plat_socket_init/mdns_plat_monitor_interface(mdns_socket.c)border_agent_init/border_agent_deinit(border_agent.c)trel_plat_init(trel.c)dhcpv6_pd_client_init(dhcp6_pd.c)
Side issue - header collision in modules/openthread/CMakeLists.txt
While diagnosing the above, a separate (and pre-existing) bug surfaced. The Zephyr-side openthread_utils library compiles zephyr/modules/openthread/openthread.c, which does:
#include <openthread_utils.h>
#include "openthread_border_router.h"
openthread_border_router.h only exists in zephyr/subsys/net/l2/openthread/, which is not on the include path of the openthread_utils target. That dir, however, also contains a different openthread_utils.h that is not self-contained (it depends on struct openthread_context).
Adding the L2 dir to the target's include path to satisfy openthread_border_router.h then shadows the correct openthread_utils.h (in zephyr/modules/openthread/include/) and produces:
.../subsys/net/l2/openthread/openthread_utils.h:38:23: error:
invalid use of undefined type 'struct openthread_context'
.../zephyr/modules/openthread/openthread_utils.c:21:20: error:
unknown type name 'uint8_t'
The application-side workaround is to prepend zephyr/modules/openthread/include with BEFORE PRIVATE so the module-private header always wins for <openthread_utils.h>, while the L2 dir still satisfies "openthread_border_router.h". This should be fixed at source - either:
modules/openthread/CMakeLists.txtshould not be doing#include "openthread_border_router.h"from a file in a different target, or- the L2 directory should not export a non-self-contained header named the same as the module-private one.
Suggested fix
Either:
(a) Sync the OpenThread submodule pinned by the NCS v3.3.0 manifest forward to a revision that contains otMdnsSetLocalHostName, otBorderAgentIsEnabled, otBorderAgentSetEnabled, and otPlatInfraIfSendIcmp6Nd, and update nrf/modules/openthread/platform/CMakeLists.txt to include the new BR platform sources from ${ZEPHYR_BASE}/modules/openthread/platform/ under CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER, mirroring upstream Zephyr's CMakeLists.txt.
(b) Or: keep OPENTHREAD_ZEPHYR_BORDER_ROUTER Kconfig-gated as depends on !NRF_* (or similar) until the Nordic platform overlay is updated, so users on Nordic targets get a clear Kconfig-time error instead of a link failure deep into the build.
Workaround currently in use
Reverted the workspace to NCS v3.2.4, which uses the older Zephyr BR layer that the existing nrf overlay supports.
Severity / impact
- Blocks anyone trying to build a Nordic-based OTBR on NCS v3.3.0 with the new Zephyr L2 BR path.
- No runtime workaround on v3.3.0 - purely a build-system / submodule-sync problem.
- Discoverable only at link time after a long build, so the failure is expensive to hit.