APF: add debug code.

1. Store additional debug information when RA filtering.
2. Add a toString method for RA packets.
3. Make "adb shell dumpsys connectivity apf" dump APF filter
   state.

Bug: 26238573
Change-Id: I1441ff7bc90e63624f8b10a220b2ac97f4d390a5
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 3c13577..b7fca1a 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -1784,6 +1784,38 @@
         return false;
     }
 
+    private void dumpNetworkDiagnostics(IndentingPrintWriter pw) {
+        final List<NetworkDiagnostics> netDiags = new ArrayList<NetworkDiagnostics>();
+        final long DIAG_TIME_MS = 5000;
+        for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+            // Start gathering diagnostic information.
+            netDiags.add(new NetworkDiagnostics(
+                    nai.network,
+                    new LinkProperties(nai.linkProperties),  // Must be a copy.
+                    DIAG_TIME_MS));
+        }
+
+        for (NetworkDiagnostics netDiag : netDiags) {
+            pw.println();
+            netDiag.waitForMeasurements();
+            netDiag.dump(pw);
+        }
+    }
+
+    private void dumpApf(IndentingPrintWriter pw) {
+        pw.println("APF filters:");
+        pw.increaseIndent();
+        for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+            if (nai.apfFilter != null) {
+                pw.println(nai.name() + ":");
+                pw.increaseIndent();
+                nai.apfFilter.dump(pw);
+                pw.decreaseIndent();
+            }
+        }
+        pw.decreaseIndent();
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
@@ -1796,23 +1828,13 @@
             return;
         }
 
-        final List<NetworkDiagnostics> netDiags = new ArrayList<NetworkDiagnostics>();
         if (argsContain(args, "--diag")) {
-            final long DIAG_TIME_MS = 5000;
-            for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
-                // Start gathering diagnostic information.
-                netDiags.add(new NetworkDiagnostics(
-                        nai.network,
-                        new LinkProperties(nai.linkProperties),  // Must be a copy.
-                        DIAG_TIME_MS));
-            }
+            dumpNetworkDiagnostics(pw);
+            return;
+        }
 
-            for (NetworkDiagnostics netDiag : netDiags) {
-                pw.println();
-                netDiag.waitForMeasurements();
-                netDiag.dump(pw);
-            }
-
+        if (argsContain(args, "apf")) {
+            dumpApf(pw);
             return;
         }
 
@@ -1878,6 +1900,9 @@
         pw.println();
         mKeepaliveTracker.dump(pw);
 
+        pw.println();
+        dumpApf(pw);
+
         if (mInetLog != null && mInetLog.size() > 0) {
             pw.println();
             pw.println("Inet condition reports:");
diff --git a/services/core/java/com/android/server/connectivity/ApfFilter.java b/services/core/java/com/android/server/connectivity/ApfFilter.java
index 4696bd8..8195319 100644
--- a/services/core/java/com/android/server/connectivity/ApfFilter.java
+++ b/services/core/java/com/android/server/connectivity/ApfFilter.java
@@ -29,14 +29,19 @@
 import android.util.Pair;
 
 import com.android.internal.util.HexDump;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.ConnectivityService;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.lang.Thread;
+import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.net.NetworkInterface;
 import java.net.SocketException;
+import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
+import java.nio.BufferUnderflowException;
 import java.util.ArrayList;
 import java.util.Arrays;
 
@@ -85,6 +90,7 @@
     }
 
     private static final String TAG = "ApfFilter";
+    private static final boolean VDBG = true;
 
     private final ConnectivityService mConnectivityService;
     private final NetworkAgentInfo mNai;
@@ -152,6 +158,8 @@
         private static final int ETH_HEADER_LEN = 14;
 
         private static final int IPV6_HEADER_LEN = 40;
+        private static final int IPV6_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 8;
+        private static final int IPV6_DST_ADDR_OFFSET = ETH_HEADER_LEN + 24;
 
         // From RFC4861:
         private static final int ICMP6_RA_HEADER_LEN = 16;
@@ -192,6 +200,66 @@
         // When the packet was last captured, in seconds since Unix Epoch
         long mLastSeen;
 
+        // For debugging only. Offsets into the packet where PIOs are.
+        private final ArrayList<Integer> mPrefixOptionOffsets;
+        // For debugging only. How many times this RA was seen.
+        int seenCount = 0;
+
+
+        private String IPv6AddresstoString(int pos) {
+            try {
+                byte[] array = mPacket.array();
+                // Can't just call copyOfRange() and see if it throws, because if it reads past the
+                // end it pads with zeros instead of throwing.
+                if (pos < 0 || pos + 16 > array.length || pos + 16 < pos) {
+                    return "???";
+                }
+                byte[] addressBytes = Arrays.copyOfRange(array, pos, pos + 16);
+                InetAddress address = (Inet6Address) InetAddress.getByAddress(addressBytes);
+                return address.getHostAddress();
+            } catch (UnsupportedOperationException e) {
+                // array() failed. Cannot happen, mPacket is array-backed and read-write.
+                return "???";
+            } catch (ClassCastException | UnknownHostException e) {
+                // Cannot happen.
+                return "???";
+            }
+        }
+
+        // Can't be static because it's in a non-static inner class.
+        // TODO: Make this final once RA is its own class.
+        private int uint8(byte b) {
+            return b & 0xff;
+        }
+
+        private int uint16(short s) {
+            return s & 0xffff;
+        }
+
+        private long uint32(int s) {
+            return s & 0xffffffff;
+        }
+
+        public String toString() {
+            try {
+                StringBuffer sb = new StringBuffer();
+                sb.append(String.format("RA %s -> %s %d ",
+                        IPv6AddresstoString(IPV6_SRC_ADDR_OFFSET),
+                        IPv6AddresstoString(IPV6_DST_ADDR_OFFSET),
+                        uint16(mPacket.getShort(ICMP6_RA_ROUTER_LIFETIME_OFFSET))));
+                for (int i: mPrefixOptionOffsets) {
+                    String prefix = IPv6AddresstoString(i + 16);
+                    int length = uint8(mPacket.get(i + 2));
+                    long valid = mPacket.getInt(i + 4);
+                    long preferred = mPacket.getInt(i + 8);
+                    sb.append(String.format("%s/%d %d/%d ", prefix, length, valid, preferred));
+                }
+                return sb.toString();
+            } catch (BufferUnderflowException | IndexOutOfBoundsException e) {
+                return "<Malformed RA>";
+            }
+        }
+
         /**
          * Add a binary range of the packet that does not include a lifetime to mNonLifetimes.
          * Assumes mPacket.position() is as far as we've parsed the packet.
@@ -230,6 +298,7 @@
                     ICMP6_RA_ROUTER_LIFETIME_LEN);
 
             // Parse ICMP6 options
+            mPrefixOptionOffsets = new ArrayList<>();
             mPacket.position(ICMP6_RA_OPTION_OFFSET);
             while (mPacket.hasRemaining()) {
                 int optionType = ((int)mPacket.get(mPacket.position())) & 0xff;
@@ -244,6 +313,7 @@
                         lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
                                 ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET,
                                 ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN);
+                        mPrefixOptionOffsets.add(mPacket.position());
                         break;
                     // These three options have the same lifetime offset and size, so process
                     // together:
@@ -383,6 +453,9 @@
     // How long should the last installed filter program live for? In seconds.
     private long mLastInstalledProgramMinLifetime;
 
+    // For debugging only. The length in bytes of the last program.
+    private long mLastInstalledProgramLength;
+
     private void installNewProgram() {
         if (mRas.size() == 0) return;
         final byte[] program;
@@ -422,7 +495,12 @@
         }
         mLastTimeInstalledProgram = curTime();
         mLastInstalledProgramMinLifetime = programMinLifetime;
-        hexDump("Installing filter: ", program, program.length);
+        mLastInstalledProgramLength = program.length;
+        if (VDBG) {
+            hexDump("Installing filter: ", program, program.length);
+        } else {
+            Log.d(TAG, "Installing filter length=" + program.length);
+        }
         mConnectivityService.pushApfProgramToNetwork(mNai, program);
     }
 
@@ -441,16 +519,17 @@
     }
 
     private void processRa(byte[] packet, int length) {
-        hexDump("Read packet = ", packet, length);
+        if (VDBG) hexDump("Read packet = ", packet, length);
 
         // Have we seen this RA before?
         for (int i = 0; i < mRas.size(); i++) {
             Ra ra = mRas.get(i);
             if (ra.matches(packet, length)) {
-                log("matched RA");
+                if (VDBG) log("matched RA " + ra);
                 // Update lifetimes.
                 ra.mLastSeen = curTime();
                 ra.mMinLifetime = ra.minLifetime(packet, length);
+                ra.seenCount++;
 
                 // Keep mRas in LRU order so as to prioritize generating filters for recently seen
                 // RAs. LRU prioritizes this because RA filters are generated in order from mRas
@@ -468,7 +547,7 @@
         // Purge expired RAs.
         for (int i = 0; i < mRas.size();) {
             if (mRas.get(i).isExpired()) {
-                log("expired RA");
+                log("Expired RA " + mRas.get(i));
                 mRas.remove(i);
             } else {
                 i++;
@@ -477,8 +556,9 @@
         // TODO: figure out how to proceed when we've received more then MAX_RAS RAs.
         if (mRas.size() >= MAX_RAS) return;
         try {
-            log("adding RA");
-            mRas.add(new Ra(packet, length));
+            Ra ra = new Ra(packet, length);
+            log("Adding " + ra);
+            mRas.add(ra);
         } catch (Exception e) {
             Log.e(TAG, "Error parsing RA: " + e);
             return;
@@ -493,8 +573,8 @@
     public static void maybeInstall(ConnectivityService connectivityService, NetworkAgentInfo nai) {
         if (nai.networkMisc == null) return;
         if (nai.networkMisc.apfVersionSupported == 0) return;
-        if (nai.networkMisc.maximumApfProgramSize < 200) {
-            Log.e(TAG, "Uselessly small APF size limit: " + nai.networkMisc.maximumApfProgramSize);
+        if (nai.networkMisc.maximumApfProgramSize < 512) {
+            Log.e(TAG, "Unacceptably small APF limit: " + nai.networkMisc.maximumApfProgramSize);
             return;
         }
         // For now only support generating programs for Ethernet frames. If this restriction is
@@ -511,9 +591,35 @@
 
     public void shutdown() {
         if (mReceiveThread != null) {
-            log("shuting down");
+            log("shutting down");
             mReceiveThread.halt();  // Also closes socket.
             mReceiveThread = null;
         }
     }
+
+    public void dump(IndentingPrintWriter pw) {
+        pw.println("APF version: " + mNai.networkMisc.apfVersionSupported);
+        pw.println("Max program size: " + mNai.networkMisc.maximumApfProgramSize);
+        pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED"));
+        if (mLastTimeInstalledProgram == 0) {
+            pw.println("No program installed.");
+            return;
+        }
+
+        pw.println(String.format(
+                "Last program length %d, installed %ds ago, lifetime %d",
+                mLastInstalledProgramLength, curTime() - mLastTimeInstalledProgram,
+                mLastInstalledProgramMinLifetime));
+
+        pw.println("RA filters:");
+        pw.increaseIndent();
+        for (Ra ra: mRas) {
+            pw.println(ra);
+            pw.increaseIndent();
+            pw.println(String.format(
+                    "Seen: %d, last %ds ago", ra.seenCount, curTime() - ra.mLastSeen));
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+    }
 }