Add tethering IPv4 UDP forwarding test

This is a preparation for testing IPv4 BPF offload.
- Add an ARP responder.
- Add a basic UDP forwarding test.

Test: atest EthernetTetheringTest
Change-Id: I720a5a2c4b97493eb6a5570cecd73dfc1eabf5cd
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 15f07f2..8bf1a2b 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -25,9 +25,14 @@
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringTester.RemoteResponder;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_UDP;
 
 import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
+import static com.android.net.module.util.HexDump.dumpHexString;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
@@ -47,20 +52,26 @@
 import android.net.TetheringManager.StartTetheringCallback;
 import android.net.TetheringManager.TetheringEventCallback;
 import android.net.TetheringManager.TetheringRequest;
+import android.net.TetheringTester.TetheredDevice;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.PacketBuilder;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.UdpHeader;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TapPacketReader;
 import com.android.testutils.TestNetworkTracker;
@@ -71,6 +82,7 @@
 import org.junit.runner.RunWith;
 
 import java.io.FileDescriptor;
+import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.InterfaceAddress;
 import java.net.NetworkInterface;
@@ -92,10 +104,13 @@
 
     private static final String TAG = EthernetTetheringTest.class.getSimpleName();
     private static final int TIMEOUT_MS = 5000;
+    private static final int TETHER_REACHABILITY_ATTEMPTS = 20;
     private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/8");
     private static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64");
     private static final InetAddress TEST_IP4_DNS = parseNumericAddress("8.8.8.8");
     private static final InetAddress TEST_IP6_DNS = parseNumericAddress("2001:db8:1::888");
+    private static final ByteBuffer TEST_REACHABILITY_PAYLOAD =
+            ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
 
     private final Context mContext = InstrumentationRegistry.getContext();
     private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
@@ -105,6 +120,7 @@
     private HandlerThread mHandlerThread;
     private Handler mHandler;
     private TapPacketReader mDownstreamReader;
+    private TapPacketReader mUpstreamReader;
 
     private TetheredInterfaceRequester mTetheredInterfaceRequester;
     private MyTetheringEventCallback mTetheringEventCallback;
@@ -140,6 +156,11 @@
             mUpstreamTracker.teardown();
             mUpstreamTracker = null;
         }
+        if (mUpstreamReader != null) {
+            TapPacketReader reader = mUpstreamReader;
+            mHandler.post(() -> reader.stop());
+            mUpstreamReader = null;
+        }
 
         mTm.stopTethering(TETHERING_ETHERNET);
         if (mTetheringEventCallback != null) {
@@ -706,6 +727,168 @@
         // TODO: do basic forwarding test here.
     }
 
+    // Test network topology:
+    //
+    //         public network (rawip)                 private network
+    //                   |                 UE                |
+    // +------------+    V    +------------+------------+    V    +------------+
+    // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
+    // +------------+         +------------+------------+         +------------+
+    // remote ip              public ip                           private ip
+    // 8.8.8.8:443            <Upstream ip>:9876                  <TetheredDevice ip>:9876
+    //
+    private static final Inet4Address REMOTE_IP4_ADDR =
+            (Inet4Address) parseNumericAddress("8.8.8.8");
+    // Used by public port and private port. Assume port 9876 has not been used yet before the
+    // testing that public port and private port are the same in the testing. Note that NAT port
+    // forwarding could be different between private port and public port.
+    private static final short LOCAL_PORT = 9876;
+    private static final short REMOTE_PORT = 433;
+    private static final byte TYPE_OF_SERVICE = 0;
+    private static final short ID = 27149;
+    private static final short ID2 = 27150;
+    private static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0
+    private static final byte TIME_TO_LIVE = (byte) 0x40;
+    private static final ByteBuffer PAYLOAD =
+            ByteBuffer.wrap(new byte[] { (byte) 0x12, (byte) 0x34 });
+    private static final ByteBuffer PAYLOAD2 =
+            ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 });
+
+    private boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEther,
+            @NonNull final ByteBuffer payload) {
+        final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
+
+        if (hasEther) {
+            final EthernetHeader etherHeader = Struct.parse(EthernetHeader.class, buf);
+            if (etherHeader == null) return false;
+        }
+
+        final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, buf);
+        if (ipv4Header == null) return false;
+
+        final UdpHeader udpHeader = Struct.parse(UdpHeader.class, buf);
+        if (udpHeader == null) return false;
+
+        if (buf.remaining() != payload.limit()) return false;
+
+        return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
+                payload.array());
+    }
+
+    @NonNull
+    private ByteBuffer buildUdpv4Packet(@Nullable final MacAddress srcMac,
+            @Nullable final MacAddress dstMac, short id,
+            @NonNull final Inet4Address srcIp, @NonNull final Inet4Address dstIp,
+            short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+            throws Exception {
+        final boolean hasEther = (srcMac != null && dstMac != null);
+        final int payloadLen = (payload == null) ? 0 : payload.limit();
+        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, IPPROTO_IP, IPPROTO_UDP,
+                payloadLen);
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+
+        if (hasEther) packetBuilder.writeL2Header(srcMac, dstMac, (short) ETHER_TYPE_IPV4);
+        packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                TIME_TO_LIVE, (byte) IPPROTO_UDP, srcIp, dstIp);
+        packetBuilder.writeUdpHeader(srcPort, dstPort);
+        if (payload != null) {
+            buffer.put(payload);
+            // in case data might be reused by caller, restore the position and
+            // limit of bytebuffer.
+            payload.clear();
+        }
+
+        return packetBuilder.finalizePacket();
+    }
+
+    @NonNull
+    private ByteBuffer buildUdpv4Packet(short id, @NonNull final Inet4Address srcIp,
+            @NonNull final Inet4Address dstIp, short srcPort, short dstPort,
+            @Nullable final ByteBuffer payload) throws Exception {
+        return buildUdpv4Packet(null /* srcMac */, null /* dstMac */, id, srcIp, dstIp, srcPort,
+                dstPort, payload);
+    }
+
+    // TODO: remove this verification once upstream connected notification race is fixed.
+    // See #runUdp4Test.
+    private boolean isIpv4TetherConnectivityVerified(TetheringTester tester,
+            RemoteResponder remote, TetheredDevice tethered) throws Exception {
+        final ByteBuffer probePacket = buildUdpv4Packet(tethered.macAddr,
+                tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
+                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /*dstPort */,
+                TEST_REACHABILITY_PAYLOAD);
+
+        // Send a UDP packet from client and check the packet can be found on upstream interface.
+        for (int i = 0; i < TETHER_REACHABILITY_ATTEMPTS; i++) {
+            tester.sendPacket(probePacket);
+            byte[] expectedPacket = remote.getNextMatchedPacket(p -> {
+                Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+                return isExpectedUdpPacket(p, false /* hasEther */, TEST_REACHABILITY_PAYLOAD);
+            });
+            if (expectedPacket != null) return true;
+        }
+        return false;
+    }
+
+    private void runUdp4Test(TetheringTester tester, RemoteResponder remote) throws Exception {
+        final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
+                "1:2:3:4:5:6"));
+
+        // TODO: remove the connectivity verification for upstream connected notification race.
+        // Because async upstream connected notification can't guarantee the tethering routing is
+        // ready to use. Need to test tethering connectivity before testing.
+        // For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
+        // from upstream. That can guarantee that the routing is ready. Long term plan is that
+        // refactors upstream connected notification from async to sync.
+        assertTrue(isIpv4TetherConnectivityVerified(tester, remote, tethered));
+
+        // Send a UDP packet in original direction.
+        final ByteBuffer originalPacket = buildUdpv4Packet(tethered.macAddr,
+                tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
+                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /*dstPort */,
+                PAYLOAD /* payload */);
+        tester.verifyUpload(remote, originalPacket, p -> {
+            Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+            return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD);
+        });
+
+        // Send a UDP packet in reply direction.
+        final Inet4Address publicIp4Addr = (Inet4Address) TEST_IP4_ADDR.getAddress();
+        final ByteBuffer replyPacket = buildUdpv4Packet(ID2, REMOTE_IP4_ADDR /* srcIp */,
+                publicIp4Addr /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /*dstPort */,
+                PAYLOAD2 /* payload */);
+        remote.verifyDownload(tester, replyPacket, p -> {
+            Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+            return isExpectedUdpPacket(p, true/* hasEther */, PAYLOAD2);
+        });
+    }
+
+    @Test
+    public void testUdpV4() throws Exception {
+        assumeFalse(mEm.isAvailable());
+
+        // MyTetheringEventCallback currently only support await first available upstream. Tethering
+        // may select internet network as upstream if test network is not available and not be
+        // preferred yet. Create test upstream network before enable tethering.
+        mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR));
+
+        mDownstreamIface = createTestInterface();
+        mEm.setIncludeTestInterfaces(true);
+
+        final String iface = mTetheredInterfaceRequester.getInterface();
+        assertEquals("TetheredInterfaceCallback for unexpected interface",
+                mDownstreamIface.getInterfaceName(), iface);
+
+        mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName());
+        assertEquals("onUpstreamChanged for unexpected network", mUpstreamTracker.getNetwork(),
+                mTetheringEventCallback.awaitFirstUpstreamConnected());
+
+        mDownstreamReader = makePacketReader(mDownstreamIface);
+        mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
+
+        runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader));
+    }
+
     private <T> List<T> toList(T... array) {
         return Arrays.asList(array);
     }
diff --git a/Tethering/tests/integration/src/android/net/TetheringTester.java b/Tethering/tests/integration/src/android/net/TetheringTester.java
index d61bbb3..d24661a 100644
--- a/Tethering/tests/integration/src/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/src/android/net/TetheringTester.java
@@ -16,6 +16,12 @@
 
 package android.net;
 
+import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
+import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
 import android.net.dhcp.DhcpAckPacket;
@@ -24,6 +30,9 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.arp.ArpPacket;
 import com.android.testutils.TapPacketReader;
 
 import java.net.Inet4Address;
@@ -72,15 +81,17 @@
     }
 
     public class TetheredDevice {
-        private final MacAddress mMacAddr;
-
-        public final Inet4Address mIpv4Addr;
+        public final MacAddress macAddr;
+        public final MacAddress routerMacAddr;
+        public final Inet4Address ipv4Addr;
 
         private TetheredDevice(MacAddress mac) throws Exception {
-            mMacAddr = mac;
+            macAddr = mac;
 
-            DhcpResults dhcpResults = runDhcp(mMacAddr.toByteArray());
-            mIpv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
+            DhcpResults dhcpResults = runDhcp(macAddr.toByteArray());
+            ipv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
+            routerMacAddr = getRouterMacAddressFromArp(ipv4Addr, macAddr,
+                    dhcpResults.serverAddress);
         }
     }
 
@@ -146,12 +157,84 @@
                 DhcpPacket.decodeFullPacket(packet, packet.length, DhcpPacket.ENCAP_L2);
     }
 
+    @Nullable
+    private ArpPacket parseArpPacket(final byte[] packet) {
+        try {
+            return ArpPacket.parseArpPacket(packet, packet.length);
+        } catch (ArpPacket.ParseException e) {
+            return null;
+        }
+    }
+
+    private void maybeReplyArp(byte[] packet) {
+        ByteBuffer buf = ByteBuffer.wrap(packet);
+
+        final ArpPacket arpPacket = parseArpPacket(packet);
+        if (arpPacket == null || arpPacket.opCode != ARP_REQUEST) return;
+
+        for (int i = 0; i < mTetheredDevices.size(); i++) {
+            TetheredDevice tethered = mTetheredDevices.valueAt(i);
+            if (!arpPacket.targetIp.equals(tethered.ipv4Addr)) continue;
+
+            final ByteBuffer arpReply = ArpPacket.buildArpPacket(
+                    arpPacket.senderHwAddress.toByteArray() /* dst */,
+                    tethered.macAddr.toByteArray() /* srcMac */,
+                    arpPacket.senderIp.getAddress() /* target IP */,
+                    arpPacket.senderHwAddress.toByteArray() /* target HW address */,
+                    tethered.ipv4Addr.getAddress() /* sender IP */,
+                    (short) ARP_REPLY);
+            try {
+                sendPacket(arpReply);
+            } catch (Exception e) {
+                fail("Failed to reply ARP for " + tethered.ipv4Addr);
+            }
+            return;
+        }
+    }
+
+    private MacAddress getRouterMacAddressFromArp(final Inet4Address tetherIp,
+            final MacAddress tetherMac, final Inet4Address routerIp) throws Exception {
+        final ByteBuffer arpProbe = ArpPacket.buildArpPacket(ETHER_BROADCAST /* dst */,
+                tetherMac.toByteArray() /* srcMac */, routerIp.getAddress() /* target IP */,
+                new byte[ETHER_ADDR_LEN] /* target HW address */,
+                tetherIp.getAddress() /* sender IP */, (short) ARP_REQUEST);
+        sendPacket(arpProbe);
+
+        final byte[] packet = getNextMatchedPacket((p) -> {
+            final ArpPacket arpPacket = parseArpPacket(p);
+            if (arpPacket == null || arpPacket.opCode != ARP_REPLY) return false;
+            return arpPacket.targetIp.equals(tetherIp);
+        });
+
+        if (packet != null) {
+            Log.d(TAG, "Get Mac address from ARP");
+            final ArpPacket arpReply = ArpPacket.parseArpPacket(packet, packet.length);
+            return arpReply.senderHwAddress;
+        }
+
+        fail("Could not get ARP packet");
+        return null;
+    }
+
     public void sendPacket(ByteBuffer packet) throws Exception {
         mDownstreamReader.sendResponse(packet);
     }
 
     public byte[] getNextMatchedPacket(Predicate<byte[]> filter) {
-        return mDownstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
+        byte[] packet;
+        while ((packet = mDownstreamReader.poll(PACKET_READ_TIMEOUT_MS)) != null) {
+            if (filter.test(packet)) return packet;
+
+            maybeReplyArp(packet);
+        }
+
+        return null;
+    }
+
+    public void verifyUpload(final RemoteResponder dst, final ByteBuffer packet,
+            final Predicate<byte[]> filter) throws Exception {
+        sendPacket(packet);
+        assertNotNull("Upload fail", dst.getNextMatchedPacket(filter));
     }
 
     public static class RemoteResponder {
@@ -167,5 +250,11 @@
         public byte[] getNextMatchedPacket(Predicate<byte[]> filter) throws Exception {
             return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
         }
+
+        public void verifyDownload(final TetheringTester dst, final ByteBuffer packet,
+                final Predicate<byte[]> filter) throws Exception {
+            sendPacket(packet);
+            assertNotNull("Download fail", dst.getNextMatchedPacket(filter));
+        }
     }
 }