1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.thread;
18 
19 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
20 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
21 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
22 import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
23 import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
24 import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
25 import static android.net.thread.utils.IntegrationTestUtils.getPrefixesFromNetData;
26 import static android.net.thread.utils.IntegrationTestUtils.getThreadNetwork;
27 import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
28 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
29 
30 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
31 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
32 import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
33 
34 import static com.google.common.io.BaseEncoding.base16;
35 import static com.google.common.truth.Truth.assertThat;
36 import static com.google.common.truth.Truth.assertWithMessage;
37 
38 import android.content.Context;
39 import android.net.ConnectivityManager;
40 import android.net.InetAddresses;
41 import android.net.IpPrefix;
42 import android.net.LinkAddress;
43 import android.net.LinkProperties;
44 import android.net.thread.utils.FullThreadDevice;
45 import android.net.thread.utils.OtDaemonController;
46 import android.net.thread.utils.ThreadFeatureCheckerRule;
47 import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
48 import android.net.thread.utils.ThreadNetworkControllerWrapper;
49 import android.os.SystemClock;
50 
51 import androidx.test.core.app.ApplicationProvider;
52 import androidx.test.filters.LargeTest;
53 import androidx.test.runner.AndroidJUnit4;
54 
55 import org.junit.After;
56 import org.junit.Before;
57 import org.junit.Rule;
58 import org.junit.Test;
59 import org.junit.runner.RunWith;
60 
61 import java.io.IOException;
62 import java.net.DatagramPacket;
63 import java.net.DatagramSocket;
64 import java.net.Inet6Address;
65 import java.net.InetAddress;
66 import java.time.Duration;
67 import java.util.Arrays;
68 import java.util.List;
69 import java.util.concurrent.ExecutorService;
70 import java.util.concurrent.Executors;
71 
72 /** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
73 @LargeTest
74 @RequiresThreadFeature
75 @RunWith(AndroidJUnit4.class)
76 public class ThreadIntegrationTest {
77     // The byte[] buffer size for UDP tests
78     private static final int UDP_BUFFER_SIZE = 1024;
79 
80     // The maximum time for OT addresses to be propagated to the TUN interface "thread-wpan"
81     private static final Duration TUN_ADDR_UPDATE_TIMEOUT = Duration.ofSeconds(1);
82 
83     // The maximum time for changes to be propagated to netdata.
84     private static final Duration NET_DATA_UPDATE_TIMEOUT = Duration.ofSeconds(1);
85 
86     // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
87     private static final byte[] DEFAULT_DATASET_TLVS =
88             base16().decode(
89                             "0E080000000000010000000300001335060004001FFFE002"
90                                     + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
91                                     + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
92                                     + "642D643961300102D9A00410A245479C836D551B9CA557F7"
93                                     + "B9D351B40C0402A0FFF8");
94     private static final ActiveOperationalDataset DEFAULT_DATASET =
95             ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
96 
97     private static final Inet6Address GROUP_ADDR_ALL_ROUTERS =
98             (Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
99 
100     private static final String TEST_NO_SLAAC_PREFIX = "9101:dead:beef:cafe::/64";
101     private static final InetAddress TEST_NO_SLAAC_PREFIX_ADDRESS =
102             InetAddresses.parseNumericAddress("9101:dead:beef:cafe::");
103 
104     @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
105 
106     private ExecutorService mExecutor;
107     private final Context mContext = ApplicationProvider.getApplicationContext();
108     private final ThreadNetworkControllerWrapper mController =
109             ThreadNetworkControllerWrapper.newInstance(mContext);
110     private OtDaemonController mOtCtl;
111     private FullThreadDevice mFtd;
112 
113     @Before
setUp()114     public void setUp() throws Exception {
115         mExecutor = Executors.newSingleThreadExecutor();
116         mOtCtl = new OtDaemonController();
117         mController.leaveAndWait();
118 
119         // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
120         mOtCtl.factoryReset();
121 
122         mFtd = new FullThreadDevice(10 /* nodeId */);
123     }
124 
125     @After
tearDown()126     public void tearDown() throws Exception {
127         mController.setTestNetworkAsUpstreamAndWait(null);
128         mController.leaveAndWait();
129 
130         mFtd.destroy();
131         mExecutor.shutdownNow();
132     }
133 
134     @Test
otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped()135     public void otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped() throws Exception {
136         mController.leaveAndWait();
137 
138         runShellCommand("stop ot-daemon");
139         // TODO(b/323331973): the sleep is needed to workaround the race conditions
140         SystemClock.sleep(200);
141 
142         mController.waitForRole(DEVICE_ROLE_STOPPED, CALLBACK_TIMEOUT);
143     }
144 
145     @Test
otDaemonRestart_JoinedNetworkAndStopped_autoRejoinedAndTunIfStateConsistent()146     public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoinedAndTunIfStateConsistent()
147             throws Exception {
148         mController.joinAndWait(DEFAULT_DATASET);
149 
150         runShellCommand("stop ot-daemon");
151 
152         mController.waitForRole(DEVICE_ROLE_DETACHED, CALLBACK_TIMEOUT);
153         mController.waitForRole(DEVICE_ROLE_LEADER, RESTART_JOIN_TIMEOUT);
154         assertThat(mOtCtl.isInterfaceUp()).isTrue();
155         assertThat(runShellCommand("ifconfig thread-wpan")).contains("UP POINTOPOINT RUNNING");
156     }
157 
158     @Test
otDaemonFactoryReset_deviceRoleIsStopped()159     public void otDaemonFactoryReset_deviceRoleIsStopped() throws Exception {
160         mController.joinAndWait(DEFAULT_DATASET);
161 
162         mOtCtl.factoryReset();
163 
164         assertThat(mController.getDeviceRole()).isEqualTo(DEVICE_ROLE_STOPPED);
165     }
166 
167     @Test
otDaemonFactoryReset_addressesRemoved()168     public void otDaemonFactoryReset_addressesRemoved() throws Exception {
169         mController.joinAndWait(DEFAULT_DATASET);
170 
171         mOtCtl.factoryReset();
172 
173         String ifconfig = runShellCommand("ifconfig thread-wpan");
174         assertThat(ifconfig).doesNotContain("inet6 addr");
175     }
176 
177     // TODO (b/323300829): add test for removing an OT address
178     @Test
tunInterface_joinedNetwork_otAndTunAddressesMatch()179     public void tunInterface_joinedNetwork_otAndTunAddressesMatch() throws Exception {
180         mController.joinAndWait(DEFAULT_DATASET);
181 
182         List<Inet6Address> otAddresses = mOtCtl.getAddresses();
183         assertThat(otAddresses).isNotEmpty();
184         // TODO: it's cleaner to have a retry() method to retry failed asserts in given delay so
185         // that we can write assertThat() in the Predicate
186         waitFor(
187                 () -> {
188                     List<Inet6Address> tunAddresses =
189                             getIpv6LinkAddresses("thread-wpan").stream()
190                                     .map(linkAddr -> (Inet6Address) linkAddr.getAddress())
191                                     .toList();
192                     return otAddresses.containsAll(tunAddresses)
193                             && tunAddresses.containsAll(otAddresses);
194                 },
195                 TUN_ADDR_UPDATE_TIMEOUT);
196     }
197 
198     @Test
otDaemonRestart_latestCountryCodeIsSetToOtDaemon()199     public void otDaemonRestart_latestCountryCodeIsSetToOtDaemon() throws Exception {
200         runThreadCommand("force-country-code enabled CN");
201 
202         runShellCommand("stop ot-daemon");
203         // TODO(b/323331973): the sleep is needed to workaround the race conditions
204         SystemClock.sleep(200);
205         mController.waitForRole(DEVICE_ROLE_STOPPED, CALLBACK_TIMEOUT);
206 
207         assertThat(mOtCtl.getCountryCode()).isEqualTo("CN");
208     }
209 
210     @Test
udp_appStartEchoServer_endDeviceUdpEchoSuccess()211     public void udp_appStartEchoServer_endDeviceUdpEchoSuccess() throws Exception {
212         // Topology:
213         //   Test App ------ thread-wpan ------ End Device
214 
215         mController.joinAndWait(DEFAULT_DATASET);
216         startFtdChild(mFtd, DEFAULT_DATASET);
217         final Inet6Address serverAddress = mOtCtl.getMeshLocalAddresses().get(0);
218         final int serverPort = 9527;
219 
220         mExecutor.execute(() -> startUdpEchoServerAndWait(serverAddress, serverPort));
221         mFtd.udpOpen();
222         mFtd.udpSend("Hello,Thread", serverAddress, serverPort);
223         String udpReply = mFtd.udpReceive();
224 
225         assertThat(udpReply).isEqualTo("Hello,Thread");
226     }
227 
228     @Test
joinNetworkWithBrDisabled_meshLocalAddressesArePreferred()229     public void joinNetworkWithBrDisabled_meshLocalAddressesArePreferred() throws Exception {
230         // When BR feature is disabled, there is no OMR address, so the mesh-local addresses are
231         // expected to be preferred.
232         mOtCtl.executeCommand("br disable");
233         mController.joinAndWait(DEFAULT_DATASET);
234 
235         IpPrefix meshLocalPrefix = DEFAULT_DATASET.getMeshLocalPrefix();
236         List<LinkAddress> linkAddresses = getIpv6LinkAddresses("thread-wpan");
237         for (LinkAddress address : linkAddresses) {
238             if (meshLocalPrefix.contains(address.getAddress())) {
239                 assertThat(address.getDeprecationTime())
240                         .isGreaterThan(SystemClock.elapsedRealtime());
241                 assertThat(address.isPreferred()).isTrue();
242             }
243         }
244 
245         mOtCtl.executeCommand("br enable");
246     }
247 
248     @Test
joinNetwork_tunInterfaceJoinsAllRouterMulticastGroup()249     public void joinNetwork_tunInterfaceJoinsAllRouterMulticastGroup() throws Exception {
250         mController.joinAndWait(DEFAULT_DATASET);
251 
252         assertTunInterfaceMemberOfGroup(GROUP_ADDR_ALL_ROUTERS);
253     }
254 
255     @Test
edPingsMeshLocalAddresses_oneReplyPerRequest()256     public void edPingsMeshLocalAddresses_oneReplyPerRequest() throws Exception {
257         mController.joinAndWait(DEFAULT_DATASET);
258         startFtdChild(mFtd, DEFAULT_DATASET);
259         List<Inet6Address> meshLocalAddresses = mOtCtl.getMeshLocalAddresses();
260 
261         for (Inet6Address address : meshLocalAddresses) {
262             assertWithMessage(
263                             "There may be duplicated replies of ping request to "
264                                     + address.getHostAddress())
265                     .that(mFtd.ping(address, 2))
266                     .isEqualTo(2);
267         }
268     }
269 
270     @Test
addPrefixToNetData_routeIsAddedToTunInterface()271     public void addPrefixToNetData_routeIsAddedToTunInterface() throws Exception {
272         ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
273         mController.joinAndWait(DEFAULT_DATASET);
274 
275         // Ftd child doesn't have the ability to add a prefix, so let BR itself add a prefix.
276         mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
277         mOtCtl.executeCommand("netdata register");
278         waitFor(
279                 () -> {
280                     String netData = mOtCtl.executeCommand("netdata show");
281                     return getPrefixesFromNetData(netData).contains(TEST_NO_SLAAC_PREFIX);
282                 },
283                 NET_DATA_UPDATE_TIMEOUT);
284 
285         LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
286         assertThat(lp).isNotNull();
287         assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
288                 .isTrue();
289     }
290 
291     @Test
removePrefixFromNetData_routeIsRemovedFromTunInterface()292     public void removePrefixFromNetData_routeIsRemovedFromTunInterface() throws Exception {
293         ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
294         mController.joinAndWait(DEFAULT_DATASET);
295         mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
296         mOtCtl.executeCommand("netdata register");
297 
298         mOtCtl.executeCommand("prefix remove " + TEST_NO_SLAAC_PREFIX);
299         mOtCtl.executeCommand("netdata register");
300         waitFor(
301                 () -> {
302                     String netData = mOtCtl.executeCommand("netdata show");
303                     return !getPrefixesFromNetData(netData).contains(TEST_NO_SLAAC_PREFIX);
304                 },
305                 NET_DATA_UPDATE_TIMEOUT);
306 
307         LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
308         assertThat(lp).isNotNull();
309         assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
310                 .isFalse();
311     }
312 
313     @Test
toggleThreadNetwork_routeFromPreviousNetDataIsRemoved()314     public void toggleThreadNetwork_routeFromPreviousNetDataIsRemoved() throws Exception {
315         ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
316         mController.joinAndWait(DEFAULT_DATASET);
317         mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
318         mOtCtl.executeCommand("netdata register");
319 
320         mController.leaveAndWait();
321         mOtCtl.factoryReset();
322         mController.joinAndWait(DEFAULT_DATASET);
323 
324         LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
325         assertThat(lp).isNotNull();
326         assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
327                 .isFalse();
328     }
329 
330     // TODO (b/323300829): add more tests for integration with linux platform and
331     // ConnectivityService
332 
runThreadCommand(String cmd)333     private static String runThreadCommand(String cmd) {
334         return runShellCommandOrThrow("cmd thread_network " + cmd);
335     }
336 
startFtdChild(FullThreadDevice ftd, ActiveOperationalDataset activeDataset)337     private void startFtdChild(FullThreadDevice ftd, ActiveOperationalDataset activeDataset)
338             throws Exception {
339         ftd.factoryReset();
340         ftd.joinNetwork(activeDataset);
341         ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8));
342     }
343 
344     /**
345      * Starts a UDP echo server and replies to the first UDP message.
346      *
347      * <p>This method exits when the first UDP message is received and the reply is sent
348      */
startUdpEchoServerAndWait(InetAddress serverAddress, int serverPort)349     private void startUdpEchoServerAndWait(InetAddress serverAddress, int serverPort) {
350         try (var udpServerSocket = new DatagramSocket(serverPort, serverAddress)) {
351             DatagramPacket recvPacket =
352                     new DatagramPacket(new byte[UDP_BUFFER_SIZE], UDP_BUFFER_SIZE);
353             udpServerSocket.receive(recvPacket);
354             byte[] sendBuffer = Arrays.copyOf(recvPacket.getData(), recvPacket.getData().length);
355             udpServerSocket.send(
356                     new DatagramPacket(
357                             sendBuffer,
358                             sendBuffer.length,
359                             (Inet6Address) recvPacket.getAddress(),
360                             recvPacket.getPort()));
361         } catch (IOException e) {
362             throw new IllegalStateException(e);
363         }
364     }
365 
assertTunInterfaceMemberOfGroup(Inet6Address address)366     private void assertTunInterfaceMemberOfGroup(Inet6Address address) throws Exception {
367         waitFor(() -> isInMulticastGroup(TUN_IF_NAME, address), TUN_ADDR_UPDATE_TIMEOUT);
368     }
369 }
370