Add bpf forwording packet count test

- Add dumpsys option to dump stats map in BpfCoordinator
- Add stats test to check bpf forwording packet count in
  EthernetTetheringTest

Bug: 227159997
Test: atest TetheringCoverageTests
Change-Id: I177787ed0c28ebe137ff1431f40a23b1e4b22a19
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 60949a2..ecb6478 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -121,6 +121,7 @@
     private static final String TETHER_LIMIT_MAP_PATH = makeMapPath("limit");
     private static final String TETHER_ERROR_MAP_PATH = makeMapPath("error");
     private static final String TETHER_DEV_MAP_PATH = makeMapPath("dev");
+    private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
     private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
 
     // Using "," as a separator is safe because base64 characters are [0-9a-zA-Z/=+].
@@ -1088,14 +1089,14 @@
         return keyBase64Str + DUMP_BASE64_DELIMITER + valueBase64Str;
     }
 
-    private void dumpRawIpv4ForwardingRuleMap(
-            BpfMap<Tether4Key, Tether4Value> map, IndentingPrintWriter pw) throws ErrnoException {
+    private <K extends Struct, V extends Struct> void dumpRawMap(BpfMap<K, V> map,
+            IndentingPrintWriter pw) throws ErrnoException {
         if (map == null) {
-            pw.println("No IPv4 support");
+            pw.println("No BPF support");
             return;
         }
         if (map.isEmpty()) {
-            pw.println("No rules");
+            pw.println("No entries");
             return;
         }
         map.forEach((k, v) -> pw.println(bpfMapEntryToBase64String(k, v)));
@@ -1112,9 +1113,17 @@
         // it is okay for now because this is used by test only and test is supposed to use
         // expected argument order.
         // TODO: dump downstream4 map.
+        if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
+            try (BpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
+                dumpRawMap(statsMap, pw);
+            } catch (ErrnoException e) {
+                pw.println("Error dumping stats map: " + e);
+            }
+            return;
+        }
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
             try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
-                dumpRawIpv4ForwardingRuleMap(upstreamMap, pw);
+                dumpRawMap(upstreamMap, pw);
             } catch (ErrnoException e) {
                 pw.println("Error dumping IPv4 map: " + e);
             }
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 1d71557..e73b7d5 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -74,6 +74,8 @@
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
 import com.android.net.module.util.structs.Ipv4Header;
@@ -128,6 +130,13 @@
     // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
     // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
     private static final int UDP_STREAM_TS_MS = 2000;
+    // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
+    private static final int RX_UDP_PACKET_SIZE = 30;
+    private static final int RX_UDP_PACKET_COUNT = 456;
+    // Per TX UDP packet size: ethhdr (14) + iphdr (20) + udphdr (8) + payload (2) = 44 bytes.
+    private static final int TX_UDP_PACKET_SIZE = 44;
+    private static final int TX_UDP_PACKET_COUNT = 123;
+
     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");
@@ -136,6 +145,7 @@
             ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
 
     private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
+    private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
     private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
     private static final String BASE64_DELIMITER = ",";
     private static final String LINE_DELIMITER = "\\n";
@@ -957,6 +967,7 @@
                 return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD3);
             });
 
+            // [1] Verify IPv4 upstream rule map.
             final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
                     Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
             assertNotNull(upstreamMap);
@@ -965,20 +976,60 @@
             final Map.Entry<Tether4Key, Tether4Value> rule =
                     upstreamMap.entrySet().iterator().next();
 
-            final Tether4Key key = rule.getKey();
-            assertEquals(IPPROTO_UDP, key.l4proto);
-            assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), key.src4));
-            assertEquals(LOCAL_PORT, key.srcPort);
-            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), key.dst4));
-            assertEquals(REMOTE_PORT, key.dstPort);
+            final Tether4Key upstream4Key = rule.getKey();
+            assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
+            assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
+            assertEquals(LOCAL_PORT, upstream4Key.srcPort);
+            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
+            assertEquals(REMOTE_PORT, upstream4Key.dstPort);
 
-            final Tether4Value value = rule.getValue();
+            final Tether4Value upstream4Value = rule.getValue();
             assertTrue(Arrays.equals(publicIp4Addr.getAddress(),
-                    InetAddress.getByAddress(value.src46).getAddress()));
-            assertEquals(LOCAL_PORT, value.srcPort);
+                    InetAddress.getByAddress(upstream4Value.src46).getAddress()));
+            assertEquals(LOCAL_PORT, upstream4Value.srcPort);
             assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
-                    InetAddress.getByAddress(value.dst46).getAddress()));
-            assertEquals(REMOTE_PORT, value.dstPort);
+                    InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
+            assertEquals(REMOTE_PORT, upstream4Value.dstPort);
+
+            // [2] Verify stats map.
+            // Transmit packets on both direction for verifying stats. Because we only care the
+            // packet count in stats test, we just reuse the existing packets to increaes
+            // the packet count on both direction.
+
+            // Send packets on original direction.
+            for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+                tester.verifyUpload(remote, originalPacket, p -> {
+                    Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+                    return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD);
+                });
+            }
+
+            // Send packets on reply direction.
+            for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+                remote.verifyDownload(tester, replyPacket, p -> {
+                    Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+                    return isExpectedUdpPacket(p, true/* hasEther */, PAYLOAD2);
+                });
+            }
+
+            // Dump stats map to verify.
+            final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
+                    TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
+            assertNotNull(statsMap);
+            assertEquals(1, statsMap.size());
+
+            final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
+                    statsMap.entrySet().iterator().next();
+
+            // TODO: verify the upstream index in TetherStatsKey.
+
+            final TetherStatsValue statsValue = stats.getValue();
+            assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
+            assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
+            assertEquals(0, statsValue.rxErrors);
+            assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
+            assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
+            assertEquals(0, statsValue.txErrors);
         }
     }