From b5c6ac2fe7302a81c6bc4877ad2cbdf683fb1919 Mon Sep 17 00:00:00 2001 From: Handa Wang <handaw@google.com> Date: Mon, 29 Jan 2024 02:17:07 +0000 Subject: [PATCH] Add a test case that a Border Router sends a UDP message to Thread device This test case lets the BR send a UDP message to Thread device using socket API and verify the message is received on the Thread device via ot-ctl udp command. For now this only verifies the direction BR -> ED because the other direction ED -> BR is depending on platform UDP feature which is not enabled yet. Bug: 321168973 Test: atest ThreadNetworkIntegrationTests:BorderRoutingTest#unicastRouting_borderRouterSendsUdpToThreadDevice_datagramReceived Change-Id: I590583dd818f3d72dec41184e5427a8b08f842bb --- .../android/net/thread/BorderRoutingTest.java | 46 +++++++++++++++++++ .../net/thread/utils/FullThreadDevice.java | 35 ++++++++++++++ .../thread/utils/IntegrationTestUtils.java | 28 +++++++++++ 3 files changed, 109 insertions(+) diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java index 29ada1b004..2fccf6bdfc 100644 --- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java +++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java @@ -21,10 +21,12 @@ import static android.Manifest.permission.NETWORK_SETTINGS; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER; import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED; import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT; +import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT; import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet; import static android.net.thread.utils.IntegrationTestUtils.isSimulatedThreadRadioSupported; import static android.net.thread.utils.IntegrationTestUtils.newPacketReader; import static android.net.thread.utils.IntegrationTestUtils.readPacketFrom; +import static android.net.thread.utils.IntegrationTestUtils.sendUdpMessage; import static android.net.thread.utils.IntegrationTestUtils.waitFor; import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf; @@ -35,10 +37,13 @@ import static com.android.testutils.TestPermissionUtil.runAsShell; import static com.google.common.io.BaseEncoding.base16; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assume.assumeNotNull; import static org.junit.Assume.assumeTrue; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + import android.content.Context; import android.net.LinkProperties; import android.net.MacAddress; @@ -62,6 +67,7 @@ import org.junit.runner.RunWith; import java.net.Inet6Address; import java.time.Duration; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -187,4 +193,44 @@ public class BorderRoutingTest { infraNetworkReader, p -> isExpectedIcmpv6Packet(p, ICMPV6_ECHO_REPLY_TYPE))); } + + @Test + public void unicastRouting_borderRouterSendsUdpToThreadDevice_datagramReceived() + throws Exception { + assumeTrue(isSimulatedThreadRadioSupported()); + + /* + * <pre> + * Topology: + * Thread + * Border Router -------------- Full Thread device + * (Cuttlefish) + * </pre> + */ + + // BR forms a network. + CompletableFuture<Void> joinFuture = new CompletableFuture<>(); + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete)); + joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS); + + // Creates a Full Thread Device (FTD) and lets it join the network. + FullThreadDevice ftd = new FullThreadDevice(6 /* node ID */); + ftd.joinNetwork(DEFAULT_DATASET); + ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT); + waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60)); + Inet6Address ftdOmr = ftd.getOmrAddress(); + assertNotNull(ftdOmr); + Inet6Address ftdMlEid = ftd.getMlEid(); + assertNotNull(ftdMlEid); + + ftd.udpBind(ftdOmr, 12345); + sendUdpMessage(ftdOmr, 12345, "aaaaaaaa"); + assertEquals("aaaaaaaa", ftd.udpReceive()); + + ftd.udpBind(ftdMlEid, 12345); + sendUdpMessage(ftdMlEid, 12345, "bbbbbbbb"); + assertEquals("bbbbbbbb", ftd.udpReceive()); + } } diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java index 031d2050fe..5ca40e359c 100644 --- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java +++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java @@ -35,6 +35,8 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A class that launches and controls a simulation Full Thread Device (FTD). @@ -94,6 +96,12 @@ public final class FullThreadDevice { return null; } + /** Returns the Mesh-local EID address on this device if any. */ + public Inet6Address getMlEid() { + List<String> addresses = executeCommand("ipaddr mleid"); + return (Inet6Address) InetAddresses.parseNumericAddress(addresses.get(0)); + } + /** * Joins the Thread network using the given {@link ActiveOperationalDataset}. * @@ -132,6 +140,33 @@ public final class FullThreadDevice { return executeCommand("state").get(0); } + /** Closes the UDP socket. */ + public void udpClose() { + executeCommand("udp close"); + } + + /** Opens the UDP socket. */ + public void udpOpen() { + executeCommand("udp open"); + } + + /** Opens the UDP socket and binds it to a specific address and port. */ + public void udpBind(Inet6Address address, int port) { + udpClose(); + udpOpen(); + executeCommand(String.format("udp bind %s %d", address.getHostAddress(), port)); + } + + /** Returns the message received on the UDP socket. */ + public String udpReceive() throws IOException { + Pattern pattern = + Pattern.compile("> (\\d+) bytes from ([\\da-f:]+) (\\d+) ([\\x00-\\x7F]+)"); + Matcher matcher = pattern.matcher(mReader.readLine()); + matcher.matches(); + + return matcher.group(4); + } + /** Runs the "factoryreset" command on the device. */ public void factoryReset() { try { diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java index f223367e80..4eef0e5d80 100644 --- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java +++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java @@ -39,6 +39,12 @@ import com.android.testutils.TapPacketReader; import com.google.common.util.concurrent.SettableFuture; import java.io.FileDescriptor; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.ByteBuffer; import java.time.Duration; import java.util.ArrayList; @@ -219,4 +225,26 @@ public final class IntegrationTestUtils { } return pioList; } + + /** + * Sends a UDP message to a destination. + * + * @param dstAddress the IP address of the destination + * @param dstPort the port of the destination + * @param message the message in UDP payload + * @throws IOException if failed to send the message + */ + public static void sendUdpMessage(InetAddress dstAddress, int dstPort, String message) + throws IOException { + SocketAddress dstSockAddr = new InetSocketAddress(dstAddress, dstPort); + + try (DatagramSocket socket = new DatagramSocket()) { + socket.connect(dstSockAddr); + + byte[] msgBytes = message.getBytes(); + DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length); + + socket.send(packet); + } + } } -- GitLab