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
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 29ada1b..2fccf6b 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.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.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 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 @@
                         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 031d205..5ca40e3 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.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 @@
         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 @@
         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 f223367..4eef0e5 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.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 @@
         }
         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);
+        }
+    }
 }