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