Skip to content

Commit e4b1afd

Browse files
committed
[docker] add TREL automated integration test suite in DinD
Adds automated integration testing for the Thread Radio Encapsulation Link (TREL) feature in the Docker-in-Docker (DinD) test environment. Specifically: - Refactors tests/scripts/spinel_proxy.sh to support EXP_RCP_PORT for concurrent container simulation relays on shared bridge links. - Modifies tests/scripts/test_dind_dns_sd.sh to sequentially orchestrate dind_dns_sd.exp and dind_trel.exp expect suites, optimizing CI build layers caching while extending cleanup traps to drop PTY and bridges. - Implements tests/scripts/expect/dind_trel.exp to orchestrate PTY socat tunnels, configure datasets pre-boot, wait for child attachment, assert mDNS peer discovery via glob matches, parse unicast routable route targets, and verify TREL keep-alive inbound packet counters.
1 parent 2323cd1 commit e4b1afd

3 files changed

Lines changed: 301 additions & 2 deletions

File tree

tests/scripts/expect/dind_trel.exp

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
#!/usr/bin/expect -f
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+
proc fail {message} {
31+
send_user "Error: $message\n"
32+
exit 1
33+
}
34+
35+
source "tests/scripts/expect/_common.exp"
36+
37+
proc create_socat_tcp {id port} {
38+
spawn socat -d -d pty,raw,echo=0 tcp-listen:$port,reuseaddr,bind=0.0.0.0
39+
set pty ""
40+
expect {
41+
-re {PTY is (\S+)} {
42+
set pty $expect_out(1,string)
43+
}
44+
timeout {
45+
send_user "Error: Timed out starting socat for id $id on port $port\n"; exit 1
46+
}
47+
}
48+
return $pty
49+
}
50+
51+
# Dynamic helper to start the OTBR docker container and map the spinel relay
52+
proc start_otbr_docker_trel {name sim_app sim_id pty rcp_port} {
53+
exec docker run -d \
54+
--name $name \
55+
--network infrastructure-link \
56+
--cap-add=NET_ADMIN \
57+
--privileged \
58+
--add-host=host.docker.internal:host-gateway \
59+
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
60+
--sysctl net.ipv4.conf.all.forwarding=1 \
61+
--sysctl net.ipv4.conf.default.forwarding=1 \
62+
--sysctl net.ipv6.conf.all.forwarding=1 \
63+
--sysctl net.ipv6.conf.default.forwarding=1 \
64+
-v $::env(EXP_REPO_ROOT)/tests/scripts/spinel_proxy.sh:/usr/bin/spinel_proxy.sh \
65+
-e OT_RCP_DEVICE=spinel+hdlc+forkpty:///usr/bin/spinel_proxy.sh \
66+
-e OT_INFRA_IF=eth0 \
67+
-e OT_THREAD_IF=wpan0 \
68+
-e EXP_RCP_PORT=$rcp_port \
69+
$::env(EXP_OTBR_DOCKER_IMAGE)
70+
sleep 2
71+
72+
# Start the simulated RCP binary on the host
73+
exec $sim_app $sim_id <$pty >$pty &
74+
sleep 5
75+
}
76+
77+
# Start relays and containers
78+
set pty1 [create_socat_tcp 1 9000]
79+
set pty2 [create_socat_tcp 2 9001]
80+
81+
set container1 "otbr-test-container-1"
82+
set container2 "otbr-test-container-2"
83+
84+
set dataset "0e080000000000010000000300001435060004001fffe002087d61eb42cdc48d6a0708fd0d07fca1b9f0500510ba088fc2bd6c3b3897f7a10f58263ff3030f4f70656e5468726561642d353234660102524f04109dc023ccd447b12b50997ef68020f19e0c0402a0f7f8"
85+
86+
send_user -- "--- Starting container 1 ---\n"
87+
start_otbr_docker_trel $container1 $::env(EXP_OT_RCP_PATH) 2 $pty1 9000
88+
89+
send_user -- "--- Starting container 2 ---\n"
90+
start_otbr_docker_trel $container2 $::env(EXP_OT_RCP_PATH) 3 $pty2 9001
91+
92+
# Ensure both containers have initialized correctly and otbr-agent socket is ready
93+
foreach c [list $container1 $container2] {
94+
set socket_ready false
95+
for {set i 0} {$i < 10} {incr i} {
96+
if {![catch {exec docker exec -i $c ot-ctl state}]} {
97+
set socket_ready true
98+
break
99+
}
100+
sleep 2
101+
}
102+
if {!$socket_ready} {
103+
send_user "Error: ot-ctl failed to communicate with otbr-agent on $c\n"; exit 1
104+
}
105+
}
106+
107+
# Stop Thread and down interface on both containers initially to prevent default auto-start
108+
foreach c [list $container1 $container2] {
109+
exec docker exec -i $c ot-ctl thread stop
110+
exec docker exec -i $c ot-ctl ifconfig down
111+
}
112+
113+
# 1. Configure Active Dataset on both containers BEFORE starting Thread
114+
send_user -- "--- Configuring Active Dataset on both containers ---\n"
115+
exec docker exec -i $container1 ot-ctl dataset set active $dataset
116+
exec docker exec -i $container2 ot-ctl dataset set active $dataset
117+
118+
# Start Thread on Container 1 to form the network
119+
send_user -- "--- Starting Thread on Container 1 ---\n"
120+
exec docker exec -i $container1 ot-ctl ifconfig up
121+
exec docker exec -i $container1 ot-ctl thread start
122+
123+
# Wait for Container 1 to become leader
124+
set leader false
125+
for {set i 0} {$i < 20} {incr i} {
126+
spawn docker exec -i $container1 ot-ctl state
127+
set timeout 3
128+
expect {
129+
"leader" {
130+
set leader true
131+
break
132+
}
133+
timeout {}
134+
}
135+
catch {close}; catch {wait}
136+
sleep 2
137+
}
138+
if {!$leader} {
139+
send_user "Error: Container 1 did not become leader.\n"; exit 1
140+
}
141+
142+
# 2. Start Thread on Container 2 to join the network
143+
send_user -- "--- Starting Thread on Container 2 ---\n"
144+
exec docker exec -i $container2 ot-ctl ifconfig up
145+
exec docker exec -i $container2 ot-ctl thread start
146+
147+
# Wait for Container 2 to join successfully (router or child state)
148+
set joined false
149+
for {set i 0} {$i < 25} {incr i} {
150+
spawn docker exec -i $container2 ot-ctl state
151+
set timeout 3
152+
expect {
153+
"router" {
154+
set joined true
155+
break
156+
}
157+
"child" {
158+
set joined true
159+
break
160+
}
161+
timeout {}
162+
}
163+
catch {close}; catch {wait}
164+
sleep 2
165+
}
166+
if {!$joined} {
167+
send_user "Error: Container 2 failed to join the network.\n"; exit 1
168+
}
169+
170+
# 3. Block simulated 15.4 radio link between Container 1 and Container 2 using macfilter, forcing traffic over TREL
171+
send_user -- "--- Fetching Extended MAC Addresses for firewall rules ---\n"
172+
set otbr1_extaddr [exec docker exec -i $container1 ot-ctl extaddr]
173+
set otbr1_extaddr [lindex [split $otbr1_extaddr "\n"] 0]
174+
set otbr1_extaddr [string trim $otbr1_extaddr]
175+
set otbr2_extaddr [exec docker exec -i $container2 ot-ctl extaddr]
176+
set otbr2_extaddr [lindex [split $otbr2_extaddr "\n"] 0]
177+
set otbr2_extaddr [string trim $otbr2_extaddr]
178+
179+
180+
181+
# Retrieve infrastructure global IP address of both containers to verify TREL SocketAddress mappings
182+
set format "\{\{range .NetworkSettings.Networks\}\}\{\{.GlobalIPv6Address\}\}\{\{end\}\}"
183+
set otbr1_infra_ip [exec docker inspect $container1 -f $format]
184+
set otbr1_infra_ip [string trim $otbr1_infra_ip]
185+
186+
set otbr2_infra_ip [exec docker inspect $container2 -f $format]
187+
set otbr2_infra_ip [string trim $otbr2_infra_ip]
188+
189+
send_user "Container 1 Infra IP: $otbr1_infra_ip\n"
190+
send_user "Container 2 Infra IP: $otbr2_infra_ip\n"
191+
192+
# Allow time for DNS-SD to perform TREL peer registration and browsing over local infrastructure bridge netif
193+
send_user "Waiting 15 seconds for TREL peer discovery to execute...\n"
194+
sleep 15
195+
196+
# 4. Verify peer discovery via 'trel peers' matching peer's Extended MAC address using glob wildcards
197+
send_user -- "--- Verifying TREL peer discovery on Container 1 ---\n"
198+
spawn docker exec -i $container1 ot-ctl trel peers
199+
set timeout 10
200+
expect {
201+
"*$otbr2_extaddr*" {
202+
send_user "Found Container 2 TREL peer (Ext MAC: $otbr2_extaddr) on Container 1!\n"
203+
}
204+
timeout {
205+
fail "Error: Container 2 TREL peer not discovered on Container 1 (Timeout)."
206+
}
207+
eof {
208+
fail "Error: Container 2 TREL peer not discovered on Container 1 (EOF)."
209+
}
210+
}
211+
catch {close}; catch {wait}
212+
213+
send_user -- "--- Verifying TREL peer discovery on Container 2 ---\n"
214+
spawn docker exec -i $container2 ot-ctl trel peers
215+
set timeout 10
216+
expect {
217+
"*$otbr1_extaddr*" {
218+
send_user "Found Container 1 TREL peer (Ext MAC: $otbr1_extaddr) on Container 2!\n"
219+
}
220+
timeout {
221+
fail "Error: Container 1 TREL peer not discovered on Container 2 (Timeout)."
222+
}
223+
eof {
224+
fail "Error: Container 1 TREL peer not discovered on Container 2 (EOF)."
225+
}
226+
}
227+
catch {close}; catch {wait}
228+
229+
# 5. Ping peer and verify UDP packet encapsulation counters
230+
set otbr2_addrs [exec docker exec -i $container2 ot-ctl ipaddr]
231+
set ping_target ""
232+
foreach addr [split $otbr2_addrs "\n"] {
233+
set addr [string trim $addr]
234+
if {[string match "fd*" $addr] && ![string match "*ff:fe00:*" $addr]} {
235+
set ping_target $addr
236+
break
237+
}
238+
}
239+
if {$ping_target == ""} {
240+
fail "Error: Could not locate a routable Thread OMR/ML-EID address for Container 2."
241+
}
242+
send_user "Pinging Container 2 Thread Routable Address: $ping_target\n"
243+
244+
spawn docker exec -i $container1 ot-ctl ping $ping_target
245+
set timeout 10
246+
expect {
247+
"16 bytes from" {
248+
send_user "Ping succeeded!\n"
249+
}
250+
timeout {
251+
fail "Error: Ping timed out."
252+
}
253+
eof {
254+
fail "Error: Ping failed (EOF)."
255+
}
256+
}
257+
catch {close}; catch {wait}
258+
259+
# Verify TREL link counters (either keep-alives or data packets must be present)
260+
spawn docker exec -i $container1 ot-ctl trel counters
261+
expect {
262+
-re {Inbound:\s+Packets\s+([1-9][0-9]*)} {
263+
send_user "TREL link traffic verified: Inbound $expect_out(1,string) packets received over TREL!\n"
264+
}
265+
timeout {
266+
fail "Error: TREL Inbound counter is zero (Timeout)."
267+
}
268+
eof {
269+
fail "Error: TREL Inbound counter is zero (EOF)."
270+
}
271+
}
272+
catch {close}; catch {wait}
273+
274+
# Clean up containers
275+
exec docker stop $container1
276+
exec docker rm $container1
277+
exec docker stop $container2
278+
exec docker rm $container2
279+
280+
send_user -- "--- TREL Integration Test PASSED successfully! ---\n"
281+
exit 0

tests/scripts/spinel_proxy.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
# POSSIBILITY OF SUCH DAMAGE.
2727
#
2828

29-
exec 3<>/dev/tcp/"${EXP_GATEWAY_IP:-host.docker.internal}"/9000
29+
exec 3<>/dev/tcp/"${EXP_GATEWAY_IP:-host.docker.internal}"/"${EXP_RCP_PORT:-9000}"
3030
stdbuf -i0 -o0 -e0 cat <&3 &
3131
stdbuf -i0 -o0 -e0 cat >&3
3232
kill $!

tests/scripts/test_dind_dns_sd.sh

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,24 @@ test_teardown()
7474
docker ps -a || true
7575
echo "Container logs:"
7676
docker logs "${CONTAINER_NAME}" || true
77+
docker logs "otbr-test-container-1" || true
78+
docker logs "otbr-test-container-2" || true
7779
echo "Test client logs:"
7880
docker logs "test-client" || true
7981
echo "Agent logs:"
8082
docker exec -i "${CONTAINER_NAME}" cat /var/log/otbr-agent.log || true
83+
docker exec -i "otbr-test-container-1" cat /var/log/otbr-agent.log || true
84+
docker exec -i "otbr-test-container-2" cat /var/log/otbr-agent.log || true
85+
8186
docker stop "${CONTAINER_NAME}" || true
87+
docker stop "otbr-test-container-1" || true
88+
docker stop "otbr-test-container-2" || true
89+
docker stop "test-client" || true
90+
docker rm "${CONTAINER_NAME}" || true
91+
docker rm "otbr-test-container-1" || true
92+
docker rm "otbr-test-container-2" || true
93+
docker rm "test-client" || true
94+
8295
for c in $(docker network inspect "${INFRA_NET_NAME}" -f '{{range $id, $el := .Containers}}{{$id}} {{end}}' 2>/dev/null || true); do
8396
docker stop "$c" || true
8497
docker rm "$c" || true
@@ -87,6 +100,7 @@ test_teardown()
87100
docker network rm "${INFRA_NET_NAME}" || true
88101
pkill -f ot-rcp || true
89102
pkill -f ot-cli-ftd || true
103+
pkill -f socat || true
90104
rm -vf /tmp/otbr-pty1 /tmp/otbr-pty2 || true
91105
rm -vf "${REPO_ROOT}"/*.flash* || true
92106
}
@@ -147,7 +161,7 @@ test_run()
147161
{
148162
trap test_teardown EXIT
149163

150-
echo "--- Running integration expect script ---"
164+
echo "--- Running integration expect scripts ---"
151165
export EXP_OTBR_DOCKER_IMAGE="${OTBR_DOCKER_IMAGE}"
152166
export EXP_OTBR_AGENT_PATH="${REPO_ROOT}/build/src/agent/otbr-agent"
153167
export EXP_TUN_NAME="wpan0"
@@ -165,7 +179,11 @@ test_run()
165179
export EXP_OT_CLI_PATH="$ot_cli"
166180
export EXP_OT_RCP_PATH="$ot_rcp"
167181

182+
echo "--- Running DNS-SD integration test ---"
168183
expect -df "${SCRIPT_DIR}/expect/dind_dns_sd.exp"
184+
185+
echo "--- Running TREL integration test ---"
186+
expect -df "${SCRIPT_DIR}/expect/dind_trel.exp"
169187
}
170188

171189
main()

0 commit comments

Comments
 (0)