From 9f48bef67e527a9139806262afa572ce8dacaee4 Mon Sep 17 00:00:00 2001 From: Francois Michel Date: Fri, 17 Apr 2026 09:39:44 -0700 Subject: [PATCH 1/7] Add small tutorial to run OTNS with OT-BR and Matter accessories. The README contains a tutorial. The Dockerfile currently uses a fork of OT-NS and a fork of Matter and applies a patch to these forks as well. Once these forks and patches get intergrated into the mainline repos, we will update this example accordingly. --- examples/otbr-and-matter/Dockerfile | 408 +++++++++++++++++++++++++ examples/otbr-and-matter/README.md | 169 ++++++++++ examples/otbr-and-matter/run-docker.sh | 86 ++++++ 3 files changed, 663 insertions(+) create mode 100644 examples/otbr-and-matter/Dockerfile create mode 100644 examples/otbr-and-matter/README.md create mode 100755 examples/otbr-and-matter/run-docker.sh diff --git a/examples/otbr-and-matter/Dockerfile b/examples/otbr-and-matter/Dockerfile new file mode 100644 index 00000000..ee17807a --- /dev/null +++ b/examples/otbr-and-matter/Dockerfile @@ -0,0 +1,408 @@ +# Dockerfile for OTNS + OT-BR + Matter all-clusters-app development environment +# +# Works on both arm64 and x86_64/amd64 hosts + +FROM ubuntu:24.04 + +ARG TARGETARCH +ARG GO_VERSION=1.26.2 +ARG OTNS_BRANCH=pr-br +ARG OTNS_REPO=https://github.com/EskoDijk/ot-ns +ARG MATTER_BRANCH=openthread +ARG MATTER_REPO=https://github.com/gmarcosb/connectedhomeip + +ENV DEBIAN_FRONTEND=noninteractive +ENV HOME=/home/otns +ENV GOPATH=${HOME}/go +ENV PATH=${GOPATH}/bin:/usr/local/go/bin:${PATH} + +SHELL ["/bin/bash", "-c"] + +# --------------------------------------------------------------- +# Stage 1: System packages +# --------------------------------------------------------------- +RUN apt-get clean && apt-get update && apt-get install -y \ + sudo \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user (after sudo is installed so /etc/sudoers exists cleanly) +RUN useradd -m -s /bin/bash -d ${HOME} otns && \ + echo "otns ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + +RUN apt-get update && apt-get install -y \ + git \ + build-essential \ + g++ \ + cmake \ + ninja-build \ + patch \ + wget \ + curl \ + unzip \ + xdg-utils \ + python3 \ + python3-pip \ + python3-venv \ + pkg-config \ + libssl-dev \ + libdbus-1-dev \ + dbus \ + libavahi-client-dev \ + libgirepository1.0-dev \ + libglib2.0-dev \ + libreadline-dev \ + libncurses-dev \ + libprotobuf-dev \ + protobuf-compiler \ + libjsoncpp-dev \ + libgtest-dev \ + libgmock-dev \ + libnetfilter-queue1 \ + libnetfilter-queue-dev \ + iptables \ + ipset \ + bind9 \ + rsyslog \ + nodejs \ + npm \ + lsb-release \ + vim \ + nano \ + iproute2 \ + iputils-ping \ + net-tools \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# --------------------------------------------------------------- +# Stage 2: Install Go (architecture-aware) +# --------------------------------------------------------------- +RUN wget -q https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz -O /tmp/go.tar.gz && \ + tar -C /usr/local -xzf /tmp/go.tar.gz && \ + rm /tmp/go.tar.gz + +# --------------------------------------------------------------- +# Stage 2.5: Install grpcwebproxy for arm64 if we're running on +# arm64 architecrure +# --------------------------------------------------------------- +# The OTNS install-deps script downloads an x86_64 grpcwebproxy which +# won't work on arm64. Pre-install the arm64 build so install-deps +# finds it already present and skips its own download. +RUN mkdir -p ${GOPATH}/bin && \ + if [ "${TARGETARCH}" = "arm64" ]; then \ + wget -q https://github.com/improbable-eng/grpc-web/releases/download/v0.14.0/grpcwebproxy-v0.14.0-arm64.zip \ + -O /tmp/grpcwebproxy.zip && \ + cd /tmp && unzip grpcwebproxy.zip && \ + mv dist/grpcwebproxy* ${GOPATH}/bin/grpcwebproxy && \ + chmod +x ${GOPATH}/bin/grpcwebproxy && \ + rm -rf /tmp/grpcwebproxy.zip /tmp/dist; \ + fi +RUN chown -R otns:otns ${GOPATH} + +# --------------------------------------------------------------- +# Stage 3: Clone and build OTNS (Esko's branch with BR support) +# --------------------------------------------------------------- +USER otns +WORKDIR ${HOME} + +RUN git clone --branch ${OTNS_BRANCH} ${OTNS_REPO} ot-ns + +WORKDIR ${HOME}/ot-ns +RUN git submodule update --init --recursive + +# Patch 1: Matter nodes may respond with "down" or "up" to ifconfig, like OTBR +RUN sed -i 's/if node.cfg.IsRcp && node.cfg.IsBorderRouter {/if node.cfg.IsRcp \&\& node.cfg.IsBorderRouter || node.cfg.Type == MATTER {/' simulation/node.go + +# Patch 2: Drain Matter app stdout (CHIP logs) to prevent pipe blocking and write to node log +RUN sed -i '/go node.lineReaderStdOut(node.pipeOut)/a\\t\t} else if cfg.Type == MATTER {\n\t\t\tgo node.lineReaderStdErr(node.pipeOut) \/\/ drain Matter stdout (CHIP logs) and write to node log' simulation/node.go + +# Install OTNS dependencies and OTNS itself +RUN ./script/install-deps && ./script/install + +# Make otns and grpcwebproxy available system-wide (needed for sudo) +RUN sudo ln -sf ${GOPATH}/bin/otns /usr/local/bin/otns && \ + sudo ln -sf ${GOPATH}/bin/grpcwebproxy /usr/local/bin/grpcwebproxy + +# --------------------------------------------------------------- +# Stage 4: Clone and build Matter connectedhomeip (OTNS-enabled) +# --------------------------------------------------------------- +WORKDIR ${HOME} + +RUN git clone ${MATTER_REPO} connectedhomeip && \ + cd connectedhomeip && \ + git checkout ${MATTER_BRANCH} + +WORKDIR ${HOME}/connectedhomeip + +# Add ot-ns as a submodule pointing to Esko's branch, then check it out +RUN mkdir -p ./third_party/ot-ns && \ + git submodule add -b ${OTNS_BRANCH} ${OTNS_REPO} third_party/ot-ns/repo && \ + git submodule update --init third_party/ot-ns/repo + +# Checkout submodules (includes openthread, etc.) +# Enable long paths to avoid failures with deeply nested submodules +RUN git config --global core.longpaths true && \ + git submodule update --init && \ + python3 scripts/checkout_submodules.py --shallow --platform linux + +# Apply ot-rfsim build fixes needed for the Matter accessory +RUN cd third_party/ot-ns/repo && git apply - <<'PATCH' +diff --git a/ot-rfsim/src/ble.c b/ot-rfsim/src/ble.c +index 2a7f87f..7fdf05b 100644 +--- a/ot-rfsim/src/ble.c ++++ b/ot-rfsim/src/ble.c +@@ -25,7 +25,7 @@ + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +- ++#include "platform-rfsim.h" + #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE + + #include "platform-rfsim.h" +diff --git a/ot-rfsim/src/openthread-core-rfsim-config.h b/ot-rfsim/src/openthread-core-rfsim-config.h +index acb7cf4..293029c 100644 +--- a/ot-rfsim/src/openthread-core-rfsim-config.h ++++ b/ot-rfsim/src/openthread-core-rfsim-config.h +@@ -348,4 +348,19 @@ + #define OPENTHREAD_SIMULATION_VIRTUAL_TIME 1 + #endif + ++#ifndef OPENTHREAD_CONFIG_ECDSA_ENABLE ++#define OPENTHREAD_CONFIG_ECDSA_ENABLE 1 ++#endif ++ ++#ifndef OPENTHREAD_CONFIG_BLE_TCAT_ENABLE ++#define OPENTHREAD_CONFIG_BLE_TCAT_ENABLE 0 ++#endif ++ ++#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE ++#define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 0 ++#endif ++ ++#ifndef OPENTHREAD_SIMULATION_VIRTUAL_TIME_UART ++#define OPENTHREAD_SIMULATION_VIRTUAL_TIME_UART 1 ++#endif + #endif // OPENTHREAD_CORE_RFSIM_CONFIG_H_ +diff --git a/ot-rfsim/src/platform-rfsim.c b/ot-rfsim/src/platform-rfsim.c +index b98bac8..9059292 100644 +--- a/ot-rfsim/src/platform-rfsim.c ++++ b/ot-rfsim/src/platform-rfsim.c +@@ -55,8 +55,9 @@ + + #define VERIFY_EVENT_SIZE(X) OT_ASSERT((payloadLen >= sizeof(X)) && "received event payload too small"); + ++#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE + static const otIp6Address kUnspecifiedIp6Address = {.mFields = {.m32 = {0, 0, 0, 0}}}; +- ++#endif + extern int gSockFd; + + uint64_t gLastMsgId = 0; +@@ -271,13 +272,13 @@ void handleUdpForwarding(otMessage *aMessage, + size_t msgLen = otMessageGetLength(aMessage); + + OT_ASSERT(msgLen <= sizeof(buf)); +- ++ + evData.mSrcPort = aSockPort; + evData.mDstPort = aPeerPort; + memcpy(evData.mSrcIp6, &kUnspecifiedIp6Address, OT_IP6_ADDRESS_SIZE); + memcpy(evData.mDstIp6, aPeerAddr, OT_IP6_ADDRESS_SIZE); + otMessageRead(aMessage, 0, buf, msgLen); +- ++ (void) kUnspecifiedIp6Address; + otSimSendMsgToHostEvent(OT_SIM_EVENT_UDP_TO_HOST, &evData, &buf[0], msgLen); + } + +diff --git a/ot-rfsim/src/platform-rfsim.cpp b/ot-rfsim/src/platform-rfsim.cpp +index a88c6d4..579f417 100644 +--- a/ot-rfsim/src/platform-rfsim.cpp ++++ b/ot-rfsim/src/platform-rfsim.cpp +@@ -32,6 +32,7 @@ + * This file includes the C++ portions of the OT-RFSIM platform. + */ + ++#include "platform-rfsim.h" + #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE + + #include "net/ip6.hpp" +diff --git a/ot-rfsim/src/uart.c b/ot-rfsim/src/uart.c +index 8ef034a..d5589ae 100644 +--- a/ot-rfsim/src/uart.c ++++ b/ot-rfsim/src/uart.c +@@ -37,7 +37,6 @@ + #include "event-sim.h" + #include "utils/code_utils.h" + #include "utils/uart.h" +- + #if OPENTHREAD_SIMULATION_VIRTUAL_TIME_UART == 0 + + #include +PATCH + +# Apply Matter build fixes using sed (inline git patches break in Docker heredocs): + +# 1) Move otAppCliInit before Matter ThreadStack init so Matter's SRP callback +# is not overwritten by the CLI SRP client module +RUN git apply - <<'PATCH' +diff --git a/examples/platform/linux/AppMain.cpp b/examples/platform/linux/AppMain.cpp +--- a/examples/platform/linux/AppMain.cpp ++++ b/examples/platform/linux/AppMain.cpp +@@ -753,7 +753,9 @@ + otSysInit(static_cast(args.size()), args.data()); ++#if CHIP_ENABLE_OTNS ++ // Init CLI before Matter ThreadStack so Matter's SRP callback ++ // is not overwritten by the CLI SRP client module. ++ otAppCliInit(otInstanceGetSingle()); ++#endif + SuccessOrExit(err = DeviceLayer::ThreadStackMgrImpl().InitThreadStack()); + SuccessOrExit(err = DeviceLayer::ThreadStackMgrImpl().StartThreadTask()); +-#if CHIP_ENABLE_OTNS +- otAppCliInit(otInstanceGetSingle()); +-#endif + ChipLogProgress(NotSpecified, "Thread initialized."); +PATCH + +# 2) Enable OTNS status push, virtual-time UART define, and verbose CHIP logging +RUN sed -i "/chip_enable_otns=true/a\ self.extra_gn_options.append('openthread_config_otns_enable=true')" scripts/build/builders/host.py && \ + sed -i "/openthread_config_otns_enable=true/a\ self.extra_gn_options.append('chip_detail_logging=true')" scripts/build/builders/host.py && \ + sed -i "/chip_detail_logging=true/a\ self.extra_gn_options.append('chip_progress_logging=true')" scripts/build/builders/host.py && \ + sed -i "/chip_progress_logging=true/a\ self.extra_gn_options.append('chip_automation_logging=true')" scripts/build/builders/host.py + +# 3) Add OPENTHREAD_SIMULATION_VIRTUAL_TIME_UART=1 define to OTNS platform's BUILD.gn +RUN sed -i '/include_dirs = \[ "\${chip_root}\/third_party\/ot-ns\/repo\/ot-rfsim\/src" \]/a\ defines = [ "OPENTHREAD_SIMULATION_VIRTUAL_TIME_UART=1" ]' third_party/openthread/platforms/otns/BUILD.gn + +# Bootstrap the Matter build environment and build the all-clusters app +# The build target adapts to the host architecture +RUN source ./scripts/activate.sh && \ + if [ "${TARGETARCH}" = "arm64" ]; then \ + ARCH="arm64"; \ + else \ + ARCH="x64"; \ + fi && \ + ./scripts/build/build_examples.py \ + --target linux-${ARCH}-all-clusters-no-wifi-no-ble-openthread-endpoint-otns \ + build + +# Build ot-rfsim node binaries (FTD, MTD, BR, RCP) +WORKDIR ${HOME}/ot-ns/ot-rfsim +RUN ./script/build_latest +RUN ./script/build_br +RUN ./script/build_rcp + +# Create convenience symlinks expected by OTNS +WORKDIR ${HOME}/ot-ns +RUN ln -sf ./ot-rfsim/ot-versions/ot-cli-ftd ./ && \ + ln -sf ./ot-rfsim/ot-versions/ot-cli-ftd_br ./ + +# Symlink the Matter all-clusters binary as ot-matter-node so OTNS can find it +RUN MATTER_BIN=$(ls ${HOME}/connectedhomeip/out/linux-*-all-clusters-no-wifi-no-ble-openthread-endpoint-otns/chip-all-clusters-app 2>/dev/null | head -1) && \ + if [ -n "$MATTER_BIN" ]; then \ + ln -sf "$MATTER_BIN" ./ot-rfsim/ot-versions/ot-matter-node; \ + else \ + echo "WARNING: Matter binary not found, ot-matter-node symlink not created"; \ + fi + +# --------------------------------------------------------------- +# Stage 5: OT-BR-POSIX (build during image creation) +# --------------------------------------------------------------- +# OT-BR-POSIX is configured with: +# - Web GUI for browser-based management +# - Border Routing for OMR prefix advertisement +# - SRP server for Thread service registration in network data +# - NAT64 for IPv4 connectivity from Thread devices +# - mDNSResponder instead of avahi for DNS-SD +WORKDIR ${HOME} + +RUN git clone https://github.com/openthread/ot-br-posix && \ + cd ot-br-posix && \ + git submodule update --init --recursive + +# Bootstrap installs apt packages AND builds/installs mDNSResponder from source +USER root + +# Bootstrap installs apt packages AND builds/installs mDNSResponder from source +RUN HOME=/root && cd /home/otns/ot-br-posix && \ + INFRA_IF_NAME=eth0 WEB_GUI=1 NAT64=1 OTBR_MDNS=mDNSResponder ./script/bootstrap + +# Stub systemctl so setup doesn't try to start/enable services during docker build +RUN ln -sf /bin/true /usr/local/bin/systemctl + +# Run setup to install binaries, DBus config, ot-ctl, etc. +RUN HOME=/root && cd /home/otns/ot-br-posix && \ + INFRA_IF_NAME=eth0 WEB_GUI=1 NAT64=1 OTBR_MDNS=mDNSResponder DOCKER=1 FIREWALL=0 ./script/setup + +# Restore real systemctl for runtime use +RUN rm -f /usr/local/bin/systemctl + +# Rebuild OT-BR-POSIX using the OTNS-specific build script from ot-rfsim. +# This overwrites the otbr-agent binary with one that has the correct flags +# (OTNS support, no backbone router multicast routing which fails on macOS Docker). +RUN cd /home/otns/ot-ns/ot-rfsim && \ + yes | ./script/build_otbr \ + -DOTBR_BACKBONE_ROUTER=OFF \ + -DOT_BACKBONE_ROUTER_MULTICAST_ROUTING=OFF + +USER otns + +# --------------------------------------------------------------- +# Stage 6: Entrypoint with OT-BR services +# --------------------------------------------------------------- +WORKDIR ${HOME} + +# Create a startup script that launches supporting services. +# OTNS manages the border router (ot-rfsim BR) itself - we only +# start the infrastructure services it depends on. +COPY --chown=otns:otns <<'ENTRYPOINT_SCRIPT' /home/otns/entrypoint.sh +#!/bin/bash + +# Detect the main non-loopback interface for OTBR backbone +BACKBONE_IF=$(ip -o link show up | awk -F': ' '!/lo/{print $2; exit}') +BACKBONE_IF="${BACKBONE_IF:-eth0}" + +echo "=== OTNS + Matter + OT-BR Development Environment ===" +echo "" +echo "Detected backbone interface: ${BACKBONE_IF}" +echo "" +echo "Once the OTBR node is started, start the OT-BR web interface:" +echo " otbr-web -p 8080" +echo "" +echo "Matter all-clusters-app:" +ARCH_DIR=$(ls -d ~/connectedhomeip/out/linux-*-all-clusters-* 2>/dev/null | head -1) +if [ -n "$ARCH_DIR" ]; then + echo " ${ARCH_DIR}/chip-all-clusters-app" +else + echo " (not found - build may have failed)" +fi +echo "" +echo "OTNS node binaries:" +echo " ~/ot-ns/ot-rfsim/ot-versions/" +echo "" +echo "Start OTNS with border routing (OMR prefix + SRP server):" +echo " sudo otns -realtime -listen 0.0.0.0:9000 -otbr-backbone-if ${BACKBONE_IF}" +echo "" + +# Alias so that bare 'otns' uses the right flags +alias otns="sudo otns -realtime -listen 0.0.0.0:9000 -otbr-backbone-if ${BACKBONE_IF}" + +# Start D-Bus (needed by OT-BR services) +sudo mkdir -p /run/dbus +sudo dbus-daemon --system --nopidfile || true + +# Start rsyslog so Matter syslog logging works +sudo rsyslogd 2>/dev/null || true + +# Start mDNSResponder (needed by otbr-agent for DNS-SD) +sudo mdnsd > /tmp/mdnsd.log 2>&1 || echo "ERROR: mdnsd failed to start, see /tmp/mdnsd.log" + +exec "$@" +ENTRYPOINT_SCRIPT +RUN chmod +x ${HOME}/entrypoint.sh + +# Expose OTNS web ports and OT-BR web UI +EXPOSE 8997 8998 8999 9000 8080 + +ENTRYPOINT ["/home/otns/entrypoint.sh"] +WORKDIR ${HOME}/ot-ns/ot-rfsim/ot-versions +CMD ["/bin/bash"] diff --git a/examples/otbr-and-matter/README.md b/examples/otbr-and-matter/README.md new file mode 100644 index 00000000..8975fe78 --- /dev/null +++ b/examples/otbr-and-matter/README.md @@ -0,0 +1,169 @@ +# OTNS + Matter + OT-BR Docker Environment + +This Docker image provides a complete development environment for simulating +Thread networks with OTNS, including Matter device support and an OpenThread +Border Router. + +## What's included + +- **OTNS** — OpenThread Network Simulator with web UI +- **OT-BR-POSIX** — OpenThread Border Router (with border routing, SRP server, NAT64) +- **Matter all-clusters-app** — A virtual Matter accessory that runs over Thread (via OTNS) +- **ot-rfsim node binaries** — FTD, MTD, BR, RCP, and Matter node types for OTNS + +## Prerequisites + +- Docker (Docker Desktop on macOS, or Docker Engine on Linux) + +## Build (optional) + +```bash +./run-docker.sh build +``` + +Or manually: + +```bash +docker build -t otns-matter . +``` + +The build takes a long time (compiling OpenThread, Matter, OT-BR from source). + +## Run + +If you haven't built the image in the previous step, it will pull the image from Docker Hub. + +```bash +./run-docker.sh run +``` + +## Using OTNS inside the container + +The entrypoint auto-detects the backbone interface and prints the recommended +command. Typically: + +```bash +otns -realtime -listen 0.0.0.0:9000 -otbr-backbone-if eth0 +``` + +Or just type `otns` — the entrypoint sets up an alias with the right flags. + +Once OTNS is running, open `http://localhost:8997/visualize?addr=localhost:8998` in your browser to access +the OTNS web UI. + +### Spawning nodes in OTNS + +In the OTNS web UI or CLI, you can add: + +- **FTD nodes** — regular Thread Full Thread Devices +- **OTBR nodes** — Thread Border Routers (with border routing and SRP server) +- **Matter nodes** — Matter all-clusters-app devices running over Thread + +The OTBR node will automatically: +- Advertise an OMR prefix in Thread network data +- Run an SRP server and advertise it in Thread network data + +(Requires `-otbr-backbone-if` to be set to a valid interface.) + +## Commissioning a Matter device from outside the container + +If you run the container on a Linux machine or VM with `--network host`, you +can commission a simulated Matter device from outside the container using +chip-tool. + +### 1. Install avahi (for observation and debugging) + +```bash +sudo apt-get update +sudo apt-get install -y avahi-utils +``` + +Useful commands: + +```bash +avahi-browse -rt _meshcop._udp # Browse Thread Border Routers +avahi-browse -rt _matterc._udp # Browse Matter commissionable devices +``` + +### 2. Install chip-tool + +The chip-tool is used to commission and control Matter accessories. You can either install it from snap or build it from source. + +#### Installing it from snap + +```bash +sudo snap install chip-tool +``` + +#### Building from sources + +```bash +# Install build dependencies +sudo apt-get install -y libglib2.0-dev-bin libglib2.0-dev libgirepository1.0-dev libevent-dev + +git clone https://github.com/project-chip/connectedhomeip.git +cd connectedhomeip +scripts/checkout_submodules.py --shallow --platform linux +source scripts/activate.sh +./scripts/build/build_examples.py --target linux-x64-chip-tool build +``` + +The binary will be at `out/linux-x64-chip-tool/chip-tool`. +For arm64 hosts, replace `x64` with `arm64`. + +### 3. Commission and control a Matter lightbulb + +#### Setup in OTNS + +1. Start OTNS inside the container: + ``` + otns -realtime -listen 0.0.0.0:9000 -otbr-backbone-if eth0 + ``` +2. Add a Matter node first: click on "matter" or type `add matter` in the OTNS CLI +3. Add an OTBR node: click on "otbr" or type `add otbr` in the OTNS CLI +4. Add as many other nodes as you want: click on "router" or type `add router` + in the OTNS CLI +5. Monitor the announced services from the Linux VM (outside the container) using `avahi-browse -a`. + Wait until a `_matterc._udp` service shows up, meaning that the Matter node has registered its SRP service on the OTBR node. Once that is donem you can start pairing and controlling the accessory. + +**Important:** Always add the Matter node before the OTBR node. Add nodes one +at a time and wait for each to fully join before adding the next. All Matter +nodes currently use the same default pairing code, so adding multiple +simultaneously will cause conflicts. Use a different node ID for each Matter +node you commission. + +#### Pair the lightbulb + +From the Linux host (outside the container), pair using the QR code payload: + +```bash +chip-tool pairing onnetwork 1234 20202021 +``` + +The `1234` is the node ID you assign to this device — use a different ID for each +Matter node you commission (e.g., 1234, 1235, 1, 2...). + +#### Control the lightbulb + +```bash +# Turn on +./chip-tool onoff on 1234 1 + +# Turn off +./chip-tool onoff off 1234 1 + +# Read current on/off state +./chip-tool onoff read on-off 1234 1 +``` + +The `1234` is the node ID (assigned during pairing), it must be different for every matter node. The `1` is the endpoint ID. It is always `1` for every node. + +#### Viewing Matter node logs + +The Matter node writes its output to `/var/log/syslog` inside the container. +To follow logs: + +```bash +# Inside the container +sudo cat /var/log/syslog | grep ot-matter +``` diff --git a/examples/otbr-and-matter/run-docker.sh b/examples/otbr-and-matter/run-docker.sh new file mode 100755 index 00000000..cd05f841 --- /dev/null +++ b/examples/otbr-and-matter/run-docker.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Build or run the OTNS + Matter + OT-BR Docker image. +# Works on both macOS and Linux hosts. +# +# Usage: +# ./run-docker.sh build Build the Docker image +# ./run-docker.sh run Run an interactive container +# ./run-docker.sh build-run Build then run + +set -e + +IMAGE_NAME="framichel/otns-matter" +CONTAINER_NAME="otns-matter-dev" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Detect host architecture and map to Docker platform +detect_platform() { + local arch + arch="$(uname -m)" + case "${arch}" in + x86_64|amd64) echo "linux/amd64" ;; + arm64|aarch64) echo "linux/arm64" ;; + *) + echo "Unsupported architecture: ${arch}" >&2 + exit 1 + ;; + esac +} + +do_build() { + local platform + platform="$(detect_platform)" + echo "Building ${IMAGE_NAME} for platform ${platform} ..." + docker build \ + --platform "${platform}" \ + -t "${IMAGE_NAME}" \ + "${SCRIPT_DIR}" +} + +do_run() { + local platform + platform="$(detect_platform)" + echo "Starting ${CONTAINER_NAME} (platform ${platform}) ..." + + local run_args=( + -it --rm + --platform "${platform}" + --name "${CONTAINER_NAME}" + --cap-add NET_ADMIN + --device /dev/net/tun:/dev/net/tun + ) + + if [ "$(uname)" = "Linux" ]; then + # On Linux, use host networking for direct access to host interfaces + run_args+=(--network host) + else + # On macOS, use port forwarding and enable IPv6 + run_args+=( + --sysctl net.ipv6.conf.all.disable_ipv6=0 + -p 8997:8997 + -p 8998:8998 + -p 8999:8999 + -p 9000:9000 + -p 8080:8080 + ) + fi + + docker run "${run_args[@]}" "${IMAGE_NAME}" +} + +case "${1}" in + build) + do_build + ;; + run) + do_run + ;; + build-run) + do_build + do_run + ;; + *) + echo "Usage: $0 build|run|build-run" >&2 + exit 1 + ;; +esac From b5f6bfc57c7052dd91cae12b46ad70d1d5dda207 Mon Sep 17 00:00:00 2001 From: Francois Michel Date: Fri, 17 Apr 2026 16:58:49 -0700 Subject: [PATCH 2/7] add OTNS license headers --- examples/otbr-and-matter/Dockerfile | 28 +++++++++++++++++++++++++- examples/otbr-and-matter/run-docker.sh | 27 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/examples/otbr-and-matter/Dockerfile b/examples/otbr-and-matter/Dockerfile index ee17807a..03e36922 100644 --- a/examples/otbr-and-matter/Dockerfile +++ b/examples/otbr-and-matter/Dockerfile @@ -1,6 +1,32 @@ +# Copyright (c) 2026, The OTNS Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + # Dockerfile for OTNS + OT-BR + Matter all-clusters-app development environment # -# Works on both arm64 and x86_64/amd64 hosts +# Works on both arm64 and x86_64/amd64 hosts FROM ubuntu:24.04 diff --git a/examples/otbr-and-matter/run-docker.sh b/examples/otbr-and-matter/run-docker.sh index cd05f841..6bc571ec 100755 --- a/examples/otbr-and-matter/run-docker.sh +++ b/examples/otbr-and-matter/run-docker.sh @@ -1,4 +1,31 @@ #!/bin/bash +# +# Copyright (c) 2026, The OTNS Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + # Build or run the OTNS + Matter + OT-BR Docker image. # Works on both macOS and Linux hosts. # From 499ad975470d028d33dc16f72da8d1898f4c36dd Mon Sep 17 00:00:00 2001 From: Francois Michel Date: Wed, 22 Apr 2026 16:43:29 +0200 Subject: [PATCH 3/7] add automation script and update README to show how to use it --- examples/otbr-and-matter/README.md | 43 ++++++++ examples/otbr-and-matter/otns-automation.py | 116 ++++++++++++++++++++ examples/otbr-and-matter/run-docker.sh | 16 ++- 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 examples/otbr-and-matter/otns-automation.py diff --git a/examples/otbr-and-matter/README.md b/examples/otbr-and-matter/README.md index 8975fe78..aeeb00fc 100644 --- a/examples/otbr-and-matter/README.md +++ b/examples/otbr-and-matter/README.md @@ -167,3 +167,46 @@ To follow logs: # Inside the container sudo cat /var/log/syslog | grep ot-matter ``` + +## Going further with OTNS automation + +OTNS exposes a Python API that lets you script the full lifecycle of a simulated +Thread network — spawning nodes, sending node commands, and driving time — all +from a single Python script. + +An example script `otns-automation.py` is provided in this repository. To run it +inside the container: + +1. Copy `otns-automation.py` into your running container: + ```bash + ./run-docker.sh copy otns-automation.py + ``` +2. Inside the container, activate the OTNS Python virtual environment: + ```bash + source ~/ot-ns/.venv-otns/bin/activate + ``` +3. Run the script with root privileges (required to create virtual network interfaces): + ```bash + cd ~/ot-ns && sudo $(which python3) otns-automation.py + ``` +4. Open the OTNS web UI in your browser: + `http://localhost:8997/visualize?addr=localhost:8998` + +### What the script does + +The script automates the following sequence: + +1. **Detects the backbone interface** — finds the first non-loopback interface + that is up (falls back to `eth0`). +2. **Starts OTNS** in realtime mode with the web UI enabled and the correct + backbone interface set. +3. **Spawns a Matter lightbulb** node at the center of the canvas. +4. **Spawns 10 router nodes** arranged in a circle around the Matter node. Each + router enables SRP client autostart, sets a hostname (`router-`), and + registers a custom SRP service (`_otns-handson._tcp` on port 12345). +5. **Spawns an OTBR node** on the opposite side of the circle. The OTBR + advertises an OMR prefix and runs an SRP server, allowing the Matter node to + publish its service and become commissionable from outside the container. + +Once the mesh has converged you can commission and control the Matter lightbulb +exactly as described in the section above. diff --git a/examples/otbr-and-matter/otns-automation.py b/examples/otbr-and-matter/otns-automation.py new file mode 100644 index 00000000..cc5c3653 --- /dev/null +++ b/examples/otbr-and-matter/otns-automation.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +OTNS demo: spawn a Matter lightbulb, 10 routers with custom SRP services, and an OTBR. + +Usage (inside the Docker container): + sudo $(which python3) otns-demo.py +""" + +import glob +import math +import os +import time +import subprocess +from otns.cli import OTNS + +NUM_ROUTERS = 10 +CENTER_X = 400 +CENTER_Y = 400 +RADIUS = 250 + + +def detect_backbone_interface(): + """Detect the first non-loopback interface that is up.""" + try: + result = subprocess.run( + ['ip', '-o', 'link', 'show', 'up'], + capture_output=True, text=True, check=True + ) + for line in result.stdout.strip().splitlines(): + parts = line.split(': ') + if len(parts) >= 2: + iface = parts[1].split('@')[0] + if iface != 'lo': + return iface + except (subprocess.CalledProcessError, FileNotFoundError): + pass + return 'eth0' + + +print("OTNS Web UI: http://localhost:8997/visualize?addr=localhost:8998") +print("") + +backbone_if = detect_backbone_interface() +print(f"Using backbone interface: {backbone_if}") + +# Clean up stale chip-tool/Matter state files from previous runs +for pattern in ['/tmp/chip_*', '/tmp/chip-*']: + for f in glob.glob(pattern): + os.remove(f) + +# Start OTNS in realtime mode with web UI enabled +ns = OTNS(otns_args=[ + '-realtime', + '-listen', '0.0.0.0:9000', + '-web=true', + '-otbr-backbone-if', backbone_if, +]) + +# Spawn the Matter lightbulb at the center +print("Adding Matter node at center...") +matter_id = ns.add("matter", x=CENTER_X, y=CENTER_Y) +time.sleep(2) + +# Spawn router nodes in a circle around the Matter node +for i in range(NUM_ROUTERS): + angle = 2 * math.pi * i / NUM_ROUTERS + x = int(CENTER_X + RADIUS * math.cos(angle)) + y = int(CENTER_Y + RADIUS * math.sin(angle)) + + print(f"Adding router {i+1}/{NUM_ROUTERS}...") + router_id = ns.add("router", x=x, y=y) + time.sleep(2) + + # Register a custom SRP service: "router-{node_id}" _otns-handson._tcp + ns.node_cmd(router_id, 'srp client autostart enable') + ns.node_cmd(router_id, f'srp client host name router-{router_id}') + ns.node_cmd(router_id, 'srp client host address auto') + ns.node_cmd(router_id, f'srp client service add router-{router_id} _otns-handson._tcp 12345') + +# Spawn the OTBR node on the circle (opposite side from router 0) +otbr_angle = math.pi +otbr_x = int(CENTER_X + RADIUS * math.cos(otbr_angle)) +otbr_y = int(CENTER_Y + RADIUS * math.sin(otbr_angle)) +print("Adding OTBR node...") +otbr_id = ns.add("otbr", x=otbr_x, y=otbr_y) +time.sleep(2) + +print(f"\nAll nodes added:") +print(f" Matter node: {matter_id} (center)") +print(f" Routers: {NUM_ROUTERS} nodes with _otns-handson._tcp services") +print(f" OTBR node: {otbr_id}") + +# Enable autogo so the simulation runs while we wait +ns.autogo = True + +# Give time for the mesh to converge and for the Matter node to publish its SRP service +print("\nWaiting 20s for the mesh to converge...") +time.sleep(20) + +print("\n=== Ready ===") +print("Commission the Matter lightbulb by running:") +print(" chip-tool pairing onnetwork 1234 20202021") +print("") +print("Then control it with:") +print(" chip-tool onoff on 1234 1") +print(" chip-tool onoff off 1234 1") +print("") +print("Press Ctrl+C to stop.") + +# Keep the script running with interactive CLI +try: + ns.interactive_cli() +except KeyboardInterrupt: + print("\nShutting down...") + ns.close() + diff --git a/examples/otbr-and-matter/run-docker.sh b/examples/otbr-and-matter/run-docker.sh index 6bc571ec..c54cd45d 100755 --- a/examples/otbr-and-matter/run-docker.sh +++ b/examples/otbr-and-matter/run-docker.sh @@ -32,6 +32,7 @@ # Usage: # ./run-docker.sh build Build the Docker image # ./run-docker.sh run Run an interactive container +# ./run-docker.sh copy Copy a file into the running container # ./run-docker.sh build-run Build then run set -e @@ -64,6 +65,16 @@ do_build() { "${SCRIPT_DIR}" } +do_copy() { + local file="${1}" + if [ -z "${file}" ]; then + echo "Usage: $0 copy " >&2 + exit 1 + fi + echo "Copying ${file} into ${CONTAINER_NAME}:/home/otns/ot-ns/ ..." + docker cp "${file}" "${CONTAINER_NAME}:/home/otns/ot-ns/" +} + do_run() { local platform platform="$(detect_platform)" @@ -102,12 +113,15 @@ case "${1}" in run) do_run ;; + copy) + do_copy "${2}" + ;; build-run) do_build do_run ;; *) - echo "Usage: $0 build|run|build-run" >&2 + echo "Usage: $0 build|run|copy|build-run" >&2 exit 1 ;; esac From 6f38cfbbec48bc02b1b288f36038bd343ab9df4b Mon Sep 17 00:00:00 2001 From: francoismichel Date: Thu, 23 Apr 2026 09:38:38 +0200 Subject: [PATCH 4/7] Update README.md --- examples/otbr-and-matter/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/otbr-and-matter/README.md b/examples/otbr-and-matter/README.md index aeeb00fc..a66bd5c9 100644 --- a/examples/otbr-and-matter/README.md +++ b/examples/otbr-and-matter/README.md @@ -29,6 +29,14 @@ docker build -t otns-matter . The build takes a long time (compiling OpenThread, Matter, OT-BR from source). +## Load an existing image + +If you received the image as a TAR archive, you can load it by running: + +``` +docker load < +``` + ## Run If you haven't built the image in the previous step, it will pull the image from Docker Hub. From fa0e29dbb5cf53be58ed6ef38bb413e95e18c17a Mon Sep 17 00:00:00 2001 From: francoismichel Date: Thu, 23 Apr 2026 09:43:24 +0200 Subject: [PATCH 5/7] Update README with Docker image tagging instructions Added instructions to remove 'docker.io' prefix from image tag. --- examples/otbr-and-matter/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/otbr-and-matter/README.md b/examples/otbr-and-matter/README.md index a66bd5c9..778da1dc 100644 --- a/examples/otbr-and-matter/README.md +++ b/examples/otbr-and-matter/README.md @@ -37,6 +37,13 @@ If you received the image as a TAR archive, you can load it by running: docker load < ``` +On some Docker versions, the image gets tagged with the `docker.io` prefix. if thqt is the case, remove that prefix by running: + +``` +docker tag docker.io/framichel/otns-matter framichel/otns-matter +``` + + ## Run If you haven't built the image in the previous step, it will pull the image from Docker Hub. From 6b9ecabfc5b3b78718c45a8d2695e248b47e3959 Mon Sep 17 00:00:00 2001 From: francoismichel Date: Thu, 23 Apr 2026 09:44:01 +0200 Subject: [PATCH 6/7] Fix typo in README.md regarding Docker image tagging --- examples/otbr-and-matter/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/otbr-and-matter/README.md b/examples/otbr-and-matter/README.md index 778da1dc..38099346 100644 --- a/examples/otbr-and-matter/README.md +++ b/examples/otbr-and-matter/README.md @@ -37,7 +37,7 @@ If you received the image as a TAR archive, you can load it by running: docker load < ``` -On some Docker versions, the image gets tagged with the `docker.io` prefix. if thqt is the case, remove that prefix by running: +On some Docker versions, the image gets tagged with the `docker.io` prefix. if that is the case, remove that prefix by running: ``` docker tag docker.io/framichel/otns-matter framichel/otns-matter From 29930955afb6de43ea8612af6b65ec4df36e2d2a Mon Sep 17 00:00:00 2001 From: francoismichel Date: Tue, 28 Apr 2026 10:22:33 -0700 Subject: [PATCH 7/7] Clarify optional loading of existing image in README --- examples/otbr-and-matter/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/otbr-and-matter/README.md b/examples/otbr-and-matter/README.md index 38099346..e50db201 100644 --- a/examples/otbr-and-matter/README.md +++ b/examples/otbr-and-matter/README.md @@ -29,7 +29,7 @@ docker build -t otns-matter . The build takes a long time (compiling OpenThread, Matter, OT-BR from source). -## Load an existing image +## Load an existing image (optional) If you received the image as a TAR archive, you can load it by running: