Skip to content

Commit 3b188c4

Browse files
committed
[all] Add the OTBR node type, for real-time mode
The OTBR allows interfacing to a real Ethernet/Wi-Fi network. An 'OTBR' button is added in the GUI when in real-time mode. Only 1 OTBR can be added at a time. Binaries otbr-agent and ot-ctl are used, running as root user via 'sudo -n'. Automatic instructions for 'sudoers' will appear if the user attempts to start an OTBR without these processes having root permission. A build script is added to easily build these binaries and install them. The OTBR can be started with the 'restore' flag to let it restore previous (NVM) network settings in /var/lib/thread. GUIDE.md is updated with instructions to build an OTBR and run OTNS correctly.
1 parent e2136de commit 3b188c4

17 files changed

Lines changed: 437 additions & 72 deletions

File tree

GUIDE.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,42 @@ Done
262262
This shows the "ext" type for node 2, and also the string "(external-node)" instead of the actual node's executable name, which is unknown to OTNS.
263263

264264
In case the externally running node process is interrupted (e.g. by pressing Ctrl+C) or killed, the corresponding node automatically gets removed from the simulation and a warning is logged by OTNS.
265+
266+
## Adding an OpenThread Border Router (OTBR) interfacing to a real network (Optional, for Advanced Use Only)
267+
268+
In OTNS real-time mode, a real OpenThread Border Router (OTBR) can be added to the simulation. The OTBR can route IPv6 between the simulated network and a real network.
269+
270+
Running an OTBR requires the binaries `otbr-agent` (daemon) and `ot-ctl` (CLI interface) to be built, from the project `openthread/ot-br-posix`. The current process for this includes two steps:
271+
272+
1. Use Git to check out the project `openthread/ot-br-posix` into a local directory, located at `../ot-br-posix`. So this directory resides in the same parent directory as this `ot-ns` project directory.
273+
2. Use the build script to build the OTBR correctly (with parameters suitable for simulation):
274+
275+
```bash
276+
cd ot-rfsim
277+
./script/build_otbr
278+
[... build output here ...]
279+
Build of 'otbr-agent' done.
280+
Copy otbr-agent to /usr/local/sbin for use by OTNS? [y/N]
281+
[... next question here ...]
282+
```
283+
284+
Answer 'y' to the two questions to copy the build result. This requires entering the root password (for sudo). It results in the two binaries being installed locally, to be used by OTNS.
285+
286+
To run these, OTNS needs root access (sudo) for these binaries. When starting an OTBR node, OTNS will try to execute both using the `sudo -n` non-interactive invocation. To make this work, both binaries need to be added to the `/etc/sudoers` list by using `sudo visudo` and then adding the below two lines at the end of the file:
287+
288+
```bash
289+
myusername ALL=(ALL) NOPASSWD: SETENV: /usr/local/sbin/otbr-agent
290+
myusername ALL=(ALL) NOPASSWD: /usr/local/bin/ot-ctl
291+
```
292+
293+
Check that the binaries are installed in the given locations: if not, adjust the paths accordingly. If both binaries are available in the user's PATH, OTNS will find them automatically.
294+
295+
Below is an example how OTNS should be run to add an OTBR to the simulation. Once OTNS is running, the CLI command to add an OTBR is `add otbr`.
296+
297+
```bash
298+
otns -realtime -otbr-backbone-if eth0
299+
```
300+
301+
The flag `-realtime` specifies that OTNS runs in real-time mode, required to support OTBR and other RCP/Posix based nodes.
302+
303+
The parameter `-otbr-backbone-if` specifies the network interface (AIL) that the OTBR should use to connect to the real network. It can be your local Ethernet or Wi-Fi interface, or a virtual network interface set up with Linux network namespaces. If the parameter is omitted, the OTBR will connect to the loopback interface (`lo`) by default. In this case, no external communication is possible.

cli/CmdRunner.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,11 @@ func (rt *CmdRunner) executeExe(cc *CommandContext, cmd *ExeCmd) {
12981298
ec.Br = cmd.Path
12991299
}
13001300
cc.outputf("br : %s\n", ec.Br)
1301+
case OTBR:
1302+
if isSetPath {
1303+
ec.OtBr = cmd.Path
1304+
}
1305+
cc.outputf("otbr: %s\n", ec.OtBr)
13011306
case RCP:
13021307
if isSetPath {
13031308
ec.Rcp = cmd.Path
@@ -1326,13 +1331,15 @@ func (rt *CmdRunner) executeExe(cc *CommandContext, cmd *ExeCmd) {
13261331
cc.outputf("ftd: %s\n", ec.Ftd)
13271332
cc.outputf("mtd: %s\n", ec.Mtd)
13281333
cc.outputf("br : %s\n", ec.Br)
1334+
cc.outputf("otbr: %s\n", ec.OtBr)
13291335
cc.outputf("matter: %s\n", ec.Matter)
13301336
cc.outputf("rcp: %s\n", ec.Rcp)
13311337
cc.outputf("host: %s\n", ec.RcpHost)
13321338
cc.outputf("Executables search path: %s\n", ec.SearchPathsString())
13331339
cc.outputf("Detected FTD path : %s\n", ec.FindExecutable(ec.Ftd))
13341340
cc.outputf("Detected MTD path : %s\n", ec.FindExecutable(ec.Mtd))
13351341
cc.outputf("Detected BR path : %s\n", ec.FindExecutable(ec.Br))
1342+
cc.outputf("Detected OTBR path : %s\n", ec.FindExecutable(ec.OtBr))
13361343
cc.outputf("Detected Matter path : %s\n", ec.FindExecutable(ec.Matter))
13371344
cc.outputf("Detected RCP path : %s\n", ec.FindExecutable(ec.Rcp))
13381345
cc.outputf("Detected RCP Host path : %s\n", ec.FindExecutable(ec.RcpHost))

cli/ast.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ type AddCmd struct {
312312

313313
// noinspection GoVetStructTag
314314
type NodeTypeOrRole struct {
315-
Val string `@("router"|"reed"|"fed"|"med"|"sed"|"ssed"|"br"|"mtd"|"ftd"|"wifi"|"matter"|"rcp"|"host")` //nolint
315+
Val string `@("router"|"reed"|"fed"|"med"|"sed"|"ssed"|"br"|"otbr"|"mtd"|"ftd"|"wifi"|"matter"|"rcp"|"host")` //nolint
316316
}
317317

318318
// noinspection GoVetStructTag

dispatcher/dispatcher.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,10 @@ func (d *Dispatcher) HandleEvent(evt *Event) {
535535
case EventTypeUartWrite:
536536
d.Counters.UartWriteEvents += 1
537537
d.cbHandler.OnUartWrite(node.Id, evt.Data)
538+
case EventTypeUartDisconnected:
539+
d.Counters.OtherEvents += 1
540+
d.setSleeping(node.Id)
541+
d.alarmMgr.SetTimestamp(node.Id, Ever)
538542
case EventTypeLogWrite:
539543
d.Counters.LogWriteEvents += 1
540544
d.cbHandler.OnLogWrite(node.Id, evt.Data)
@@ -845,7 +849,6 @@ func (d *Dispatcher) eventsReader() {
845849
Delay: 0,
846850
Type: EventTypeNodeDisconnected,
847851
NodeId: myNodeId,
848-
Conn: myConn,
849852
}
850853
}
851854
}(conn)
@@ -1052,8 +1055,11 @@ func (d *Dispatcher) onMsgToHost(node *Node, evt *Event) {
10521055
}
10531056

10541057
func (d *Dispatcher) setAlive(nodeid NodeId) {
1055-
logger.AssertFalse(d.isDeleted(nodeid))
1056-
1058+
node := d.nodes[nodeid]
1059+
logger.AssertNotNil(node)
1060+
if node.hasDisconnected.Load() {
1061+
return
1062+
}
10571063
d.aliveNodes[nodeid] = struct{}{}
10581064
}
10591065

@@ -1582,7 +1588,7 @@ func (d *Dispatcher) SetVisualizationOptions(opts VisualizationOptions) {
15821588
// delivered to the node with other means than Event(s), such as writing to the stdin or
15831589
// the real-time UART of the node, or sending a signal to the node's process.
15841590
func (d *Dispatcher) NotifyCommand(nodeid NodeId) {
1585-
if node := d.nodes[nodeid]; node != nil && !node.hasDisconnected.Load() {
1591+
if node := d.nodes[nodeid]; node != nil {
15861592
d.setAlive(nodeid)
15871593

15881594
// Due to the externally received command, the virtual time of the node may now be

ot-rfsim/ot-versions/ot-br.sh

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Copyright (c) 2026, The OpenThread Authors.
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
# 1. Redistributions of source code must retain the above copyright
9+
# notice, this list of conditions and the following disclaimer.
10+
# 2. Redistributions in binary form must reproduce the above copyright
11+
# notice, this list of conditions and the following disclaimer in the
12+
# documentation and/or other materials provided with the distribution.
13+
# 3. Neither the name of the copyright holder nor the
14+
# names of its contributors may be used to endorse or promote products
15+
# derived from this software without specific prior written permission.
16+
#
17+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
# POSSIBILITY OF SUCH DAMAGE.
28+
#
29+
30+
# ot-br.sh - script to start an OTBR node from an OTNS simulation.
31+
32+
# When a running script receives SIGTERM
33+
cleanup()
34+
{
35+
echo "[DEBG] ot-br.sh: caught signal, cleaning up child processes"
36+
jobs -p | xargs -r kill
37+
wait
38+
echo "[DEBG] ot-br.sh: script exit"
39+
exit 0
40+
}
41+
42+
trap cleanup SIGINT SIGTERM
43+
44+
sudo_command_failed()
45+
{
46+
local cmd="$1"
47+
local cmd_path
48+
echo "[CRIT] passwordless sudo failed for '${cmd}'"
49+
echo "[CRIT] add below lines to /etc/sudoers (run: sudo visudo):"
50+
51+
cmd="otbr-agent"
52+
cmd_path=$(command -v "$cmd" 2>/dev/null)
53+
echo "[CRIT] ${USER} ALL=(ALL) NOPASSWD: SETENV: ${cmd_path:-/usr/local/sbin/${cmd}}"
54+
55+
cmd="ot-ctl"
56+
cmd_path=$(command -v "$cmd" 2>/dev/null)
57+
echo "[CRIT] ${USER} ALL=(ALL) NOPASSWD: ${cmd_path:-/usr/local/bin/${cmd}}"
58+
exit 1
59+
}
60+
61+
socket_in_use()
62+
{
63+
if command -v ss >/dev/null 2>&1; then
64+
ss -x src "$1" | grep -q "$1"
65+
elif command -v lsof >/dev/null 2>&1; then
66+
lsof "$1" >/dev/null 2>&1
67+
else
68+
return 1 # can't check, assume not in use (1 = false)
69+
fi
70+
}
71+
72+
echo "[DEBG] ot-br.sh started"
73+
74+
NODE_ID=$1
75+
BACKBONE_IF_NAME=$2
76+
AGENT_PARAM=$3
77+
RADIO_URL=$4
78+
# TODO: 'wpan1' for node 1 etc doesn't work - needs to be wpan0. Check later if there's a way to use others.
79+
THREAD_IF_NAME="wpan0"
80+
EXTRA_DELAY=0
81+
MAX_WAIT_SEC=5
82+
SOCKET_PATH="/run/openthread-${THREAD_IF_NAME}.sock"
83+
DATA_PATH="/var/lib/thread"
84+
85+
echo "[DEBG] PORT_OFFSET =${PORT_OFFSET}"
86+
echo "[DEBG] NODE_ID =${NODE_ID}"
87+
echo "[DEBG] BACKBONE_IF_NAME=${BACKBONE_IF_NAME}"
88+
echo "[DEBG] THREAD_IF_NAME =${THREAD_IF_NAME}"
89+
echo "[DEBG] RADIO_URL =${RADIO_URL}"
90+
echo "[DEBG] SOCKET_PATH =${SOCKET_PATH}"
91+
echo "[DEBG] DATA_PATH =${DATA_PATH}"
92+
echo "[DEBG] AGENT_PARAM =${AGENT_PARAM}"
93+
94+
# check for passwordless sudo access to commands
95+
sudo -n otbr-agent -V >/dev/null 2>&1 || sudo_command_failed otbr-agent
96+
sudo -n ot-ctl -h >/dev/null 2>&1 || sudo_command_failed ot-ctl
97+
98+
# check for existing socket usage
99+
if [ -S "${SOCKET_PATH}" ]; then
100+
if socket_in_use "${SOCKET_PATH}"; then
101+
echo "[CRIT] socket ${SOCKET_PATH} is already in use - is otbr-agent already running on ${THREAD_IF_NAME}?"
102+
exit 1
103+
fi
104+
echo "[DEBG] existing (unused?) file ${SOCKET_PATH} detected - start with extra delay"
105+
EXTRA_DELAY=3
106+
fi
107+
108+
echo "[DEBG] starting otbr-agent"
109+
# All otbr-agent output redirected to stderr, so that ot-ctl CLI interactions are not garbled.
110+
sudo -n PORT_OFFSET="${PORT_OFFSET}" otbr-agent --data-path "${DATA_PATH}" -s -d 7 -I ${THREAD_IF_NAME} -B "${BACKBONE_IF_NAME}" \
111+
"${AGENT_PARAM}" "${RADIO_URL}" 1>&2 &
112+
SUDO_OTBR_PID=$!
113+
114+
echo "[DEBG] otbr-agent started in background (parent PID=${SUDO_OTBR_PID}) - waiting until ready"
115+
116+
# wait for otbr-agent to create its Unix socket
117+
elapsed=0
118+
while [ ! -S "${SOCKET_PATH}" ]; do
119+
if ! kill -0 "${SUDO_OTBR_PID}" 2>/dev/null; then
120+
echo "[CRIT] otbr-agent exited before socket was ready"
121+
exit 1
122+
fi
123+
if [ "${elapsed}" -ge "$((MAX_WAIT_SEC * 10))" ]; then
124+
echo "[CRIT] timed out waiting for otbr-agent socket (${MAX_WAIT_SEC}s)"
125+
exit 1
126+
fi
127+
sleep 0.1
128+
elapsed=$((elapsed + 1))
129+
done
130+
echo "[DEBG] otbr-agent socket ready after $((elapsed / 10)).$((elapsed % 10))s, starting ot-ctl CLI"
131+
sleep ${EXTRA_DELAY}
132+
sudo -n ot-ctl
133+
134+
echo "[DEBG] ot-br.sh: ot-ctl CLI exited, cleaning up child processes"
135+
jobs -p | xargs -r kill
136+
wait
137+
echo "[DEBG] ot-br.sh: script exit"

ot-rfsim/script/build_otbr

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/bin/bash
2+
#
3+
# Copyright (c) 2023-2026, The OpenThread Authors.
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
# 1. Redistributions of source code must retain the above copyright
9+
# notice, this list of conditions and the following disclaimer.
10+
# 2. Redistributions in binary form must reproduce the above copyright
11+
# notice, this list of conditions and the following disclaimer in the
12+
# documentation and/or other materials provided with the distribution.
13+
# 3. Neither the name of the copyright holder nor the
14+
# names of its contributors may be used to endorse or promote products
15+
# derived from this software without specific prior written permission.
16+
#
17+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
# POSSIBILITY OF SUCH DAMAGE.
28+
29+
# Build script to build an OTBR, to use in OTNS realtime mode.
30+
# It requires a manual setup of the 'ot-br-posix' repository in the same directory where
31+
# the 'ot-ns' repo is checked out.
32+
33+
set -euxo pipefail
34+
35+
# OpenThread Border Router (OTBR) options.
36+
OTBR_OPTIONS=(
37+
"-DOT_THREAD_VERSION=1.4"
38+
"-DOT_REFERENCE_DEVICE=OFF"
39+
"-DOT_LOG_OUTPUT=PLATFORM_DEFINED"
40+
"-DOT_VENDOR_SW_VERSION=2.0.0"
41+
"-DOT_OTNS=ON"
42+
"-DOT_FULL_LOGS=ON"
43+
"-DOT_LOG_LEVEL=DEBG"
44+
"-DOT_MLR=ON"
45+
"-DOT_BORDER_ROUTING=ON"
46+
"-DOT_BORDER_ROUTING_DHCP6_PD=ON"
47+
"-DOT_SRP_CLIENT=ON"
48+
"-DOT_DNS_CLIENT=ON"
49+
"-DOT_TCP=ON"
50+
"-DOT_DNS_CLIENT_OVER_TCP=ON"
51+
"-DOT_FIREWALL=OFF"
52+
"-DOTBR_VENDOR_NAME=OpenThread.io"
53+
"-DOTBR_PRODUCT_NAME=RFSIM-OTBR-v1.4"
54+
"-DOTBR_SRP_SERVER_AUTO_ENABLE=ON"
55+
"-DOTBR_TREL=ON"
56+
"-DOTBR_NAT64=ON"
57+
"-DOTBR_DHCP6_PD=ON"
58+
"-DOTBR_DHCP6_PD_CLIENT=openthread"
59+
"-DOTBR_DBUS=ON"
60+
"-DOTBR_FEATURE_FLAGS=OFF"
61+
"-DOTBR_TELEMETRY_DATA_API=ON"
62+
"-DOTBR_WEB=ON"
63+
"-DOTBR_UNSECURE_JOIN=ON"
64+
"-DOTBR_LINK_METRICS_TELEMETRY=ON"
65+
)
66+
readonly OTBR_OPTIONS
67+
68+
main()
69+
{
70+
local options=()
71+
options+=("${OTBR_OPTIONS[@]}" "$@")
72+
73+
(
74+
cd ../../ot-br-posix
75+
./script/cmake-build "${options[@]}"
76+
)
77+
78+
echo "Build of 'otbr-agent' done."
79+
80+
read -rp "Copy otbr-agent to /usr/local/sbin for use by OTNS? [y/N] " answer || true
81+
if [[ ${answer} == [Yy] ]]; then
82+
sudo cp ../../ot-br-posix/build/otbr/src/agent/otbr-agent /usr/local/sbin
83+
echo "Copied otbr-agent to /usr/local/sbin"
84+
else
85+
echo "Skipped. To copy manually: sudo cp ../../ot-br-posix/build/otbr/src/agent/otbr-agent /usr/local/sbin"
86+
fi
87+
88+
read -rp "Copy ot-ctl to /usr/local/bin for use by OTNS? [y/N] " answer || true
89+
if [[ ${answer} == [Yy] ]]; then
90+
sudo cp ../../ot-br-posix/build/otbr/third_party/openthread/repo/src/posix/ot-ctl /usr/local/bin
91+
echo "Copied ot-ctl to /usr/local/bin"
92+
else
93+
echo "Skipped. To copy manually: sudo cp ../../ot-br-posix/build/otbr/third_party/openthread/repo/src/posix/ot-ctl /usr/local/bin"
94+
fi
95+
}
96+
97+
main "$@"

ot-rfsim/src/event-sim.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848
enum
4949
{
5050
OT_SIM_EVENT_ALARM_FIRED = 0,
51-
OT_SIM_EVENT_RADIO_RECEIVED = 1, // legacy
51+
OT_SIM_EVENT_RADIO_RECEIVED = 1, // not used / legacy
5252
OT_SIM_EVENT_UART_WRITE = 2,
5353
OT_SIM_EVENT_UART_DISCONNECTED = 3, // not used on OT node side
54-
OT_SIM_EVENT_POSTCMD = 4, // not used?
54+
OT_SIM_EVENT_POSTCMD = 4, // not used / legacy
5555
OT_SIM_EVENT_OTNS_STATUS_PUSH = 5,
5656
OT_SIM_EVENT_RADIO_COMM_START = 6,
5757
OT_SIM_EVENT_RADIO_TX_DONE = 7,

0 commit comments

Comments
 (0)