Skip to content

Commit eb468e9

Browse files
authored
[tests] add docker-in-docker NAT64 integration test (#3387)
Add a new integration test script to cover NAT64 functionality in the Docker-in-Docker (DinD) test runner. - Spawns a simulated FTD client, an OTBR container with RCP mode, and a sleeping IPv4 server container connected on the infrastructure-link bridge network. - Verifies that NAT64 translator and PrefixManager are successfully initialized in the Active state. - Queries the local NAT64 prefix and confirms that the Thread client successfully discovers it in the network data with the NAT64 ('n') route flag. - Validates the data plane by pinging the server's IPv4 address from the Thread client (which triggers automatic synthesizing of the IPv6 address). - Audits the translation mappings to ensure that the client's source address is translated to a NAT64 CIDR-allocated IPv4 address (e.g. 192.168.255.1). - Audits packet/byte translator counters to confirm non-zero traffic traversal in the 4-to-6 and 6-to-4 directions. - Integrates the new expect script in the test_dind_dns_sd.sh runner sequence, running before DHCPv6 PD test.
1 parent 43b3555 commit eb468e9

2 files changed

Lines changed: 273 additions & 0 deletions

File tree

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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+
source "tests/scripts/expect/_common.exp"
31+
32+
# Custom start_otbr_docker for our network name
33+
proc start_otbr_docker_dind {name sim_app sim_id pty} {
34+
global rcp_pids
35+
track_container $name
36+
# Start the container using its native /init entrypoint (s6-overlay) and custom environment variables
37+
exec docker run -d \
38+
--name $name \
39+
--network infrastructure-link \
40+
--cap-add=NET_ADMIN \
41+
--privileged \
42+
--add-host=host.docker.internal:host-gateway \
43+
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
44+
--sysctl net.ipv4.conf.all.forwarding=1 \
45+
--sysctl net.ipv4.conf.default.forwarding=1 \
46+
--sysctl net.ipv6.conf.all.forwarding=1 \
47+
--sysctl net.ipv6.conf.default.forwarding=1 \
48+
-v $::env(EXP_REPO_ROOT)/tests/scripts/spinel_proxy.sh:/usr/bin/spinel_proxy.sh \
49+
-e OT_RCP_DEVICE=spinel+hdlc+forkpty:///usr/bin/spinel_proxy.sh \
50+
-e OT_INFRA_IF=eth0 \
51+
-e OT_THREAD_IF=wpan0 \
52+
$::env(EXP_OTBR_DOCKER_IMAGE)
53+
sleep 2
54+
55+
# Now start the simulated RCP binary on the host!
56+
set rcp_pid [exec $sim_app $sim_id <$pty >$pty &]
57+
lappend rcp_pids $rcp_pid
58+
sleep 5
59+
}
60+
61+
set pty1 [create_socat_tcp 1 9000]
62+
set container "otbr-test-container"
63+
set ipv4_server "ipv4-server"
64+
65+
set dataset "0e080000000000010000000300001435060004001fffe002087d61eb42cdc48d6a0708fd0d07fca1b9f0500510ba088fc2bd6c3b3897f7a10f58263ff3030f4f70656e5468726561642d353234660102524f04109dc023ccd447b12b50997ef68020f19e0c0402a0f7f8"
66+
67+
# 1. Start the IPv4 Server container on infrastructure-link
68+
send_user "Starting IPv4 Server container...\n"
69+
track_container $ipv4_server
70+
exec docker run -d \
71+
--name $ipv4_server \
72+
--network infrastructure-link \
73+
--entrypoint sleep \
74+
$::env(EXP_TEST_CLIENT_IMAGE) \
75+
86400
76+
77+
# Retrieve the IPv4 address of the server container
78+
set format {{{(index .NetworkSettings.Networks "infrastructure-link").IPAddress}}}
79+
set server_ip4 ""
80+
for {set i 0} {$i < 10} {incr i} {
81+
if {![catch {exec docker inspect $ipv4_server -f $format} inspect_out]} {
82+
set server_ip4 [string trim $inspect_out]
83+
if {$server_ip4 != "" && [regexp {^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$} $server_ip4]} {
84+
break
85+
}
86+
}
87+
sleep 1
88+
}
89+
if {$server_ip4 == ""} {
90+
fail "Failed to retrieve IPv4 address for $ipv4_server"
91+
}
92+
send_user "IPv4 Server IP address: $server_ip4\n"
93+
94+
# 2. Start border router in Docker on the DinD host
95+
send_user "Starting OTBR Docker container...\n"
96+
start_otbr_docker_dind $container $::env(EXP_OT_RCP_PATH) 2 $pty1
97+
98+
# Wait for otbr-agent to create the domain socket
99+
set socket_ready false
100+
for {set i 0} {$i < 10} {incr i} {
101+
if {![catch {exec docker exec -i $container ot-ctl state}]} {
102+
set socket_ready true
103+
break
104+
}
105+
sleep 2
106+
}
107+
if {!$socket_ready} {
108+
fail "ot-ctl failed to communicate with otbr-agent"
109+
}
110+
111+
# Setup active dataset on OTBR via ot-ctl
112+
exec docker exec -i $container ot-ctl thread stop
113+
exec docker exec -i $container ot-ctl ifconfig down
114+
exec docker exec -i $container ot-ctl dataset set active ${dataset}
115+
exec docker exec -i $container ot-ctl ifconfig up
116+
exec docker exec -i $container ot-ctl thread start
117+
118+
# Wait for OTBR to become leader
119+
set leader false
120+
for {set i 0} {$i < 20} {incr i} {
121+
if {![catch {exec docker exec -i $container ot-ctl state} state] && [string match "*leader*" $state]} {
122+
set leader true
123+
break
124+
}
125+
sleep 2
126+
}
127+
if {!$leader} {
128+
fail "OTBR did not become leader"
129+
}
130+
131+
# 3. Enable NAT64 explicitly on the OTBR
132+
send_user "Enabling NAT64 on the OTBR...\n"
133+
exec docker exec -i $container ot-ctl nat64 enable
134+
# Verify NAT64 state with retries
135+
set nat64_active false
136+
for {set i 0} {$i < 10} {incr i} {
137+
if {![catch {exec docker exec -i $container ot-ctl nat64 state} nat64_state]} {
138+
set nat64_state [string trim $nat64_state]
139+
send_user "NAT64 State:\n$nat64_state\n"
140+
if {[string match "*PrefixManager: Active*" $nat64_state] && [string match "*Translator: Active*" $nat64_state]} {
141+
set nat64_active true
142+
break
143+
}
144+
}
145+
sleep 2
146+
}
147+
if {!$nat64_active} {
148+
fail "NAT64 PrefixManager or Translator is not active on the Border Router"
149+
}
150+
151+
# Query the local NAT64 prefix advertised by the Border Router
152+
set nat64_prefix ""
153+
for {set i 0} {$i < 10} {incr i} {
154+
if {![catch {exec docker exec -i $container ot-ctl br nat64prefix local} prefix_out]} {
155+
set prefix_out [string trim $prefix_out]
156+
if {[regexp {([0-9a-fA-F:]+/96)} $prefix_out match prefix]} {
157+
set nat64_prefix $prefix
158+
break
159+
}
160+
}
161+
sleep 2
162+
}
163+
if {$nat64_prefix == ""} {
164+
fail "Failed to retrieve local NAT64 prefix from Border Router"
165+
}
166+
send_user "Discovered local NAT64 prefix: $nat64_prefix\n"
167+
168+
# Get the running Border Router's active dataset dynamically to configure the client
169+
set active_dataset [exec docker exec -i $container ot-ctl dataset active -x]
170+
set active_dataset [lindex [split [string trim $active_dataset] "\n"] 0]
171+
172+
# 4. Join Thread network from Node 4 (ot-cli-ftd)
173+
send_user "Starting Simulated Thread Device (Node 4)...\n"
174+
spawn_node 4 cli $::env(EXP_OT_CLI_PATH)
175+
send "dataset set active $active_dataset\n"
176+
expect_line "Done"
177+
send "mode rdn\n"
178+
expect_line "Done"
179+
send "routereligible disable\n"
180+
expect_line "Done"
181+
send "ifconfig up\n"
182+
expect_line "Done"
183+
send "thread start\n"
184+
expect_line "Done"
185+
wait_for "state" "child"
186+
expect_line "Done"
187+
188+
# 5. Verify NAT64 prefix discovery on Thread Client Node 4
189+
send_user "Verifying NAT64 prefix discovery on Thread Client Node 4...\n"
190+
set discovered_prefix false
191+
for {set i 0} {$i < 15} {incr i} {
192+
send "netdata show\n"
193+
set timeout 3
194+
expect {
195+
-re "Routes:" {
196+
exp_continue
197+
}
198+
-re {([0-9a-fA-F:]+/96) sn} {
199+
set client_prefix $expect_out(1,string)
200+
if {[string equal -nocase $client_prefix $nat64_prefix]} {
201+
set discovered_prefix true
202+
}
203+
exp_continue
204+
}
205+
-re "Done\r?\n> " {
206+
# finished parsing
207+
}
208+
timeout {
209+
# do nothing
210+
}
211+
}
212+
if {$discovered_prefix} {
213+
break
214+
}
215+
sleep 2
216+
}
217+
if {!$discovered_prefix} {
218+
fail "Thread Client Node 4 did not discover the NAT64 prefix in Network Data"
219+
}
220+
send_user "Thread Client Node 4 successfully discovered NAT64 prefix: $nat64_prefix\n"
221+
222+
# 6. Verify NAT64 Translation & Bidirectional Routing (Ping from Thread Client Node 4 to Infrastructure IPv4 Host)
223+
send_user "Pinging IPv4 server ($server_ip4) from Thread Client Node 4...\n"
224+
switch_node 4
225+
send "ping $server_ip4 56 5\n"
226+
set timeout 15
227+
set ping_success false
228+
expect {
229+
-re {bytes from} {
230+
set ping_success true
231+
exp_continue
232+
}
233+
-re "Done\r?\n> " {
234+
# finished parsing
235+
}
236+
timeout {
237+
send_user "Error: Ping timed out\n"
238+
}
239+
}
240+
if {!$ping_success} {
241+
fail "NAT64 Ping verification failed"
242+
}
243+
send_user "Ping from Thread Client to IPv4 Server through NAT64 succeeded!\n"
244+
245+
# 7. Verify NAT64 mappings and counters on the Border Router
246+
send_user "Auditing NAT64 mappings and counters on the Border Router...\n"
247+
set mappings [exec docker exec -i $container ot-ctl nat64 mappings]
248+
send_user "Active NAT64 Mappings:\n$mappings\n"
249+
if {![string match "*192.168.255.*" $mappings]} {
250+
fail "No active mapping entry found in the NAT64 translator"
251+
}
252+
send_user "NAT64 mapping verified successfully.\n"
253+
254+
set counters [exec docker exec -i $container ot-ctl nat64 counters]
255+
send_user "NAT64 Counters:\n$counters\n"
256+
set counters_verified false
257+
if {[regexp {Total\s+\|\s+([1-9][0-9]*)} $counters match total_rx_packets]} {
258+
send_user "Total Translated Packets: $total_rx_packets\n"
259+
set counters_verified true
260+
}
261+
if {!$counters_verified} {
262+
fail "NAT64 translation counters did not register any translated packets"
263+
}
264+
send_user "NAT64 counters verified successfully.\n"
265+
266+
# 8. Clean up containers
267+
send_user "Tearing down containers...\n"
268+
dispose_all
269+
send_user "# NAT64 integration test completed successfully!\n"

tests/scripts/test_dind_dns_sd.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,12 @@ test_run()
186186
echo "--- Running TREL integration test ---"
187187
expect -df "${SCRIPT_DIR}/expect/dind_trel.exp"
188188

189+
echo "--- Running NAT64 integration test ---"
190+
expect -df "${SCRIPT_DIR}/expect/dind_nat64.exp"
191+
189192
echo "--- Running DHCPv6 Prefix Delegation integration test ---"
190193
expect -df "${SCRIPT_DIR}/expect/dind_dhcp6_pd.exp"
194+
191195
if [[ "$(uname)" != "Darwin" && "$(uname -r)" != *"linuxkit"* ]]; then
192196
echo "--- Running BBR Multicast Forwarding integration test ---"
193197
expect -df "${SCRIPT_DIR}/expect/dind_multicast.exp"

0 commit comments

Comments
 (0)