From 07cd2dc88643f7aafbe755d1e9f4811ef4d69fc6 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Sat, 25 Jul 2026 16:45:36 +0200 Subject: [PATCH 1/4] openthread-br: update to 2026.08.0 Switch from a pinned commit to the 2026.08.0 release tag (the latest release). Drop the two local patches whose fixes are upstream since 2026.07.0: - 101-rest-Permit-to-externally-provide-cJSON-library.patch - 102-openwrt-fix-LuCI-icons.patch Adjust CMAKE_OPTIONS for the proxy rework that arrived with 2026.07.0. That release added OT core Advertising and Discovery Proxies that default on whenever OTBR_MDNS is set and are mutually exclusive with the legacy POSIX proxies, so forcing OTBR_SRP_ADVERTISING_PROXY=ON and OTBR_DNSSD_DISCOVERY_PROXY=ON now fails configure with "Only one Advertising Proxy can be enabled." Drop both flags to adopt the OT core proxies, matching upstream's default build. Also drop OTBR_DUA_ROUTING, which is no longer a recognized option. Set OTBR_VENDOR_NAME and OTBR_PRODUCT_NAME. 2026.07.0 removed the built-in defaults that border_agent.hpp used to supply, so otbr-agent now exits with "Vendor name must be set." unless they are given at compile time or on the command line. They are set to OpenThread and BorderRouter, the values those defaults had, rather than to anything OpenWrt specific. The pair also forms the MeshCoP service instance name, as " ", which is the name shown when adding the border router in a Thread client and the one already-paired clients have recorded. Keeping the previous values means upgrading does not rename anyone's border router. It also leaves OTBR_NAME at OpenThread_BorderRouter, which is what it defaulted to. Pass OTBR_VERSION=$(PKG_VERSION). Left alone, otbr-agent --version reports the upstream CMake project version (0.3.0), which is unrelated to the version this package is built from, so the generic version check fails -- as it already does on master, independently of this update. OTBR_VERSION overrides that string at configure time, so the binary reports the version it was packaged as and the check passes with no override script. luci-app-openthread installs no executables, so the generic checks have nothing to probe there and need no override either. Add a test.sh as the functional smoke test: it exercises both binaries, which also covers their runtime library closure. ot-ctl is probed with -h because it has no version option at this release (openthread/openthread#13424 adds one, but the bundled openthread predates it). The segfault on unrecognized long options that earlier made -h the only safe probe is fixed in the openthread this release bundles (openthread/openthread#13423). The mDNS provider is left unchanged here; a follow-up commit switches it to OpenThread's internal implementation. Signed-off-by: Christian Glombek Assisted-By: Claude Fable 5 --- net/openthread-br/Makefile | 16 ++--- net/openthread-br/README.md | 24 +++++++ ...-to-externally-provide-cJSON-library.patch | 56 --------------- .../patches/102-openwrt-fix-LuCI-icons.patch | 68 ------------------- net/openthread-br/test.sh | 34 ++++++++++ 5 files changed, 66 insertions(+), 132 deletions(-) delete mode 100644 net/openthread-br/patches/101-rest-Permit-to-externally-provide-cJSON-library.patch delete mode 100644 net/openthread-br/patches/102-openwrt-fix-LuCI-icons.patch create mode 100644 net/openthread-br/test.sh diff --git a/net/openthread-br/Makefile b/net/openthread-br/Makefile index 948668545bc06..3d1f49ed39134 100644 --- a/net/openthread-br/Makefile +++ b/net/openthread-br/Makefile @@ -4,13 +4,13 @@ include $(TOPDIR)/rules.mk PKG_NAME:=openthread-br -PKG_SOURCE_DATE:=2025-06-12 -PKG_SOURCE_VERSION:=2f3c799c7463c8f674754e65c53f78bc0bbcbd58 -PKG_RELEASE:=2 +PKG_VERSION:=2026.08.0 +PKG_SOURCE_VERSION:=v$(PKG_VERSION) +PKG_RELEASE:=1 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL=https://github.com/openthread/ot-br-posix.git -PKG_MIRROR_HASH:=fae1c7c88102f88bac32f8ee25e044b9a8781eef6b9acef39388e1e11a44a195 +PKG_MIRROR_HASH:=4645fefcf89d0c06de87bbfa9e52e2a522de48fa52146427669323c894405eb9 PKG_MAINTAINER:=Stijn Tintel PKG_LICENSE:=BSD-3-Clause @@ -63,15 +63,15 @@ CMAKE_OPTIONS += \ -DOT_TARGET_OPENWRT=ON \ -DOTBR_BACKBONE_ROUTER=ON \ -DOTBR_BORDER_ROUTING=ON \ - -DOTBR_DNSSD_DISCOVERY_PROXY=ON \ - -DOTBR_DUA_ROUTING=ON \ -DOTBR_MDNS=mDNSResponder \ -DOTBR_NAT64=OFF \ -DOTBR_OPENWRT=ON \ + -DOTBR_PRODUCT_NAME=BorderRouter \ -DOTBR_REST=ON \ - -DOTBR_SRP_ADVERTISING_PROXY=ON \ -DOTBR_SRP_SERVER_AUTO_ENABLE=ON \ - -DOTBR_TREL=ON + -DOTBR_TREL=ON \ + -DOTBR_VENDOR_NAME=OpenThread \ + -DOTBR_VERSION=$(PKG_VERSION) TARGET_CFLAGS += -DOPENTHREAD_POSIX_CONFIG_DAEMON_SOCKET_BASENAME=\\\"/var/run/openthread-%s\\\" diff --git a/net/openthread-br/README.md b/net/openthread-br/README.md index e4a26d427ba71..25cd4c32eeb72 100644 --- a/net/openthread-br/README.md +++ b/net/openthread-br/README.md @@ -25,6 +25,30 @@ the package will likely result in more bug reports. As the package and its dependencies are unlikely to fit in any router with small flash (16MB or less), I don't see much point in making things configurable for reducing size either. +### Vendor and product name + +`OTBR_VENDOR_NAME` and `OTBR_PRODUCT_NAME` have to be set: this release removed +the built-in defaults, and otbr-agent exits with `Vendor name must be set.` +without them. + +They are deliberately set to the values those defaults had, `OpenThread` and +`BorderRouter`, rather than to something OpenWrt specific. The pair forms the +MeshCoP service instance name as ` `, which is the name shown +when adding the border router in a Thread client and the one already-paired +clients have recorded, so changing it would rename every existing user's border +router on upgrade. + +### Version string + +`OTBR_VERSION` is set to `PKG_VERSION`. Without it the build falls back to the +CMake project version, because the repacked source tree has no git directory +for `git describe` to read, so `otbr-agent --version` and the `Running ...` +line it logs on every start would report `0.3.0` rather than the release the +package was built from. + +The version test in the package CI matches on that string, so dropping this +option would make the package fail it again. + ### Firewall support OpenWrt uses firewall4 with nftables by default, but the OpenThread firewall diff --git a/net/openthread-br/patches/101-rest-Permit-to-externally-provide-cJSON-library.patch b/net/openthread-br/patches/101-rest-Permit-to-externally-provide-cJSON-library.patch deleted file mode 100644 index 812978c335096..0000000000000 --- a/net/openthread-br/patches/101-rest-Permit-to-externally-provide-cJSON-library.patch +++ /dev/null @@ -1,56 +0,0 @@ -From 59c389a91cfe91eebed9c36887fa2a3eca4cbd6f Mon Sep 17 00:00:00 2001 -From: Christian Marangi -Date: Mon, 24 Nov 2025 18:53:28 +0100 -Subject: [PATCH] [rest] Permit to externally provide cJSON library - -Permit to externally provide cJSON library if found with PKGConfig. - -Signed-off-by: Christian Marangi ---- - src/rest/CMakeLists.txt | 12 +++++++++++- - third_party/CMakeLists.txt | 5 ++++- - 2 files changed, 15 insertions(+), 2 deletions(-) - ---- a/src/rest/CMakeLists.txt -+++ b/src/rest/CMakeLists.txt -@@ -36,11 +36,22 @@ add_library(otbr-rest - response.cpp - ) - -+if (CJSON_FOUND) -+ set(CJSON_LIB_TARGETS ${CJSON_LINK_LIBRARIES}) -+else() -+ set(CJSON_LIB_TARGETS cjson) -+endif() -+ -+target_include_directories(otbr-rest -+ PRIVATE -+ ${CJSON_INCLUDE_DIRS} -+) -+ - target_link_libraries(otbr-rest - PUBLIC - http_parser - PRIVATE -- cjson -+ ${CJSON_LIB_TARGETS} - otbr-config - otbr-utils - openthread-ftd ---- a/third_party/CMakeLists.txt -+++ b/third_party/CMakeLists.txt -@@ -28,6 +28,13 @@ - - add_subdirectory(openthread) - if(OTBR_REST) -- add_subdirectory(cJSON) -+ pkg_check_modules(CJSON libcjson) -+ if (CJSON_FOUND) -+ set(CJSON_FOUND ${CJSON_FOUND} PARENT_SCOPE) -+ set(CJSON_INCLUDE_DIRS ${CJSON_INCLUDE_DIRS} PARENT_SCOPE) -+ set(CJSON_LINK_LIBRARIES ${CJSON_LINK_LIBRARIES} PARENT_SCOPE) -+ else() -+ add_subdirectory(cJSON) -+ endif() - add_subdirectory(http-parser) - endif() diff --git a/net/openthread-br/patches/102-openwrt-fix-LuCI-icons.patch b/net/openthread-br/patches/102-openwrt-fix-LuCI-icons.patch deleted file mode 100644 index 701ae0f201fb4..0000000000000 --- a/net/openthread-br/patches/102-openwrt-fix-LuCI-icons.patch +++ /dev/null @@ -1,68 +0,0 @@ -From 4b09d856fd3d82bfb6707d889fb40f981945a6a2 Mon Sep 17 00:00:00 2001 -From: Stijn Tintel -Date: Wed, 7 Jan 2026 00:20:04 +0200 -Subject: [PATCH] [openwrt] fix LuCI icons (#3178) - -The PNG icons in LuCI have been replaced with SVG icons. - -Signed-off-by: Stijn Tintel ---- - src/openwrt/view/admin_thread/thread_overview.htm | 10 +++++----- - src/openwrt/view/admin_thread/thread_scan.htm | 12 ++++++------ - 2 files changed, 11 insertions(+), 11 deletions(-) - ---- a/src/openwrt/view/admin_thread/thread_overview.htm -+++ b/src/openwrt/view/admin_thread/thread_overview.htm -@@ -32,7 +32,7 @@ - -
-
-- " id="wpan0" /> <%=threadget("interfacename").InterfaceName%> -+ " id="wpan0" /> <%=threadget("interfacename").InterfaceName%> -
-
- <%:Generic MAC 802.15.4 Thread%>
-@@ -164,13 +164,13 @@ - var scale = percent_thread_signal(info); - - if (scale == 0) -- icon = "<%=resource%>/icons/signal-0.png"; -+ icon = "<%=resource%>/icons/signal-000.svg"; - else if (scale == 30) -- icon = "<%=resource%>/icons/signal-25-50.png"; -+ icon = "<%=resource%>/icons/signal-025-050.svg"; - else if (scale == 50) -- icon = "<%=resource%>/icons/signal-50-75.png"; -+ icon = "<%=resource%>/icons/signal-050-075.svg"; - else -- icon = "<%=resource%>/icons/signal-75-100.png"; -+ icon = "<%=resource%>/icons/signal-075-100.svg"; - - return icon; - } ---- a/src/openwrt/view/admin_thread/thread_scan.htm -+++ b/src/openwrt/view/admin_thread/thread_scan.htm -@@ -17,17 +17,17 @@ - end - - if info.NetworkName == nil then -- icon = resource .. "/icons/signal-none.png" -+ icon = resource .. "/icons/signal-none.svg" - elseif scale < 15 then -- icon = resource .. "/icons/signal-0.png" -+ icon = resource .. "/icons/signal-000.svg" - elseif scale < 35 then -- icon = resource .. "/icons/signal-0-25.png" -+ icon = resource .. "/icons/signal-000-025.svg" - elseif scale < 55 then -- icon = resource .. "/icons/signal-25-50.png" -+ icon = resource .. "/icons/signal-025-050.svg" - elseif scale < 75 then -- icon = resource .. "/icons/signal-50-75.png" -+ icon = resource .. "/icons/signal-050-075.svg" - else -- icon = resource .. "/icons/signal-75-100.png" -+ icon = resource .. "/icons/signal-075-100.svg" - end - - return icon diff --git a/net/openthread-br/test.sh b/net/openthread-br/test.sh new file mode 100644 index 0000000000000..4c8f6621fce0b --- /dev/null +++ b/net/openthread-br/test.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# +# Functional smoke tests for openthread-br. + +set -e + +case "$PKG_NAME" in +openthread-br) + # Exercises otbr-agent's option parser and its full runtime closure. + # --version prints and exits without touching the RCP or the network. + # The Makefile passes OTBR_VERSION=$(PKG_VERSION), so this is also + # the string the generic version check matches. + otbr-agent --version + + # Use -h rather than --version here: ot-ctl has no version option at + # this release (openthread/openthread#13424 adds one, but the openthread + # bundled here predates it), so -h is the option that exits inside the + # parser. The older hazard -- a segfault on any unrecognized long + # option, from a getopt_long() array missing its terminating entry -- + # is fixed in the openthread this release bundles + # (openthread/openthread#13423). + ot-ctl -h >/dev/null + ;; + +luci-app-openthread) + # LuCI views and a Lua controller only; nothing executable to exercise. + ;; + +*) + echo "test.sh: unknown package '$PKG_NAME' — refusing to silently pass" >&2 + echo "test.sh: update net/openthread-br/test.sh to cover this package" >&2 + exit 1 + ;; +esac From caacf174a8ad6c422aba2e91d1c20f53ffb61619 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Sat, 25 Jul 2026 16:47:53 +0200 Subject: [PATCH 2/4] openthread-br: make the REST API listen address configurable As of the 2026.07.0 update, otbr-agent's REST API defaults to listening on 127.0.0.1, so remote clients (e.g. Home Assistant on another host) can no longer reach it. Add rest_listen_address and rest_listen_port proto options and pass them through to otbr-agent when set; leaving them unset keeps the secure loopback-only default. Also drop the leftover unused 'foobar' proto config option. Signed-off-by: Christian Glombek Assisted-By: Claude Fable 5 --- net/openthread-br/README.md | 14 +++++++++++--- net/openthread-br/files/openthread-proto.sh | 12 ++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/net/openthread-br/README.md b/net/openthread-br/README.md index 25cd4c32eeb72..e209b8998ad00 100644 --- a/net/openthread-br/README.md +++ b/net/openthread-br/README.md @@ -70,6 +70,10 @@ D-Bus support. ### REST Server The REST server is enabled to make this package compatible with Home Assistant. +It listens on 127.0.0.1 by default. `rest_listen_address` and +`rest_listen_port` can move it, but the API is unauthenticated and can read and +replace the Thread dataset — including the network key — so any non-loopback +address must be firewalled to trusted hosts. ### TREL support @@ -112,9 +116,13 @@ config interface 'thread' option verbose '0' ``` -Prefix and verbose are optional. Everything else is required. The protocol -handler will fail if a required setting is missing. If something isn't working, -check ifstatus for the OpenThread interface: +Only backbone_network, device and radio_url are required; the protocol handler +fails the interface if one of them is missing, or if backbone_network names an +interface that has no device. Everything else — dataset, prefix, verbose, +rest_listen_address and rest_listen_port — is optional. See +[REST Server](#rest-server) before moving the REST API off the loopback +default. If something isn't working, check ifstatus for the OpenThread +interface: ``` # ifup thread diff --git a/net/openthread-br/files/openthread-proto.sh b/net/openthread-br/files/openthread-proto.sh index 995906c9d3942..7244820f562c8 100644 --- a/net/openthread-br/files/openthread-proto.sh +++ b/net/openthread-br/files/openthread-proto.sh @@ -39,7 +39,8 @@ proto_openthread_init_config() { proto_config_add_string backbone_network proto_config_add_string dataset proto_config_add_string radio_url - proto_config_add_string foobar + proto_config_add_string rest_listen_address + proto_config_add_int rest_listen_port available=1 no_device=1 @@ -61,7 +62,7 @@ proto_openthread_setup() { mkdir -p /var/lib/thread - json_get_vars backbone_network dataset device radio_url verbose:0 + json_get_vars backbone_network dataset device radio_url rest_listen_address rest_listen_port verbose:0 [ -n "$backbone_network" ] || proto_openthread_setup_error "$interface" MISSING_BACKBONE_NETWORK proto_add_host_dependency "$interface" "" "$backbone_network" @@ -78,6 +79,13 @@ proto_openthread_setup() { [ "$verbose" -eq 0 ] || append opts -v append opts "-I$device" append opts "-B$backbone_ifname" + # The REST API listens on 127.0.0.1 by default. Bind it elsewhere (e.g. a + # LAN address) to let remote clients such as Home Assistant reach it; + # leaving it unset keeps the loopback-only default. The REST API is + # unauthenticated and can both read and replace the Thread dataset, so any + # non-loopback address must be firewalled to trusted hosts. + [ -n "$rest_listen_address" ] && append opts "--rest-listen-address=$rest_listen_address" + [ -n "$rest_listen_port" ] && append opts "--rest-listen-port=$rest_listen_port" append opts "$radio_url" append opts "trel://$backbone_ifname" # run in subshell to prevent wiping json data needed for prefixes From 233cfaa72c70cbcc82f35df3bcc090a586d5ff4e Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Sat, 25 Jul 2026 21:42:09 +0200 Subject: [PATCH 3/4] openthread-br: use OpenThread's internal mDNS Switch OTBR_MDNS from mDNSResponder to openthread, drop the mdnsd and mDNSResponder dependencies, and remove the proto handler's mdnsd service check, which would otherwise refuse to start otbr-agent now that mdnsd is no longer installed. This follows upstream, which made the internal implementation the default in openthread/ot-br-posix#2996 and uses it in its own OpenWrt example package, and it lets otbr-agent share a router with umdns, which is what advertises the router's procd-registered services (ssh, samba and the like) and resolves .local. The two can coexist because they never contend for a name: OpenThread's mDNS names its host after the Thread extended address, while umdns keeps .local. Both bind UDP 5353 with SO_REUSEADDR and each answers only for its own records. mDNSResponder instead takes its name from gethostname(), so it and umdns race for the same name, and the loser either stops announcing without logging anywhere procd captures or, if neither notices, both keep claiming it. That is the conflict with a running mdnsd noted in openwrt/packages#29784. Border router discovery is unaffected: in this mode OpenThread's own border agent registers the _meshcop._udp service (its OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE follows OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE), and OTBR's publisher is disabled by design, so clients such as Home Assistant still find the border router. Note that OpenThread's mDNS serves a single interface, the one given by the backbone_network option, so Thread service records are announced on that network only. Signed-off-by: Christian Glombek Assisted-By: Claude Opus 4.8 --- net/openthread-br/Makefile | 6 ++--- net/openthread-br/README.md | 29 ++++++++++++++++----- net/openthread-br/files/openthread-proto.sh | 15 ----------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/net/openthread-br/Makefile b/net/openthread-br/Makefile index 3d1f49ed39134..2d804e6bd587f 100644 --- a/net/openthread-br/Makefile +++ b/net/openthread-br/Makefile @@ -41,9 +41,7 @@ define Package/openthread-br +libreadline \ +libstdcpp \ +libubox \ - +libubus \ - +mdnsd \ - +mdnsresponder + +libubus endef define Package/openthread-br/description @@ -63,7 +61,7 @@ CMAKE_OPTIONS += \ -DOT_TARGET_OPENWRT=ON \ -DOTBR_BACKBONE_ROUTER=ON \ -DOTBR_BORDER_ROUTING=ON \ - -DOTBR_MDNS=mDNSResponder \ + -DOTBR_MDNS=openthread \ -DOTBR_NAT64=OFF \ -DOTBR_OPENWRT=ON \ -DOTBR_PRODUCT_NAME=BorderRouter \ diff --git a/net/openthread-br/README.md b/net/openthread-br/README.md index e209b8998ad00..5c5d26f5fbf1c 100644 --- a/net/openthread-br/README.md +++ b/net/openthread-br/README.md @@ -59,13 +59,28 @@ Therefore, firewall support is disabled completely. This can be revised once the following feature request is implemented: https://github.com/openthread/ot-br-posix/issues/1675 -### mDNSResponder - -The package depends on mDNSResponder. The alternative, Avahi, depends on D-Bus, -which is not something I feel comfortable with running on any router. While -there are Avahi packages without D-Bus support, using OpenThread Border Router -with Avahi requires libavahi-client, and this requires Avahi to be built with -D-Bus support. +### mDNS + +The package uses OpenThread's internal mDNS implementation +(`-DOTBR_MDNS=openthread`), which is upstream's default. This drops the +mDNSResponder dependency entirely: no separate daemon, and no Avahi, whose +libavahi-client requirement would have pulled in D-Bus. + +The internal implementation advertises on a single infrastructure interface, +the one selected by the `backbone_network` option. Anything that needs to be +announced on more than one interface still needs a general-purpose responder. + +It coexists with umdns, which remains the provider for other packages' +services. Both bind the wildcard address on port 5353 with SO_REUSEADDR, which +is what admits the second bind and gets multicast delivered to both, and they +never contend for a name: OpenThread's mDNS names its host after the Thread +extended address, while umdns keeps `.local`. + +Only multicast reaches both. A unicast datagram to port 5353 is delivered to +one socket, so a unicast reply meant for one daemon can be received by the +other. umdns does set SO_REUSEPORT, but only on a retry after its own bind +fails, and that does not happen here because SO_REUSEADDR already admits the +bind, so no SO_REUSEPORT group forms in either start order. ### REST Server diff --git a/net/openthread-br/files/openthread-proto.sh b/net/openthread-br/files/openthread-proto.sh index 7244820f562c8..fde143c51b583 100644 --- a/net/openthread-br/files/openthread-proto.sh +++ b/net/openthread-br/files/openthread-proto.sh @@ -21,18 +21,6 @@ proto_openthread_add_prefix() { [ -n "$prefix" ] && $OTCTL prefix add $prefix } -proto_openthread_check_service() { - service="$1" - ret=1 - json_init - json_add_string name "$service" - ubus call service list "$(json_dump)" | jsonfilter -e '@[*].instances[*]["running"]' > /dev/null - ret=$? - json_cleanup - - return "$ret" -} - proto_openthread_init_config() { proto_config_add_array 'prefix:list(string)' proto_config_add_boolean verbose @@ -72,9 +60,6 @@ proto_openthread_setup() { [ -n "$device" ] || proto_openthread_setup_error "$interface" MISSING_DEVICE [ -n "$radio_url" ] || proto_openthread_setup_error "$interface" MISSING_RADIO_URL - # run in subshell to prevent wiping json data needed for prefixes - ( proto_openthread_check_service mdnsd ) || proto_openthread_setup_error "$interface" MISSING_SVC_MDNSD - opts="--auto-attach=0" [ "$verbose" -eq 0 ] || append opts -v append opts "-I$device" From 98a7d04702b5a0184ae3a8d7438e4e659373f7c3 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Fri, 14 Aug 2026 05:49:57 +0200 Subject: [PATCH 4/4] openthread-br: locate the RCP dongle through otbr-rcp The protocol handler required a hand-written radio_url, so pointing the border router at its dongle meant knowing which ttyACM the kernel had picked, and a replug that renumbered the device broke the interface until someone edited the config. Bring over the otbr-rcp wrapper from the matter-openwrt packaging: when no radio_url is set, the handler resolves the dongle by its USB properties (a cdc_acm interface on a device whose product string names OpenThread), with an rcp option to pin a bus position or a fixed serial device, and optional firmware install and update through handler plugins. Discovery runs in the setup phase rather than under the launched command, since a flash can take minutes and must not race the bounded wait for the agent's ubus object. Firmware install and update are off by default: flashing is an explicit act, and with the radio no longer required to be named, a configuration that mentions no RCP at all now reaches this path. The one value the handler needs is picked out of otbr-rcp's output rather than evaluated, since otbr-rcp sources every firmware handler plugin and stdout from one of those would otherwise run as root in the proto handler. A missing dongle fails setup with the restart block every failure needs, netifd retrying immediately and without backoff otherwise. The USB hotplug handler brings such interfaces up again when a device binds, keyed on the recorded RCP_NOT_FOUND error so interfaces an administrator took down on purpose stay down. An explicit radio_url keeps today's behaviour. Assisted-By: Claude Fable 5 Signed-off-by: Christian Glombek --- net/openthread-br/Makefile | 5 +- net/openthread-br/README.md | 53 ++- net/openthread-br/files/openthread-proto.sh | 48 ++- net/openthread-br/files/otbr-rcp | 342 ++++++++++++++++++++ net/openthread-br/files/otbr-rcp.hotplug | 32 ++ 5 files changed, 471 insertions(+), 9 deletions(-) create mode 100644 net/openthread-br/files/otbr-rcp create mode 100644 net/openthread-br/files/otbr-rcp.hotplug diff --git a/net/openthread-br/Makefile b/net/openthread-br/Makefile index 2d804e6bd587f..5af168412244c 100644 --- a/net/openthread-br/Makefile +++ b/net/openthread-br/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=openthread-br PKG_VERSION:=2026.08.0 PKG_SOURCE_VERSION:=v$(PKG_VERSION) -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL=https://github.com/openthread/ot-br-posix.git @@ -91,10 +91,13 @@ endef define Package/openthread-br/install $(INSTALL_DIR) \ + $(1)/etc/hotplug.d/usb \ $(1)/etc/init.d \ $(1)/lib/netifd/proto \ $(1)/usr/sbin $(INSTALL_BIN) ./files/openthread-proto.sh $(1)/lib/netifd/proto/openthread.sh + $(INSTALL_BIN) ./files/otbr-rcp $(1)/usr/sbin + $(INSTALL_DATA) ./files/otbr-rcp.hotplug $(1)/etc/hotplug.d/usb/50-otbr-rcp $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/* $(1)/usr/sbin endef diff --git a/net/openthread-br/README.md b/net/openthread-br/README.md index 5c5d26f5fbf1c..15c75b6a06c06 100644 --- a/net/openthread-br/README.md +++ b/net/openthread-br/README.md @@ -126,17 +126,57 @@ config interface 'thread' option device 'wpan0' option proto 'openthread' option backbone_network 'lan' - option radio_url 'spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800' + option rcp '2-1' + option uart_baudrate '460800' list prefix 'fd6f:5772:5468:7200::/64 paros' option verbose '0' ``` -Only backbone_network, device and radio_url are required; the protocol handler -fails the interface if one of them is missing, or if backbone_network names an -interface that has no device. Everything else — dataset, prefix, verbose, +Only backbone_network and device are required. A radio need not be named at +all: with neither rcp nor radio_url set the handler behaves as if `rcp 'any'` +were given and auto-discovers a dongle, failing the interface with +RCP_NOT_FOUND only when none is found. It also fails if backbone_network names +an interface that has no device. Everything else — dataset, prefix, verbose, rest_listen_address and rest_listen_port — is optional. See [REST Server](#rest-server) before moving the REST API off the loopback -default. If something isn't working, check ifstatus for the OpenThread +default. + +### Finding the RCP + +The radio is named with the `rcp` option rather than a full radio URL. It takes +one of three forms: + +| value | meaning | +| --- | --- | +| `/dev/ttyACM0` | a fixed serial device; no discovery is done | +| `2-1` | a USB bus position, resolved to whatever serial device it currently exposes | +| `any` (the default) | pick a dongle automatically | + +`uart_baudrate` and `uart_flow_control` are appended to the resulting URL, and +the port is always opened exclusively. `uart_baudrate` is unset by default, +leaving the port at otbr-agent's own default; `uart_flow_control` defaults to +1, so hardware flow control is on unless it is set to 0. Turn it off for a +3-wire UART, or for a dongle that never asserts CTS. Setting `radio_url` +directly still works and overrides all of this. + +Discovery resolves USB dongles, which requires the cdc_acm driver: only an +interface bound to it is accepted, and the `/dev/ttyACM*` node does not exist +without it. Install `kmod-usb-acm` if it is not already present. A +UART-attached radio named through `/dev/tty*` or `radio_url` needs none of +this. + +Prefer a bus position to `any` unless the dongle advertises itself. Unattended +selection only accepts a device whose USB product string contains the word +"OpenThread", which many dongles — the Home Assistant Connect ZBT-2 among them +— do not. Naming the bus position is the operator saying "this one is the RCP", +so no product string is needed. `ls /sys/bus/usb/devices/` shows the positions. + +`otbr-rcp` also has a plugin point for installing or updating dongle firmware, +used when `rcp_firmware_update` is set. It is off by default, and no handlers +ship with this package, so nothing is flashed unless you both add a handler and +ask for it. + +If something isn't working, check ifstatus for the OpenThread interface: ``` @@ -234,7 +274,8 @@ config interface 'thread' option backbone_network 'lan' option dataset '0e080000000000010000000300000f35060004001fffe0020836b86cd9746ab3080708fd9850cbe719b1d205101f11a11320828c7a6ebc2f2e675c0dca030e686f6d652d617373697374616e740102716f041025804ed78614258ebedf4e2db37b3b6e0c0402a0f7f8' list prefix 'fd6f:5772:5468:7200::/64 paros' - option radio_url 'spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800' + option rcp '2-1' + option uart_baudrate '460800' option verbose '0' ``` diff --git a/net/openthread-br/files/openthread-proto.sh b/net/openthread-br/files/openthread-proto.sh index fde143c51b583..37a156a5bdc75 100644 --- a/net/openthread-br/files/openthread-proto.sh +++ b/net/openthread-br/files/openthread-proto.sh @@ -5,6 +5,7 @@ OTCTL="/usr/sbin/ot-ctl" PROG="/usr/sbin/otbr-agent" +RCP_PROG="/usr/sbin/otbr-rcp" [ -x "$PROG" ] || exit 0 @@ -27,6 +28,10 @@ proto_openthread_init_config() { proto_config_add_string backbone_network proto_config_add_string dataset proto_config_add_string radio_url + proto_config_add_string rcp + proto_config_add_boolean rcp_firmware_update + proto_config_add_int uart_baudrate + proto_config_add_boolean uart_flow_control proto_config_add_string rest_listen_address proto_config_add_int rest_listen_port @@ -44,13 +49,24 @@ proto_openthread_setup_error() { exit 1 } +proto_openthread_setup_retry() { + # A missing RCP dongle is not a configuration error, but the interface + # must still be blocked: netifd re-runs a failed setup immediately and + # without backoff, which would busy-loop until a dongle appears. The + # hotplug handler's ifup lifts the block, so recovery is unaffected; + # this helper differs from proto_openthread_setup_error only in intent. + proto_openthread_setup_error "$@" +} + proto_openthread_setup() { interface="$1" device="$2" mkdir -p /var/lib/thread - json_get_vars backbone_network dataset device radio_url rest_listen_address rest_listen_port verbose:0 + json_get_vars backbone_network dataset device radio_url rcp \ + rcp_firmware_update:0 uart_baudrate:0 uart_flow_control:1 \ + rest_listen_address rest_listen_port verbose:0 [ -n "$backbone_network" ] || proto_openthread_setup_error "$interface" MISSING_BACKBONE_NETWORK proto_add_host_dependency "$interface" "" "$backbone_network" @@ -58,7 +74,35 @@ proto_openthread_setup() { [ -n "$backbone_ifname" ] || proto_openthread_setup_error "$interface" MISSING_BACKBONE_IFNAME [ -n "$device" ] || proto_openthread_setup_error "$interface" MISSING_DEVICE - [ -n "$radio_url" ] || proto_openthread_setup_error "$interface" MISSING_RADIO_URL + if [ -z "$radio_url" ]; then + case "$rcp" in + /dev/*) + # A fixed serial device needs no discovery. + radio_url="spinel+hdlc+uart://$rcp" + ;; + *) + # Let otbr-rcp locate the dongle by its USB properties and, + # when a handler knows how, install or update its firmware. + # This runs here rather than under the launched command: a + # flash can take minutes, and it must not race the bounded + # wait for the agent's ubus object below. + # + # Pick the one value we need out of the output rather than + # evaluating it: otbr-rcp sources every firmware handler in + # /usr/share/openthread-rcp/, and a handler that prints to + # stdout would otherwise have its output run as root here. + RCPTTY="$("$RCP_PROG" \ + $([ "$rcp_firmware_update" -eq 0 ] || echo --update) \ + "${rcp:-any}" | sed -n 's/^RCPTTY=//p')" + [ -n "$RCPTTY" ] || \ + proto_openthread_setup_retry "$interface" RCP_NOT_FOUND + radio_url="spinel+hdlc+uart://$RCPTTY" + ;; + esac + radio_url="${radio_url}?uart-exclusive" + [ "$uart_baudrate" -eq 0 ] || radio_url="${radio_url}&uart-baudrate=${uart_baudrate}" + [ "$uart_flow_control" -eq 0 ] || radio_url="${radio_url}&uart-flow-control" + fi opts="--auto-attach=0" [ "$verbose" -eq 0 ] || append opts -v diff --git a/net/openthread-br/files/otbr-rcp b/net/openthread-br/files/otbr-rcp new file mode 100644 index 0000000000000..9bedb8c8145a9 --- /dev/null +++ b/net/openthread-br/files/otbr-rcp @@ -0,0 +1,342 @@ +#!/bin/sh + +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# otbr-rcp: Wrapper script for otbr-agent to manage an RCP dongle + +# RCP interfaces are recognized using the following USB properties: +# - interface has INTERFACE == 2/2/0 and DRIVER == cdc_acm +# - device product string contains the word "OpenThread" +# Additional an RCP selector string can be specified. This can be +# - "any" (the default) matches any eligible device +# - -

, e.g. "1-1.2.3" matches a specific bus/port path +# If the --wait option is given, the script will wait for a +# suitable device to be hot-plugged if necessary. +# +# If the --update option is given and suitable firmware handler +# plugins are available, the RCP firmware will be auto-updated +# or auto-installed. The latter is only performed if the USB device +# has been manually placed into boot loader mode. + +usage() { + echo "Usage: $0 [OPTIONS...] [RCP] [-- AGENT-ARGS...]" >&2 + exit 2 +} + +RC_NOT_FOUND=3 +RC_NOT_SUPPORTED=4 +RC_INTERNAL=7 +RC_BREAK=63 + +AGENT_PROG=/usr/sbin/otbr-agent + +main() { + # Parse options / arguments + local wait= update= + while [ $# -gt 0 ]; do + case "$1" in + --) break;; + --wait) wait=1; shift;; + --update) update=1; shift;; + -*) echo "Unrecognized option: ${1#-}" >&2; usage;; + *) break;; + esac + done + + local rcp=any + if [ $# -gt 0 -a "$1" != "--" ]; then + rcp="$1" + validate_rcp_selector "$rcp" || usage + shift + fi + + local launch= + if [ $# -gt 0 ]; then + [ "$1" = "--" -a $# -gt 1 ] || usage + launch=1 + shift + fi + + # Find and/or wait for the RCP device + [ -z "$update" ] || load_firmware_handlers + if [ -n "$wait" ]; then + log debug "Looking for RCP devices matching selector '$rcp'" + hotplug_wait find_rcp "$rcp" "$update" + elif ! find_rcp "$rcp" "$update"; then + log error "No RCP device found for selector '$rcp'" + return "$RC_NOT_FOUND" + fi + local rcpdev="${REPLY% *}" installhid="${REPLY#* }" + + # Install firmware if necessary + if [ -n "$update" -a "$installhid" != "-" ]; then + install_rcp "$rcpdev" "$installhid" || return $? + rcpdev="${REPLY% *}" update= + fi + + # Find the corresponding TTY + if ! usb_wait_devnode "$rcpdev" tty ttyACM; then + log error "Unable to resolve TTY for USB device $rcpdev" + return "$RC_INTERNAL" + fi + local rcptty="$REPLY" + local rcpurl="spinel+hdlc+uart://$rcptty" + + # Update firmware if requested, supported, and necessary + if [ -n "$update" ]; then + update_rcp "${rcpdev%:*}" "$rcptty" "$rcpurl" || return $? + fi + + # Exec otbr-agent if requested + if [ -n "$launch" ]; then + local arg + for arg in "$@"; do + shift + set -- "$@" "${arg//%rcpurl%/$rcpurl}" + done + log debug "Executing $AGENT_PROG $*" + exec "$AGENT_PROG" "$@" + else + echo "RCPDEV=$rcpdev" + echo "RCPTTY=$rcptty" + return 0 + fi +} + +validate_rcp_selector() { # selector + case "$1" in + any) return 0;; + *-*) return 0;; # USB DEVICENAME / -, e.g. "1-1", "3-2.1.1" + esac + return 1 +} + +find_rcp() { # selector [include-installable] => $REPLY + local rcpdev= installdev= installhid= + usb_device_foreach _find_rcp_cb "$@" + if [ $? -eq "$RC_BREAK" ] && [ -n "$rcpdev" ]; then + log debug "Found RCP interface $rcpdev for selector '$1'" + REPLY="$rcpdev -" + return 0 + fi + if [ -n "$2" -a -n "$installdev" -a -n "$installhid" ]; then + log debug "Found installable $installhid device $installdev for selector '$1'" + REPLY="$installdev $installhid" + return 0 + fi + return "$RC_NOT_FOUND" +} +_find_rcp_cb() { # selector [include-installable] + # Must match the selector (if any) + case "$1" in + *-*) [ "$1" = "$DEVICENAME" ] || return 0 + esac + + # Check for a usable cdc_acm interface + usb_interface_foreach "" _find_rcp_if_cb + if [ $? -eq "$RC_BREAK" ] && [ -n "$rcpdev" ]; then + # An explicit bus position is the operator saying this device is + # the RCP; only unattended selection needs the product string to + # say so, since many dongles (Home Assistant's ZBT-2 for one) do + # not carry the word OpenThread in theirs. + case "$1" in + *-*) return "$RC_BREAK";; + esac + usb_read_property '' product && REPLY=" $REPLY " && [ "${REPLY/ OpenThread /}" != "$REPLY" ] \ + && return "$RC_BREAK" + rcpdev= + fi + + # If requested, check if we could install RCP firmware on this device + if [ -n "$2" -a -n "$RCPFWHANDLERS" -a -z "$installdev" ]; then + local hid + for hid in $RCPFWHANDLERS; do + call_firmware_handler "$hid" installable "$DEVICENAME" && installdev="$DEVICENAME" installhid="$hid" && break + done + fi +} +_find_rcp_if_cb() { + usb_read_property '' INTERFACE && [ "$REPLY" = 2/2/0 ] || return 0 + usb_read_property '' DRIVER && [ "$REPLY" = cdc_acm ] || return 0 + rcpdev="$DEVICENAME" + return "$RC_BREAK" +} + +update_rcp() { # rcpdev rcptty rcpurl + local rcpdev="$1" rcptty="$2" rcpurl="$3" + [ -n "$RCPFWHANDLERS" ] || return 0 + + # Use uart-exclusive to ensure we don't interfere with a running otbr-agent + log debug "Determining current firmware for USB device $rcpdev ($rcptty)" + local currentfw="$("$AGENT_PROG" -s -d3 --radio-version "${rcpurl}?uart-exclusive" 2>/dev/null)" + if [ -z "$currentfw" ]; then + log notice "Not updating USB device $rcpdev, unable to determine current firmware version" + return 0 + fi + log debug " -> $currentfw" + + local hid rc + for hid in $RCPFWHANDLERS; do + call_firmware_handler "$hid" update "$@" "$currentfw"; rc=$? + if [ "$rc" -eq "$RC_NOT_SUPPORTED" ]; then + continue + elif [ "$rc" -ne 0 ]; then + log error "Failed to update $hid device $rcpdev" + return "$rc" + fi + return 0 + done + log notice "Not updating USB device $1, not supported by any firmware handler" +} + +install_rcp() { # rcpdev hid => $REPLY + local rcpdev="$1" hid="$2" retry + log notice "Attempting to install RCP firmware on $hid device $rcpdev" + if ! call_firmware_handler "$hid" install "$rcpdev"; then + log error "Failed to install firmware on USB device $rcpdev" + return "$RC_INTERNAL" + fi + for retry in 2 1 0; do + find_rcp "$rcpdev" && return 0 + [ "$retry" -gt 0 ] && sleep 1 + done + log error "Failed to find RCP device after firmware install" + return "$RC_INTERNAL" +} + +usb_device_foreach() { # callback ... (with $DEVICENAME) + local _dev DEVICENAME + for _dev in /sys/bus/usb/devices/*; do + DEVICENAME="${_dev##*/}" # e.g. "1-1" + [ "${DEVICENAME%:*}" = "$DEVICENAME" -a -f "$_dev/bDeviceClass" ] || continue + "$@"; [ $? -ne "$RC_BREAK" ] || return "$RC_BREAK" + done +} + +usb_interface_foreach() { # devicename callback ... (with $DEVICENAME) + local _dn="${1:-$DEVICENAME}" _if DEVICENAME; shift + for _if in "/sys/bus/usb/devices/$_dn/$_dn:"*; do + [ -d "$_if" -a -f "$_if/bInterfaceClass" ] || continue + DEVICENAME="${_if##*/}" # e.g. "1-1:1.0" + "$@"; [ $? -ne "$RC_BREAK" ] || return "$RC_BREAK" + done +} + +usb_read_property() { # devicename property => $REPLY + local dev="/sys/bus/usb/devices/${1:-$DEVICENAME}" u v w + REPLY= + case "$2" in + # Provide some processed / composite values that align with hotplug environment variables + DEVPATH) v="$(readlink -f "$dev" 2>/dev/null)" && REPLY="${v#/sys}";; # e.g. "/devices/platform/1e1c0000.xhci/usb1/1-1" + DRIVER) v="$(readlink "$dev/driver" 2>/dev/null)" && REPLY="${v##*/}";; # e.g. "cdc_acm" + INTERFACE) usb_read_property "$1" bInterfaceClass && u="0x0$REPLY" \ + && usb_read_property "$1" bInterfaceSubClass && v="0x0$REPLY" \ + && usb_read_property "$1" bInterfaceProtocol && w="0x0$REPLY" \ + && REPLY="$(printf '%d/%d/%d' "$u" "$v" "$w")";; # e.g. "2/2/0" + TYPE) usb_read_property "$1" bDeviceClass && u="0x0$REPLY" \ + && usb_read_property "$1" bDeviceSubClass && v="0x0$REPLY" \ + && usb_read_property "$1" bDeviceProtocol && w="0x0$REPLY" \ + && REPLY="$(printf '%d/%d/%d' "$u" "$v" "$w")";; # e.g. "239/2/1" + PRODUCT) usb_read_property "$1" idVendor && u="0x0$REPLY" \ + && usb_read_property "$1" idProduct && v="0x0$REPLY" \ + && usb_read_property "$1" bcdDevice && w="0x0$REPLY" \ + && REPLY="$(printf '%x/%x/%x' "$u" "$v" "$w")";; # e.g. "e8d/7612/100" + *) [ -r "$dev/$2" ] && read -r <"$dev/$2" 2>/dev/null;; + esac +} + +usb_find_devnode() { # devicename class [prefix] => $REPLY + local devpath node path + usb_read_property "$1" DEVPATH && devpath="$REPLY" || return $? + for node in "/sys/class/$2/$3"*; do + path="$(readlink "$node" 2>/dev/null)" && [ "${path/$devpath/}" != "$path" ] || continue + REPLY="/dev/${node##*/}" && [ -e "$REPLY" ] && return 0 + done + REPLY= + return "$RC_NOT_FOUND" +} + +usb_wait_devnode() { # devicename class [prefix] => $REPLY + local retry + for retry in 2 1 0; do + usb_find_devnode "$@" && return 0 + [ "$retry" -gt 0 ] && sleep 1 + done + return "$RC_NOT_FOUND" +} + +hotplug_wait() { # callback ... + # Note: This function MUST NOT be called from a sub-shell! + # Create a fifo that acts as a flag file for the otbr-rcp hotplug handler. + # A fifo is used so we can block interruptibly by reading from it. The fifo + # is not actually written to by the hotplug handler, since this would require + # logic to avoid hanging / blocking if this listener goes away. Instead, the + # hotplug handler directly sends an ALRM signal to wake up listeners. + local sig ignored fifo="/var/run/otbr-rcp.hotplug.$$" + trap "rm -f '$fifo' 2>/dev/null" 0 + trap 'sig=1' ALRM + mkfifo -m 0400 "$fifo" || return $? + exec 9<>"$fifo" + while true; do + # reset before polling + sig=; "$@" && break + log debug "Waiting for ALRM signal from otbr-rcp hotplug handler" + read -r ignored <&9 2>/dev/null + done + exec 9>&- + rm "$fifo" + trap '' ALRM # ignore further / later signals + trap - 0 +} + +load_firmware_handlers() { # => $RCPFWHANDLERS + local _script _hname _hid _type + RCPFWHANDLERS= + for _script in /usr/share/openthread-rcp/*.sh; do + [ -f "$_script" -a -r "$_script" ] || continue + _hname="${_script##*/}" + _hid="${_hname%.sh}"; _hid="${_hid//-/_}" + if [ -n "${_hid//[a-z0-9_]}" ]; then + log warning "Not loading firmware handler '$_hname', invalid file name" + continue + fi + log debug "Loading firmware handler '$_hname'" + . "$_script" + _type="$(type "rcp_fw_$_hid")" + if [ "${_type/function}" = "$_type" ]; then + log warning "Ignoring firmware handler '$_hname', missing entry point" + continue + fi + RCPFWHANDLERS="${RCPFWHANDLERS}${RCPFWHANDLERS:+ }${_hid}" + done + [ -n "$RCPFWHANDLERS" ] || log debug "No firmware handlers available" +} + +call_firmware_handler() { # hid args... + local hid="$1"; shift + "rcp_fw_$hid" "$@" +} + +log() { # {debug|info|notice|warning|error|...} message + logger -t otbr-rcp -p "$1" "$2" + ! [ -t 2 ] || echo "$2" >&2 +} + +# Force signal handlers to respect an EXIT (0) trap +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +main "$@" diff --git a/net/openthread-br/files/otbr-rcp.hotplug b/net/openthread-br/files/otbr-rcp.hotplug new file mode 100644 index 0000000000000..abbdbe744573a --- /dev/null +++ b/net/openthread-br/files/otbr-rcp.hotplug @@ -0,0 +1,32 @@ +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ "$ACTION" = bind ]; then + for listener in /var/run/otbr-rcp.hotplug.*; do + pid="${listener#/var/run/otbr-rcp.hotplug.}" + [ "$pid" != "*" ] && kill -ALRM "$pid" + done + + # A newly bound device is the cue for Thread interfaces that failed + # setup for want of an RCP dongle: ifup lifts their restart block. + # Matching on the recorded error keeps this away from interfaces an + # administrator took down on purpose, and interfaces that are up + # keep their running agent. + for iface in $(ubus call network.interface dump 2>/dev/null \ + | jsonfilter -e '@.interface[@.proto="openthread"&&@.up=false].interface'); do + ubus call network.interface."$iface" status 2>/dev/null \ + | jsonfilter -q -e '@.errors[@.code="RCP_NOT_FOUND"]' >/dev/null \ + && ifup "$iface" + done +fi