openthread-br: new package 19218/head
authorStijn Tintel <stijn@linux-ipv6.be>
Sun, 21 Aug 2022 17:16:02 +0000 (20:16 +0300)
committerStijn Tintel <stijn@linux-ipv6.be>
Mon, 7 Aug 2023 20:13:51 +0000 (23:13 +0300)
Add a new package for the OpenThread Border Router. Comes with a netifd
protocol handler. See README.md for more information.

Signed-off-by: Stijn Tintel <stijn@linux-ipv6.be>
net/openthread-br/Makefile [new file with mode: 0644]
net/openthread-br/README.md [new file with mode: 0644]
net/openthread-br/files/openthread-proto.sh [new file with mode: 0644]
net/openthread-br/patches/100-rest-support-deleting-the-dataset.patch [new file with mode: 0644]

diff --git a/net/openthread-br/Makefile b/net/openthread-br/Makefile
new file mode 100644 (file)
index 0000000..1b42cce
--- /dev/null
@@ -0,0 +1,107 @@
+# SPDX-FileCopyrightText: 2022-2023 Stijn Tintel <stijn@linux-ipv6.be>
+# SPDX-License-Identifier: GPL-2.0-only
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=openthread-br
+PKG_SOURCE_DATE:=2023-08-01
+PKG_SOURCE_VERSION:=1738d8cd8b42106c2ef1262fbbac2f06beab83ba
+PKG_RELEASE:=1
+
+PKG_SOURCE_PROTO:=git
+PKG_SOURCE_URL=https://github.com/openthread/ot-br-posix.git
+PKG_MIRROR_HASH:=7eb740d1a0663aae7969940f8a8b06879524dd62ae7842f46a160cae54ee8417
+
+PKG_MAINTAINER:=Stijn Tintel <stijn@linux-ipv6.be>
+PKG_LICENSE:=BSD-3-Clause
+PKG_LICENSE_FILES:=LICENSE
+
+include $(INCLUDE_DIR)/package.mk
+include $(INCLUDE_DIR)/cmake.mk
+
+define Package/luci-app-openthread
+  CATEGORY:=LuCI
+  SECTION:=luci
+  SUBMENU:=3. Applications
+  TITLE:=LuCI Support for OpenThread Border Router
+  DEPENDS:=+luci-base
+endef
+
+define Package/openthread-br
+  CATEGORY:=Network
+  SECTION:=net
+  TITLE:=OpenThread Border Router
+  DEPENDS:= \
+       +libblobmsg-json \
+       +libjson-c \
+       +libncurses \
+       +libnetfilter-queue \
+       +libreadline \
+       +libstdcpp \
+       +libubox \
+       +libubus \
+       +mdnsd \
+       +mdnsresponder
+endef
+
+define Package/openthread-br/description
+  A Thread border router for POSIX-based platforms.
+endef
+
+define Package/openthread-br/conffiles
+/var/lib/thread
+endef
+
+CMAKE_OPTIONS += \
+       -DOT_BORDER_ROUTER:BOOL=ON \
+       -DOT_BORDER_ROUTING_NAT64:BOOL=ON \
+       -DOT_CHANNEL_MANAGER:BOOL=ON \
+       -DOT_CHANNEL_MONITOR:BOOL=ON \
+       -DOT_COMMISSIONER:BOOL=ON \
+       -DOT_ECDSA:BOOL=ON \
+       -DOT_FIREWALL:BOOL=OFF \
+       -DOT_SERVICE:BOOL=ON \
+       -DOT_SRP_CLIENT:BOOL=ON \
+       -DOT_SRP_SERVER:BOOL=ON \
+       -DOTBR_BACKBONE_ROUTER:BOOL=ON \
+       -DOTBR_BORDER_ROUTING:BOOL=ON \
+       -DOTBR_DNSSD_DISCOVERY_PROXY:BOOL=ON \
+       -DOTBR_DUA_ROUTING:BOOL=ON \
+       -DOTBR_MDNS=mDNSResponder \
+       -DOTBR_OPENWRT:BOOL=ON \
+       -DOTBR_REST:BOOL=ON \
+       -DOTBR_SRP_ADVERTISING_PROXY:BOOL=ON \
+       -DOTBR_SRP_SERVER_AUTO_ENABLE:BOOL=ON \
+       -DOTBR_TREL:BOOL=ON
+
+TARGET_CFLAGS += -DOPENTHREAD_POSIX_CONFIG_DAEMON_SOCKET_BASENAME=\\\"/var/run/openthread-%s\\\"
+
+define Package/luci-app-openthread/install
+       $(INSTALL_DIR) \
+               $(1)/usr/lib/lua/luci/controller/admin \
+               $(1)/usr/lib/lua/luci/view/admin_thread \
+               $(1)/www/luci-static/resources
+       $(INSTALL_DATA) \
+               $(PKG_BUILD_DIR)/src/openwrt/controller/thread.lua \
+               $(1)/usr/lib/lua/luci/controller/admin
+       $(INSTALL_DATA) \
+               $(PKG_BUILD_DIR)/src/openwrt/view/admin_thread/* \
+               $(1)/usr/lib/lua/luci/view/admin_thread
+       $(INSTALL_DATA) \
+               $(PKG_BUILD_DIR)/src/openwrt/handle_error.js \
+               $(1)/www/luci-static/resources
+endef
+
+define Package/openthread-br/install
+       $(INSTALL_DIR) \
+               $(1)/etc/init.d \
+               $(1)/lib/netifd/proto \
+               $(1)/usr/sbin \
+               $(1)/var/lib/thread
+       $(INSTALL_BIN) ./files/openthread-proto.sh $(1)/lib/netifd/proto/openthread.sh
+       $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/* $(1)/usr/sbin
+endef
+
+
+$(eval $(call BuildPackage,luci-app-openthread))
+$(eval $(call BuildPackage,openthread-br))
diff --git a/net/openthread-br/README.md b/net/openthread-br/README.md
new file mode 100644 (file)
index 0000000..e4a26d4
--- /dev/null
@@ -0,0 +1,222 @@
+# OpenThread Border Router
+
+This package contains the OpenThread Border Router.
+
+## Requirements
+
+To use this package, you need a Thread Radio Co-Processor (RCP). Testing of
+this package was done with Nordic Semiconductor nRF52840 USB dongles.
+
+Building and flashing the dongle with the Thread RCP firmware is out of scope
+of this document.
+
+One caveat for this dongle is worth mentioning here. The nRF52840 USB dongle
+seems to come with the U2F bootloader. To get it in mass storage mode to copy a
+firmware file, you need to plug it in while pressing the reset button. However,
+after the initial flash with the ot-rcp firmware, this method stops working.
+Instead, you need to double press the reset button after plugging in the dongle.
+
+## Packaging decisions
+
+### Configurable package build
+
+OpenThread is complex software. Adding config options to change the build of
+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.
+
+### Firewall support
+
+OpenWrt uses firewall4 with nftables by default, but the OpenThread firewall
+implementation uses IPTables and IPset. While we still support firewall3 with
+IPTables, it's not a good idea to add new dependencies to old things.
+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.
+
+### REST Server
+
+The REST server is enabled to make this package compatible with Home Assistant.
+
+### TREL support
+
+Thread Radio Encapsulation Link support is enabled, as it allows Border Routers
+to communicate over other links (e.g. Ethernet), reducing traffic over the
+802.15.4 radios.
+
+The following Github discussion contains a good explanation of TREL:
+https://github.com/openthread/openthread/discussions/8478
+
+### UCI/netifd support
+
+The package contains a minimal netifd protocol handler. This allows configuring
+the Thread network in /etc/config/network. The agent will be started by netifd,
+rather than using an init script.
+
+OpenThread does not store prefix information in non-volatile storage. As a
+result, every time the agent is restarted, a different prefix would be used.
+This is not very nice, and makes it very difficult to run the OpenThread Border
+Router on a device that is not your main router. Therefore, prefixes can be
+configured in /etc/config/network. This way, you can add a static route to the
+Thread prefix(es) in your main router, making it possible to access devices on
+the Thread network from your entire network.
+
+## Create network
+
+When starting the OpenThread Border Router for the first time, a Thread network
+must be created.
+
+As the agent is started by netifd, we first need to create an interface in
+/etc/config/network:
+
+```
+config interface 'thread'
+        option device 'wpan0'
+        option proto 'openthread'
+        option backbone_network 'lan'
+        option radio_url 'spinel+hdlc+uart:///dev/ttyACM0?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:
+
+```
+# ifup thread
+# ifstatus thread
+{
+        "up": false,
+        "pending": false,
+        "available": true,
+        "autostart": false,
+        "dynamic": false,
+        "proto": "openthread",
+        "data": {
+
+        },
+        "errors": [
+                {
+                        "subsystem": "openthread",
+                        "code": "MISSING_BACKBONE_NETWORK"
+                }
+        ]
+}
+```
+
+In the above example, the backbone_network option is missing.
+
+The protocol handler will automatically start the the Thread network, so we
+need to bring it down for the initial setup. This only needs to be done once.
+
+```
+ubus call otbr threadstop
+```
+
+### LuCI
+
+Creating a network in LuCI appears to be broken for the moment.
+
+### CLI
+
+```
+ot-ctl dataset init new
+ot-ctl dataset panid 0x12ab
+ot-ctl dataset extpanid 12ab12ab12ab12ab
+ot-ctl dataset networkname OpenWrThread
+ot-ctl dataset networkkey ddf429af1c52d1735ffaf36fae343ee8
+ot-ctl dataset commit active
+ot-ctl ifconfig up
+ot-ctl thread start
+ot-ctl netdata register
+```
+
+### Configure route
+
+Before you can join a device to your new Thread network, you must add a route
+to the Thread prefix on the commissioner device via the OpenWrt router running
+the OpenThread Border Router.
+
+Get the prefix:
+```
+ot-ctl prefix
+```
+
+Example output:
+
+```
+fd6b:a92f:c531:1::/64 paros low f000
+Done
+```
+
+Configuring the route is out of scope of this document, but it must be done, or
+joining Thread devices will fail.
+
+### Get hex-encoded operational dataset TLV
+
+This is needed to join devices to the Thread Network.
+
+```
+ot-ctl dataset active -x
+```
+
+Example output:
+
+```
+0e080000000000010000000300001035060004001fffe00708fd488c6a892ec30c04106e220c964a14a7e10e9004691920ec390c0402a0f7f80102ffff030b5468726541646c6576696f0208ffffffffffffffff0510ddf429af1c52d1735ffaf36fae343ee8
+```
+
+## Join another OpenThread Border Router
+
+Simply configure the active dataset in /etc/config/network:
+
+```
+config interface 'thread'
+        option device 'wpan0'
+        option proto 'openthread'
+        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 verbose '0'
+```
+
+Afterwards, bring up the interface:
+
+```
+ifup thread
+```
+
+## Join a Thread device via Matter
+
+### ESP32
+The following procedure has been tested with an ESP32-C6 using [the Matter
+lighting-app example](https://github.com/project-chip/connectedhomeip/tree/master/examples/lighting-app/esp32).
+Building and flashing that app is out of scope of this document.
+
+During startup, the lighting app will print the SetupQRCode to the serial
+console:
+
+```
+I (1614) chip[SVR]: SetupQRCode: [MT:Y.K9042C00KA0648G00]
+I (1624) chip[SVR]: Copy/paste the below URL in a browser to see the QR Code:
+I (1634) chip[SVR]: https://project-chip.github.io/connectedhomeip/qrcode.html?data=MT%3AY.K9042C00KA0648G00
+I (1644) chip[SVR]: Manual pairing code: [34970112332]
+```
+
+Decide on a node ID for the device.
+
+```
+./chip-tool pairing code-thread 0x65737933320000 hex:0e080000000000010000000300001035060004001fffe00708fd488c6a892ec30c04106e220c964a14a7e10e9004691920ec390c0402a0f7f80102ffff030b5468726541646c6576696f0208ffffffffffffffff0510ddf429af1c52d1735ffaf36fae343ee8 MT:Y.K9042C00KA0648G00 --paa-trust-store-path /path/to/connectedhomeip/credentials/test/attestation/
+```
+
diff --git a/net/openthread-br/files/openthread-proto.sh b/net/openthread-br/files/openthread-proto.sh
new file mode 100644 (file)
index 0000000..928bafe
--- /dev/null
@@ -0,0 +1,106 @@
+#!/bin/sh
+#
+# SPDX-FileCopyrightText: 2023 Stijn Tintel <stijn@linux-ipv6.be>
+# SPDX-License-Identifier: GPL-2.0-only
+
+OTCTL="/usr/sbin/ot-ctl"
+PROG="/usr/sbin/otbr-agent"
+
+[ -x "$PROG" ] || exit 0
+
+[ -n "$INCLUDE_ONLY" ] || {
+       . /lib/functions.sh
+       . /lib/functions/network.sh
+       . ../netifd-proto.sh
+       init_proto "$@"
+}
+
+proto_openthread_add_prefix() {
+       prefix="$1"
+       # shellcheck disable=SC2086
+       [ -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
+
+       available=1
+       no_device=1
+}
+
+proto_openthread_setup_error() {
+       interface="$1"
+       error="$2"
+       proto_notify_error "$interface" "$error"
+       # prevent netifd from trying to bring up interface over and over
+       proto_block_restart "$interface"
+       proto_setup_failed "$interface"
+       exit 1
+}
+
+proto_openthread_setup() {
+       interface="$1"
+       device="$2"
+
+       json_get_vars backbone_network dataset device radio_url 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
+
+       opts="--auto-attach=0"
+       [ "$verbose" -eq 0 ] || append opts -v
+       append opts "-I$device"
+       append opts "-B$backbone_ifname"
+       append opts "$radio_url"
+       append opts "trel://$backbone_ifname"
+       # run in subshell to prevent wiping json data needed for prefixes
+       ( proto_run_command "$interface" "$PROG" $opts )
+
+       ubus -t30 wait_for otbr
+
+       [ -n "$dataset" ] && {
+               $OTCTL dataset set active "$dataset"
+       }
+
+       json_for_each_item proto_openthread_add_prefix prefix
+       ubus call otbr threadstart || proto_openthread_setup_error "$interface" MISSING_UBUS_OBJ
+       $OTCTL netdata register
+
+       proto_init_update "$device" 1 1
+       proto_send_update "$interface"
+}
+
+proto_openthread_teardown() {
+       interface="$1"
+       ubus call otbr threadstop
+       proto_kill_command "$interface"
+}
+
+[ -n "$INCLUDE_ONLY" ] || {
+       add_protocol openthread
+}
diff --git a/net/openthread-br/patches/100-rest-support-deleting-the-dataset.patch b/net/openthread-br/patches/100-rest-support-deleting-the-dataset.patch
new file mode 100644 (file)
index 0000000..014c306
--- /dev/null
@@ -0,0 +1,124 @@
+From d9086b843d5da519fca876794d14026b14cc68ae Mon Sep 17 00:00:00 2001
+Message-ID: <d9086b843d5da519fca876794d14026b14cc68ae.1689665371.git.stefan@agner.ch>
+From: Stefan Agner <stefan@agner.ch>
+Date: Mon, 5 Jun 2023 23:41:50 +0200
+Subject: [PATCH] [rest] support deleting the dataset
+
+Add REST API to support deleting the active or pending operational
+dataset. Deleting the active operational dataset requires the Thread
+network to be disabled (just like modifying the active operational
+dataset). Subsequent use of the PUT method allows to build entirly
+new datasets with values generated by the stack (through
+otDatasetCreateNewNetwork).
+---
+ src/rest/openapi.yaml | 21 +++++++++++++++++++++
+ src/rest/resource.cpp | 35 +++++++++++++++++++++++++++++++++++
+ src/rest/resource.hpp |  1 +
+ 3 files changed, 57 insertions(+)
+
+diff --git a/src/rest/openapi.yaml b/src/rest/openapi.yaml
+index 2ba2a4dd56..2edc4af29a 100644
+--- a/src/rest/openapi.yaml
++++ b/src/rest/openapi.yaml
+@@ -248,6 +248,18 @@ paths:
+           description: Invalid request body.
+         "409":
+           description: Writing active operational dataset rejected because Thread network is active.
++    delete:
++      tags:
++        - node
++      summary: Deletes the active operational dataset
++      description: |-
++        Deletes the the active operational dataset on the current node. Only allowed if the Thread node
++        is inactive.
++      responses:
++        "200":
++          description: Successfully deleted the active operational dataset.
++        "409":
++          description: Deleting active operational dataset rejected because Thread network is active.
+   /node/dataset/pending:
+     get:
+       tags:
+@@ -291,6 +303,15 @@ paths:
+           description: Successfully created the pending operational dataset.
+         "400":
+           description: Invalid request body.
++    delete:
++      tags:
++        - node
++      summary: Deletes the pending operational dataset
++      description: |-
++        Deletes the the pending operational dataset on the current node.
++      responses:
++        "200":
++          description: Successfully deleted the active operational dataset.
+ components:
+   schemas:
+     LeaderData:
+diff --git a/src/rest/resource.cpp b/src/rest/resource.cpp
+index a60e9d9483..829835341a 100644
+--- a/src/rest/resource.cpp
++++ b/src/rest/resource.cpp
+@@ -767,12 +767,47 @@ exit:
+     }
+ }
++void Resource::DeleteDataset(DatasetType aDatasetType, Response &aResponse) const
++{
++    otbrError                error       = OTBR_ERROR_NONE;
++    std::string              errorCode   = GetHttpStatus(HttpStatusCode::kStatusOk);
++    otOperationalDatasetTlvs datasetTlvs = {};
++
++    if (aDatasetType == DatasetType::kActive)
++    {
++        VerifyOrExit(otThreadGetDeviceRole(mInstance) == OT_DEVICE_ROLE_DISABLED, error = OTBR_ERROR_INVALID_STATE);
++    }
++
++    if (aDatasetType == DatasetType::kActive)
++    {
++        VerifyOrExit(otDatasetSetActiveTlvs(mInstance, &datasetTlvs) == OT_ERROR_NONE, error = OTBR_ERROR_REST);
++    }
++    else if (aDatasetType == DatasetType::kPending)
++    {
++        VerifyOrExit(otDatasetSetPendingTlvs(mInstance, &datasetTlvs) == OT_ERROR_NONE, error = OTBR_ERROR_REST);
++    }
++    aResponse.SetResponsCode(errorCode);
++
++exit:
++    if (error == OTBR_ERROR_INVALID_STATE)
++    {
++        ErrorHandler(aResponse, HttpStatusCode::kStatusConflict);
++    }
++    else if (error != OTBR_ERROR_NONE)
++    {
++        ErrorHandler(aResponse, HttpStatusCode::kStatusInternalServerError);
++    }
++}
++
+ void Resource::Dataset(DatasetType aDatasetType, const Request &aRequest, Response &aResponse) const
+ {
+     std::string errorCode;
+     switch (aRequest.GetMethod())
+     {
++    case HttpMethod::kDelete:
++        DeleteDataset(aDatasetType, aResponse);
++        break;
+     case HttpMethod::kGet:
+         GetDataset(aDatasetType, aRequest, aResponse);
+         break;
+diff --git a/src/rest/resource.hpp b/src/rest/resource.hpp
+index d79085dbfc..362e501471 100644
+--- a/src/rest/resource.hpp
++++ b/src/rest/resource.hpp
+@@ -150,6 +150,7 @@ private:
+     void GetDataRloc(Response &aResponse) const;
+     void GetDataset(DatasetType aDatasetType, const Request &aRequest, Response &aResponse) const;
+     void SetDataset(DatasetType aDatasetType, const Request &aRequest, Response &aResponse) const;
++    void DeleteDataset(DatasetType aDatasetType, Response &aResponse) const;
+     void DeleteOutDatedDiagnostic(void);
+     void UpdateDiag(std::string aKey, std::vector<otNetworkDiagTlv> &aDiag);
+-- 
+2.41.0
+