Have ConnectivityService install packet filters when possible

Listen for ICMP6 router advertisements on networks that support
packet filters.  Construct packet filters and install them to
ignore redundant future ICMP6 router advertisements.

Bug: 26238573
Change-Id: If78300b9fda257c21f3ee6533e1da7de9f897cb4
diff --git a/core/java/android/net/NetworkAgent.java b/core/java/android/net/NetworkAgent.java
index 20c2168..9e360e1 100644
--- a/core/java/android/net/NetworkAgent.java
+++ b/core/java/android/net/NetworkAgent.java
@@ -200,6 +200,14 @@
      */
     public static final int CMD_PREVENT_AUTOMATIC_RECONNECT = BASE + 15;
 
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to install an APF program in the network
+     * chipset for use to filter packets.
+     *
+     * obj = byte[] containing the APF program bytecode.
+     */
+    public static final int CMD_PUSH_APF_PROGRAM = BASE + 16;
+
     public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni,
             NetworkCapabilities nc, LinkProperties lp, int score) {
         this(looper, context, logTag, ni, nc, lp, score, null);
@@ -319,6 +327,10 @@
                 preventAutomaticReconnect();
                 break;
             }
+            case CMD_PUSH_APF_PROGRAM: {
+                installPacketFilter((byte[]) msg.obj);
+                break;
+            }
         }
     }
 
@@ -494,6 +506,15 @@
     protected void preventAutomaticReconnect() {
     }
 
+    /**
+     * Install a packet filter.
+     * @param filter an APF program to filter incoming packets.
+     * @return {@code true} if filter successfully installed, {@code false} otherwise.
+     */
+    protected boolean installPacketFilter(byte[] filter) {
+        return false;
+    }
+
     protected void log(String s) {
         Log.d(LOG_TAG, "NetworkAgent: " + s);
     }
diff --git a/core/java/android/net/NetworkMisc.java b/core/java/android/net/NetworkMisc.java
index 5511a24..748699e 100644
--- a/core/java/android/net/NetworkMisc.java
+++ b/core/java/android/net/NetworkMisc.java
@@ -56,6 +56,22 @@
      */
     public String subscriberId;
 
+    /**
+     * Version of APF instruction set supported for packet filtering. 0 indicates no support for
+     * packet filtering using APF programs.
+     */
+    public int apfVersionSupported;
+
+    /**
+     * Maximum size of APF program allowed.
+     */
+    public int maximumApfProgramSize;
+
+    /**
+     * Format of packets passed to APF filter. Should be one of ARPHRD_*
+     */
+    public int apfPacketFormat;
+
     public NetworkMisc() {
     }
 
@@ -65,6 +81,9 @@
             explicitlySelected = nm.explicitlySelected;
             acceptUnvalidated = nm.acceptUnvalidated;
             subscriberId = nm.subscriberId;
+            apfVersionSupported = nm.apfVersionSupported;
+            maximumApfProgramSize = nm.maximumApfProgramSize;
+            apfPacketFormat = nm.apfPacketFormat;
         }
     }
 
@@ -79,6 +98,9 @@
         out.writeInt(explicitlySelected ? 1 : 0);
         out.writeInt(acceptUnvalidated ? 1 : 0);
         out.writeString(subscriberId);
+        out.writeInt(apfVersionSupported);
+        out.writeInt(maximumApfProgramSize);
+        out.writeInt(apfPacketFormat);
     }
 
     public static final Creator<NetworkMisc> CREATOR = new Creator<NetworkMisc>() {
@@ -89,6 +111,9 @@
             networkMisc.explicitlySelected = in.readInt() != 0;
             networkMisc.acceptUnvalidated = in.readInt() != 0;
             networkMisc.subscriberId = in.readString();
+            networkMisc.apfVersionSupported = in.readInt();
+            networkMisc.maximumApfProgramSize = in.readInt();
+            networkMisc.apfPacketFormat = in.readInt();
             return networkMisc;
         }
 
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index c6d919f..555032d 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -62,6 +62,13 @@
     public native static void attachDhcpFilter(FileDescriptor fd) throws SocketException;
 
     /**
+     * Attaches a socket filter that accepts ICMP6 router advertisement packets to the given socket.
+     * @param fd the socket's {@link FileDescriptor}.
+     * @param packetType the hardware address type, one of ARPHRD_*.
+     */
+    public native static void attachRaFilter(FileDescriptor fd, int packetType) throws SocketException;
+
+    /**
      * Binds the current process to the network designated by {@code netId}.  All sockets created
      * in the future (and not explicitly bound via a bound {@link SocketFactory} (see
      * {@link Network#getSocketFactory}) will be bound to this network.  Note that if this
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index defb88a..880a79c 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -26,10 +26,13 @@
 #include <net/if.h>
 #include <linux/filter.h>
 #include <linux/if.h>
+#include <linux/if_arp.h>
 #include <linux/if_ether.h>
 #include <linux/if_packet.h>
 #include <net/if_ether.h>
+#include <netinet/icmp6.h>
 #include <netinet/ip.h>
+#include <netinet/ip6.h>
 #include <netinet/udp.h>
 #include <cutils/properties.h>
 
@@ -64,10 +67,9 @@
 
 static void android_net_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd)
 {
-    int fd = jniGetFDFromFileDescriptor(env, javaFd);
     uint32_t ip_offset = sizeof(ether_header);
     uint32_t proto_offset = ip_offset + offsetof(iphdr, protocol);
-    uint32_t flags_offset = ip_offset +  offsetof(iphdr, frag_off);
+    uint32_t flags_offset = ip_offset + offsetof(iphdr, frag_off);
     uint32_t dport_indirect_offset = ip_offset + offsetof(udphdr, dest);
     struct sock_filter filter_code[] = {
         // Check the protocol is UDP.
@@ -94,6 +96,45 @@
         filter_code,
     };
 
+    int fd = jniGetFDFromFileDescriptor(env, javaFd);
+    if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
+    }
+}
+
+static void android_net_utils_attachRaFilter(JNIEnv *env, jobject clazz, jobject javaFd,
+        jint hardwareAddressType)
+{
+    if (hardwareAddressType != ARPHRD_ETHER) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "attachRaFilter only supports ARPHRD_ETHER");
+        return;
+    }
+
+    uint32_t ipv6_offset = sizeof(ether_header);
+    uint32_t ipv6_next_header_offset = ipv6_offset + offsetof(ip6_hdr, ip6_nxt);
+    uint32_t icmp6_offset = ipv6_offset + sizeof(ip6_hdr);
+    uint32_t icmp6_type_offset = icmp6_offset + offsetof(icmp6_hdr, icmp6_type);
+    struct sock_filter filter_code[] = {
+        // Check IPv6 Next Header is ICMPv6.
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  ipv6_next_header_offset),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    IPPROTO_ICMPV6, 0, 3),
+
+        // Check ICMPv6 type is Router Advertisement.
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  icmp6_type_offset),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    ND_ROUTER_ADVERT, 0, 1),
+
+        // Accept or reject.
+        BPF_STMT(BPF_RET | BPF_K,              0xffff),
+        BPF_STMT(BPF_RET | BPF_K,              0)
+    };
+    struct sock_fprog filter = {
+        sizeof(filter_code) / sizeof(filter_code[0]),
+        filter_code,
+    };
+
+    int fd = jniGetFDFromFileDescriptor(env, javaFd);
     if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
         jniThrowExceptionFmt(env, "java/net/SocketException",
                 "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
@@ -148,6 +189,7 @@
     { "protectFromVpn", "(I)Z", (void*)android_net_utils_protectFromVpn },
     { "queryUserAccess", "(II)Z", (void*)android_net_utils_queryUserAccess },
     { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDhcpFilter },
+    { "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter },
 };
 
 int register_android_net_NetworkUtils(JNIEnv* env)
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 9c09f24..c80a82e 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -125,6 +125,7 @@
 import com.android.server.connectivity.NetworkMonitor;
 import com.android.server.connectivity.PacManager;
 import com.android.server.connectivity.PermissionMonitor;
+import com.android.server.connectivity.ApfFilter;
 import com.android.server.connectivity.Tethering;
 import com.android.server.connectivity.Vpn;
 import com.android.server.net.BaseNetworkObserver;
@@ -359,6 +360,13 @@
      */
     private static final int EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT = 31;
 
+    /**
+     * used to push APF program to NetworkAgent
+     * replyTo = NetworkAgent message handler
+     * obj = byte[] of APF program
+     */
+    private static final int EVENT_PUSH_APF_PROGRAM_TO_NETWORK = 32;
+
     /** Handler thread used for both of the handlers below. */
     @VisibleForTesting
     protected final HandlerThread mHandlerThread;
@@ -2188,6 +2196,7 @@
             mKeepaliveTracker.handleStopAllKeepalives(nai,
                     ConnectivityManager.PacketKeepalive.ERROR_INVALID_NETWORK);
             nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_DISCONNECTED);
+            if (nai.apfFilter != null) nai.apfFilter.shutdown();
             mNetworkAgentInfos.remove(msg.replyTo);
             updateClat(null, nai.linkProperties, nai);
             synchronized (mNetworkForNetId) {
@@ -2402,6 +2411,13 @@
                 accept ? 1 : 0, always ? 1: 0, network));
     }
 
+    public void pushApfProgramToNetwork(NetworkAgentInfo nai, byte[] program) {
+        enforceConnectivityInternalPermission();
+        Message msg = mHandler.obtainMessage(EVENT_PUSH_APF_PROGRAM_TO_NETWORK, program);
+        msg.replyTo = nai.messenger;
+        mHandler.sendMessage(msg);
+    }
+
     private void handleSetAcceptUnvalidated(Network network, boolean accept, boolean always) {
         if (DBG) log("handleSetAcceptUnvalidated network=" + network +
                 " accept=" + accept + " always=" + always);
@@ -2556,6 +2572,16 @@
                     handleMobileDataAlwaysOn();
                     break;
                 }
+                case EVENT_PUSH_APF_PROGRAM_TO_NETWORK: {
+                    NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
+                    if (nai == null) {
+                        loge("EVENT_PUSH_APF_PROGRAM_TO_NETWORK from unknown NetworkAgent");
+                    } else {
+                         nai.asyncChannel.sendMessage(NetworkAgent.CMD_PUSH_APF_PROGRAM,
+                                 (byte[]) msg.obj);
+                    }
+                    break;
+                }
                 // Sent by KeepaliveTracker to process an app request on the state machine thread.
                 case NetworkAgent.CMD_START_PACKET_KEEPALIVE: {
                     mKeepaliveTracker.handleStartKeepalive(msg);
@@ -3944,6 +3970,9 @@
         if (networkAgent.clatd != null) {
             networkAgent.clatd.fixupLinkProperties(oldLp);
         }
+        if (networkAgent.apfFilter != null) {
+            networkAgent.apfFilter.updateFilter();
+        }
 
         updateInterfaces(newLp, oldLp, netId);
         updateMtu(newLp, oldLp);
diff --git a/services/core/java/com/android/server/connectivity/ApfFilter.java b/services/core/java/com/android/server/connectivity/ApfFilter.java
new file mode 100644
index 0000000..25c84e1
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/ApfFilter.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static android.system.OsConstants.*;
+
+import android.net.NetworkUtils;
+import android.net.apf.ApfGenerator;
+import android.net.apf.ApfGenerator.IllegalInstructionException;
+import android.net.apf.ApfGenerator.Register;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.PacketSocketAddress;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.util.HexDump;
+import com.android.server.ConnectivityService;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.Thread;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import libcore.io.IoBridge;
+
+/**
+ * For networks that support packet filtering via APF programs, {@code ApfFilter}
+ * listens for IPv6 ICMPv6 router advertisements (RAs) and generates APF programs to
+ * filter out redundant duplicate ones.
+ *
+ * @hide
+ */
+public class ApfFilter {
+    // Thread to listen for RAs.
+    private class ReceiveThread extends Thread {
+        private final byte[] mPacket = new byte[1514];
+        private final FileDescriptor mSocket;
+        private volatile boolean mStopped;
+
+        public ReceiveThread(FileDescriptor socket) {
+            mSocket = socket;
+        }
+
+        public void halt() {
+            mStopped = true;
+            try {
+                // Interrupts the read() call the thread is blocked in.
+                IoBridge.closeAndSignalBlockedThreads(mSocket);
+            } catch (IOException ignored) {}
+        }
+
+        @Override
+        public void run() {
+            log("begin monitoring");
+            while (!mStopped) {
+                try {
+                    int length = Os.read(mSocket, mPacket, 0, mPacket.length);
+                    processRa(mPacket, length);
+                } catch (IOException|ErrnoException e) {
+                    if (!mStopped) {
+                        Log.e(TAG, "Read error", e);
+                    }
+                }
+            }
+        }
+    }
+
+    private static final String TAG = "ApfFilter";
+
+    private final ConnectivityService mConnectivityService;
+    private final NetworkAgentInfo mNai;
+    private ReceiveThread mReceiveThread;
+    private String mIfaceName;
+    private long mUniqueCounter;
+
+    private ApfFilter(ConnectivityService connectivityService, NetworkAgentInfo nai) {
+        mConnectivityService = connectivityService;
+        mNai = nai;
+        maybeStartFilter();
+    }
+
+    private void log(String s) {
+        Log.d(TAG, "(" + mNai.network.netId + "): " + s);
+    }
+
+    private long getUniqueNumber() {
+        return mUniqueCounter++;
+    }
+
+    /**
+     * Attempt to start listening for RAs and, if RAs are received, generating and installing
+     * filters to ignore useless RAs.
+     */
+    private void maybeStartFilter() {
+        mIfaceName = mNai.linkProperties.getInterfaceName();
+        if (mIfaceName == null) return;
+        FileDescriptor socket;
+        try {
+            socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6);
+            PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IPV6,
+                    NetworkInterface.getByName(mIfaceName).getIndex());
+            Os.bind(socket, addr);
+            NetworkUtils.attachRaFilter(socket, mNai.networkMisc.apfPacketFormat);
+        } catch(SocketException|ErrnoException e) {
+            Log.e(TAG, "Error filtering raw socket", e);
+            return;
+        }
+        mReceiveThread = new ReceiveThread(socket);
+        mReceiveThread.start();
+    }
+
+    /**
+     * mNai's LinkProperties may have changed, take appropriate action.
+     */
+    public void updateFilter() {
+        // If we're not listening for RAs, try starting.
+        if (mReceiveThread == null) {
+            maybeStartFilter();
+        // If interface name has changed, restart.
+        } else if (!mIfaceName.equals(mNai.linkProperties.getInterfaceName())) {
+            shutdown();
+            maybeStartFilter();
+        }
+    }
+
+    // Returns seconds since Unix Epoch.
+    private static long curTime() {
+        return System.currentTimeMillis() / 1000L;
+    }
+
+    // A class to hold information about an RA.
+    private class Ra {
+        private static final int ETH_HEADER_LEN = 14;
+
+        private static final int IPV6_HEADER_LEN = 40;
+
+        // From RFC4861:
+        private static final int ICMP6_RA_HEADER_LEN = 16;
+        private static final int ICMP6_RA_OPTION_OFFSET =
+                ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
+        private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
+                ETH_HEADER_LEN + IPV6_HEADER_LEN + 6;
+        private static final int ICMP6_RA_ROUTER_LIFETIME_LEN = 2;
+        // Prefix information option.
+        private static final int ICMP6_PREFIX_OPTION_TYPE = 3;
+        private static final int ICMP6_PREFIX_OPTION_LEN = 32;
+        private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4;
+        private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN = 4;
+        private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8;
+        private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4;
+
+        // From RFC6106: Recursive DNS Server option
+        private static final int ICMP6_RDNSS_OPTION_TYPE = 25;
+        // From RFC6106: DNS Search List option
+        private static final int ICMP6_DNSSL_OPTION_TYPE = 31;
+
+        // From RFC4191: Route Information option
+        private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
+        // Above three options all have the same format:
+        private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
+        private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4;
+
+        private final ByteBuffer mPacket;
+        // List of binary ranges that include the whole packet except the lifetimes.
+        // Pairs consist of offset and length.
+        private final ArrayList<Pair<Integer, Integer>> mNonLifetimes =
+                new ArrayList<Pair<Integer, Integer>>();
+        // Minimum lifetime in packet
+        long mMinLifetime;
+        // When the packet was last captured, in seconds since Unix Epoch
+        long mLastSeen;
+
+        /**
+         * 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.
+         * @param lastNonLifetimeStart offset within packet of where the last binary range of
+         *                             data not including a lifetime.
+         * @param lifetimeOffset offset from mPacket.position() to the next lifetime data.
+         * @param lifetimeLength length of the next lifetime data.
+         * @return offset within packet of where the next binary range of data not including
+         *         a lifetime.  This can be passed into the next invocation of this function
+         *         via {@code lastNonLifetimeStart}.
+         */
+        private int addNonLifetime(int lastNonLifetimeStart, int lifetimeOffset,
+                int lifetimeLength) {
+            lifetimeOffset += mPacket.position();
+            mNonLifetimes.add(new Pair<Integer, Integer>(lastNonLifetimeStart,
+                    lifetimeOffset - lastNonLifetimeStart));
+            return lifetimeOffset + lifetimeLength;
+        }
+
+        // Note that this parses RA and may throw IllegalArgumentException (from
+        // Buffer.position(int) ) or IndexOutOfBoundsException (from ByteBuffer.get(int) ) if
+        // parsing encounters something non-compliant with specifications.
+        Ra(byte[] packet, int length) {
+            mPacket = ByteBuffer.allocate(length).put(ByteBuffer.wrap(packet, 0, length));
+            mPacket.clear();
+            mLastSeen = curTime();
+
+            // Parse router lifetime
+            int lastNonLifetimeStart = addNonLifetime(0, ICMP6_RA_ROUTER_LIFETIME_OFFSET,
+                    ICMP6_RA_ROUTER_LIFETIME_LEN);
+            // Parse ICMP6 options
+            mPacket.position(ICMP6_RA_OPTION_OFFSET);
+            while (mPacket.hasRemaining()) {
+                int optionType = ((int)mPacket.get(mPacket.position())) & 0xff;
+                int optionLength = (((int)mPacket.get(mPacket.position() + 1)) & 0xff) * 8;
+                switch (optionType) {
+                    case ICMP6_PREFIX_OPTION_TYPE:
+                        // Parse valid lifetime
+                        lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
+                                ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET,
+                                ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN);
+                        // Parse preferred lifetime
+                        lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
+                                ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET,
+                                ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN);
+                        break;
+                    // These three options have the same lifetime offset and size, so process
+                    // together:
+                    case ICMP6_ROUTE_INFO_OPTION_TYPE:
+                    case ICMP6_RDNSS_OPTION_TYPE:
+                    case ICMP6_DNSSL_OPTION_TYPE:
+                        // Parse lifetime
+                        lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
+                                ICMP6_4_BYTE_LIFETIME_OFFSET,
+                                ICMP6_4_BYTE_LIFETIME_LEN);
+                        break;
+                    default:
+                        // RFC4861 section 4.2 dictates we ignore unknown options for fowards
+                        // compatibility.
+                        break;
+                }
+                mPacket.position(mPacket.position() + optionLength);
+            }
+            // Mark non-lifetime bytes since last lifetime.
+            addNonLifetime(lastNonLifetimeStart, 0, 0);
+            mMinLifetime = minLifetime(packet, length);
+        }
+
+        // Ignoring lifetimes (which may change) does {@code packet} match this RA?
+        boolean matches(byte[] packet, int length) {
+            if (length != mPacket.limit()) return false;
+            ByteBuffer a = ByteBuffer.wrap(packet);
+            ByteBuffer b = mPacket;
+            for (Pair<Integer, Integer> nonLifetime : mNonLifetimes) {
+                a.clear();
+                b.clear();
+                a.position(nonLifetime.first);
+                b.position(nonLifetime.first);
+                a.limit(nonLifetime.first + nonLifetime.second);
+                b.limit(nonLifetime.first + nonLifetime.second);
+                if (a.compareTo(b) != 0) return false;
+            }
+            return true;
+        }
+
+        // What is the minimum of all lifetimes within {@code packet} in seconds?
+        // Precondition: matches(packet, length) already returned true.
+        long minLifetime(byte[] packet, int length) {
+            long minLifetime = Long.MAX_VALUE;
+            // Wrap packet in ByteBuffer so we can read big-endian values easily
+            ByteBuffer byteBuffer = ByteBuffer.wrap(packet);
+            for (int i = 0; (i + 1) < mNonLifetimes.size(); i++) {
+                int offset = mNonLifetimes.get(i).first + mNonLifetimes.get(i).second;
+                int lifetimeLength = mNonLifetimes.get(i+1).first - offset;
+                long val;
+                switch (lifetimeLength) {
+                    case 2: val = byteBuffer.getShort(offset); break;
+                    case 4: val = byteBuffer.getInt(offset); break;
+                    default: throw new IllegalStateException("bogus lifetime size " + length);
+                }
+                // Mask to size, converting signed to unsigned
+                val &= (1L << (lifetimeLength * 8)) - 1;
+                minLifetime = Math.min(minLifetime, val);
+            }
+            return minLifetime;
+        }
+
+        // How many seconds does this RA's have to live, taking into account the fact
+        // that we might have seen it a while ago.
+        long currentLifetime() {
+            return mMinLifetime - (curTime() - mLastSeen);
+        }
+
+        boolean isExpired() {
+            return currentLifetime() < 0;
+        }
+
+        // Append a filter for this RA to {@code gen}. Jump to DROP_LABEL if it should be dropped.
+        // Jump to the next filter if packet doesn't match this RA.
+        long generateFilter(ApfGenerator gen) throws IllegalInstructionException {
+            String nextFilterLabel = "Ra" + getUniqueNumber();
+            // Skip if packet is not the right size
+            gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT);
+            gen.addJumpIfR0NotEquals(mPacket.limit(), nextFilterLabel);
+            int filterLifetime = (int)(currentLifetime() / FRACTION_OF_LIFETIME_TO_FILTER);
+            // Skip filter if expired
+            gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT);
+            gen.addJumpIfR0GreaterThan(filterLifetime, nextFilterLabel);
+            for (int i = 0; i < mNonLifetimes.size(); i++) {
+                // Generate code to match the packet bytes
+                Pair<Integer, Integer> nonLifetime = mNonLifetimes.get(i);
+                gen.addLoadImmediate(Register.R0, nonLifetime.first);
+                gen.addJumpIfBytesNotEqual(Register.R0,
+                        Arrays.copyOfRange(mPacket.array(), nonLifetime.first,
+                                           nonLifetime.first + nonLifetime.second),
+                        nextFilterLabel);
+                // Generate code to test the lifetimes haven't gone down too far
+                if ((i + 1) < mNonLifetimes.size()) {
+                    Pair<Integer, Integer> nextNonLifetime = mNonLifetimes.get(i + 1);
+                    int offset = nonLifetime.first + nonLifetime.second;
+                    int length = nextNonLifetime.first - offset;
+                    switch (length) {
+                        case 4: gen.addLoad32(Register.R0, offset); break;
+                        case 2: gen.addLoad16(Register.R0, offset); break;
+                        default: throw new IllegalStateException("bogus lifetime size " + length);
+                    }
+                    gen.addJumpIfR0LessThan(filterLifetime, nextFilterLabel);
+                }
+            }
+            gen.addJump(gen.DROP_LABEL);
+            gen.defineLabel(nextFilterLabel);
+            return filterLifetime;
+        }
+    }
+
+    // Maximum number of RAs to filter for.
+    private static final int MAX_RAS = 10;
+    private ArrayList<Ra> mRas = new ArrayList<Ra>();
+
+    // There is always some marginal benefit to updating the installed APF program when an RA is
+    // seen because we can extend the program's lifetime slightly, but there is some cost to
+    // updating the program, so don't bother unless the program is going to expire soon. This
+    // constant defines "soon" in seconds.
+    private static final long MAX_PROGRAM_LIFETIME_WORTH_REFRESHING = 30;
+    // We don't want to filter an RA for it's whole lifetime as it'll be expired by the time we ever
+    // see a refresh.  Using half the lifetime might be a good idea except for the fact that
+    // packets may be dropped, so let's use 6.
+    private static final int FRACTION_OF_LIFETIME_TO_FILTER = 6;
+
+    // When did we last install a filter program? In seconds since Unix Epoch.
+    private long mLastTimeInstalledProgram;
+    // How long should the last installed filter program live for? In seconds.
+    private long mLastInstalledProgramMinLifetime;
+
+    private void installNewProgram() {
+        if (mRas.size() == 0) return;
+        final byte[] program;
+        long programMinLifetime = Long.MAX_VALUE;
+        try {
+            ApfGenerator gen = new ApfGenerator();
+            // This is guaranteed to return true because of the check in maybeInstall.
+            gen.setApfVersion(mNai.networkMisc.apfVersionSupported);
+            // Step 1: Determine how many RA filters we can fit in the program.
+            int ras = 0;
+            for (Ra ra : mRas) {
+                if (ra.isExpired()) continue;
+                ra.generateFilter(gen);
+                if (gen.programLengthOverEstimate() > mNai.networkMisc.maximumApfProgramSize) {
+                    // We went too far.  Use prior number of RAs in "ras".
+                    break;
+                } else {
+                    // Yay! this RA filter fits, increment "ras".
+                    ras++;
+                }
+            }
+            // Step 2: Generate RA filters
+            gen = new ApfGenerator();
+            // This is guaranteed to return true because of the check in maybeInstall.
+            gen.setApfVersion(mNai.networkMisc.apfVersionSupported);
+            for (Ra ra : mRas) {
+                if (ras-- == 0) break;
+                if (ra.isExpired()) continue;
+                programMinLifetime = Math.min(programMinLifetime, ra.generateFilter(gen));
+            }
+            // Execution will reach the end of the program if no filters match, which will pass the
+            // packet to the AP.
+            program = gen.generate();
+        } catch (IllegalInstructionException e) {
+            Log.e(TAG, "Program failed to generate: ", e);
+            return;
+        }
+        mLastTimeInstalledProgram = curTime();
+        mLastInstalledProgramMinLifetime = programMinLifetime;
+        hexDump("Installing filter: ", program, program.length);
+        mConnectivityService.pushApfProgramToNetwork(mNai, program);
+    }
+
+    // Install a new filter program if the last installed one will die soon.
+    private void maybeInstallNewProgram() {
+        if (mRas.size() == 0) return;
+        // If the current program doesn't expire for a while, don't bother updating.
+        long expiry = mLastTimeInstalledProgram + mLastInstalledProgramMinLifetime;
+        if (expiry < curTime() + MAX_PROGRAM_LIFETIME_WORTH_REFRESHING) {
+            installNewProgram();
+        }
+    }
+
+    private void hexDump(String msg, byte[] packet, int length) {
+        log(msg + HexDump.toHexString(packet, 0, length));
+    }
+
+    private void processRa(byte[] packet, int length) {
+        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");
+                // Update lifetimes.
+                ra.mLastSeen = curTime();
+                ra.mMinLifetime = ra.minLifetime(packet, length);
+
+                // 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
+                // until the filter program exceeds the maximum filter program size allowed by the
+                // chipset, so RAs appearing earlier in mRas are more likely to make it into the
+                // filter program.
+                // TODO: consider sorting the RAs in order of increasing expiry time as well.
+                // Swap to front of array.
+                mRas.add(0, mRas.remove(i));
+
+                maybeInstallNewProgram();
+                return;
+            }
+        }
+        // Purge expired RAs.
+        for (int i = 0; i < mRas.size();) {
+            if (mRas.get(i).isExpired()) {
+                log("expired RA");
+                mRas.remove(i);
+            } else {
+                i++;
+            }
+        }
+        // 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));
+        } catch (Exception e) {
+            Log.e(TAG, "Error parsing RA: " + e);
+            return;
+        }
+        installNewProgram();
+    }
+
+    /**
+     * Install an {@link ApfFilter} on {@code nai} if {@code nai} supports packet
+     * filtering using APF programs.
+     */
+    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);
+            return;
+        }
+        // For now only support generating programs for Ethernet frames. If this restriction is
+        // lifted:
+        //   1. the program generator will need its offsets adjusted.
+        //   2. the packet filter attached to our packet socket will need its offset adjusted.
+        if (nai.networkMisc.apfPacketFormat != ARPHRD_ETHER) return;
+        if (!new ApfGenerator().setApfVersion(nai.networkMisc.apfVersionSupported)) {
+            Log.e(TAG, "Unsupported APF version: " + nai.networkMisc.apfVersionSupported);
+            return;
+        }
+        nai.apfFilter = new ApfFilter(connectivityService, nai);
+    }
+
+    public void shutdown() {
+        if (mReceiveThread != null) {
+            log("shuting down");
+            mReceiveThread.halt();  // Also closes socket.
+            mReceiveThread = null;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 0029279..384c620 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -32,6 +32,7 @@
 import com.android.internal.util.AsyncChannel;
 import com.android.server.ConnectivityService;
 import com.android.server.connectivity.NetworkMonitor;
+import com.android.server.connectivity.ApfFilter;
 
 import java.util.ArrayList;
 import java.util.Comparator;
@@ -163,6 +164,8 @@
     // Used by ConnectivityService to keep track of 464xlat.
     public Nat464Xlat clatd;
 
+    public ApfFilter apfFilter;
+
     public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, Network net, NetworkInfo info,
             LinkProperties lp, NetworkCapabilities nc, int score, Context context, Handler handler,
             NetworkMisc misc, NetworkRequest defaultRequest, ConnectivityService connService) {
@@ -175,6 +178,7 @@
         currentScore = score;
         networkMonitor = connService.createNetworkMonitor(context, handler, this, defaultRequest);
         networkMisc = misc;
+        apfFilter.maybeInstall(connService, this);
     }
 
     /**