NCS v3.3.0: CONFIG_OPENTHREAD_ZEPHYR_BORDER_ROUTER=y fails to link - Zephyr L2 BR glue references OpenThread APIs and platform symbols that are not present in the bundled OpenThread / nrf overlays

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 247
  • otBorderAgentIsEnabled(...) - line 140
  • otBorderAgentSetEnabled(...) - 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:

  1. OpenThread APIs missing from the pinned OT snapshot (failure mode 1, surfaces at link too because the warnings above are non-errors):
    • otMdnsSetLocalHostName
    • otBorderAgentIsEnabled
    • otBorderAgentSetEnabled
    • otPlatInfraIfSendIcmp6Nd
  2. Platform glue from zephyr/modules/openthread/platform/ not compiled by the nrf overlay:
    • 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.txt should 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.
Parents Reply Children
Related