Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions net/openthread-br/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
include $(TOPDIR)/rules.mk

PKG_NAME:=openthread-br
PKG_SOURCE_DATE:=2025-06-12
PKG_SOURCE_VERSION:=2f3c799c7463c8f674754e65c53f78bc0bbcbd58
PKG_VERSION:=2026.08.0
PKG_SOURCE_VERSION:=v$(PKG_VERSION)
PKG_RELEASE:=2

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 <stijn@linux-ipv6.be>
PKG_LICENSE:=BSD-3-Clause
Expand Down Expand Up @@ -41,9 +41,7 @@ define Package/openthread-br
+libreadline \
+libstdcpp \
+libubox \
+libubus \
+mdnsd \
+mdnsresponder
+libubus
Comment thread
LorbusChris marked this conversation as resolved.
endef

define Package/openthread-br/description
Expand All @@ -63,15 +61,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_MDNS=openthread \
-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\\\"

Expand All @@ -93,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

Expand Down
110 changes: 99 additions & 11 deletions net/openthread-br/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<vendor> <product>`, 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
Expand All @@ -35,17 +59,36 @@ 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
### 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 `<hostname>.local`.

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.
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

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

Expand Down Expand Up @@ -83,14 +126,58 @@ 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'
```

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 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.

### 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:

```
# ifup thread
Expand Down Expand Up @@ -187,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'
```

Expand Down
73 changes: 55 additions & 18 deletions net/openthread-br/files/openthread-proto.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

OTCTL="/usr/sbin/ot-ctl"
PROG="/usr/sbin/otbr-agent"
RCP_PROG="/usr/sbin/otbr-rcp"

[ -x "$PROG" ] || exit 0

Expand All @@ -21,25 +22,18 @@ 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
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 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

available=1
no_device=1
Expand All @@ -55,29 +49,72 @@ 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 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"
network_get_device backbone_ifname "$backbone_network"

[ -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

# run in subshell to prevent wiping json data needed for prefixes
( proto_openthread_check_service mdnsd ) || proto_openthread_setup_error "$interface" MISSING_SVC_MDNSD
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"
Comment thread
LorbusChris marked this conversation as resolved.
fi

opts="--auto-attach=0"
[ "$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
Expand Down
Loading