Merge changes I1250730c,I7d00848c

* changes:
  Create a wrapper for IpClient
  Move IpClient to NetworkStack
diff --git a/Android.bp b/Android.bp
index 2f7d599..a2da0a0 100644
--- a/Android.bp
+++ b/Android.bp
@@ -24,7 +24,7 @@
         ":services-networkstack-shared-srcs",
     ],
     static_libs: [
-        "dhcp-packet-lib",
+        "services-netlink-lib",
     ]
 }
 
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7f8bb93..5ab833b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -28,6 +28,7 @@
     <!-- Launch captive portal app as specific user -->
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.NETWORK_STACK" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
     <application
         android:label="NetworkStack"
         android:defaultToDeviceProtectedStorage="true"
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
new file mode 100644
index 0000000..50c4dfc
--- /dev/null
+++ b/src/android/net/apf/ApfFilter.java
@@ -0,0 +1,1591 @@
+/*
+ * 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 android.net.apf;
+
+import static android.net.util.SocketUtils.makePacketSocketAddress;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ARPHRD_ETHER;
+import static android.system.OsConstants.ETH_P_ARP;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_RAW;
+
+import static com.android.internal.util.BitUtils.bytesToBEInt;
+import static com.android.internal.util.BitUtils.getUint16;
+import static com.android.internal.util.BitUtils.getUint32;
+import static com.android.internal.util.BitUtils.getUint8;
+import static com.android.internal.util.BitUtils.uint32;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkUtils;
+import android.net.apf.ApfGenerator.IllegalInstructionException;
+import android.net.apf.ApfGenerator.Register;
+import android.net.ip.IpClient.IpClientCallbacksWrapper;
+import android.net.metrics.ApfProgramEvent;
+import android.net.metrics.ApfStats;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.RaEvent;
+import android.net.util.InterfaceParams;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.HexDump;
+import com.android.internal.util.IndentingPrintWriter;
+
+import libcore.io.IoBridge;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * 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.
+ *
+ * Threading model:
+ * A collection of RAs we've received is kept in mRas. Generating APF programs uses mRas to
+ * know what RAs to filter for, thus generating APF programs is dependent on mRas.
+ * mRas can be accessed by multiple threads:
+ * - ReceiveThread, which listens for RAs and adds them to mRas, and generates APF programs.
+ * - callers of:
+ *    - setMulticastFilter(), which can cause an APF program to be generated.
+ *    - dump(), which dumps mRas among other things.
+ *    - shutdown(), which clears mRas.
+ * So access to mRas is synchronized.
+ *
+ * @hide
+ */
+public class ApfFilter {
+
+    // Helper class for specifying functional filter parameters.
+    public static class ApfConfiguration {
+        public ApfCapabilities apfCapabilities;
+        public boolean multicastFilter;
+        public boolean ieee802_3Filter;
+        public int[] ethTypeBlackList;
+    }
+
+    // Enums describing the outcome of receiving an RA packet.
+    private static enum ProcessRaResult {
+        MATCH,          // Received RA matched a known RA
+        DROPPED,        // Received RA ignored due to MAX_RAS
+        PARSE_ERROR,    // Received RA could not be parsed
+        ZERO_LIFETIME,  // Received RA had 0 lifetime
+        UPDATE_NEW_RA,  // APF program updated for new RA
+        UPDATE_EXPIRY   // APF program updated for expiry
+    }
+
+    /**
+     * APF packet counters.
+     *
+     * Packet counters are 32bit big-endian values, and allocated near the end of the APF data
+     * buffer, using negative byte offsets, where -4 is equivalent to maximumApfProgramSize - 4,
+     * the last writable 32bit word.
+     */
+    @VisibleForTesting
+    public static enum Counter {
+        RESERVED_OOB,  // Points to offset 0 from the end of the buffer (out-of-bounds)
+        TOTAL_PACKETS,
+        PASSED_ARP,
+        PASSED_DHCP,
+        PASSED_IPV4,
+        PASSED_IPV6_NON_ICMP,
+        PASSED_IPV4_UNICAST,
+        PASSED_IPV6_ICMP,
+        PASSED_IPV6_UNICAST_NON_ICMP,
+        PASSED_ARP_NON_IPV4,
+        PASSED_ARP_UNKNOWN,
+        PASSED_ARP_UNICAST_REPLY,
+        PASSED_NON_IP_UNICAST,
+        DROPPED_ETH_BROADCAST,
+        DROPPED_RA,
+        DROPPED_GARP_REPLY,
+        DROPPED_ARP_OTHER_HOST,
+        DROPPED_IPV4_L2_BROADCAST,
+        DROPPED_IPV4_BROADCAST_ADDR,
+        DROPPED_IPV4_BROADCAST_NET,
+        DROPPED_IPV4_MULTICAST,
+        DROPPED_IPV6_ROUTER_SOLICITATION,
+        DROPPED_IPV6_MULTICAST_NA,
+        DROPPED_IPV6_MULTICAST,
+        DROPPED_IPV6_MULTICAST_PING,
+        DROPPED_IPV6_NON_ICMP_MULTICAST,
+        DROPPED_802_3_FRAME,
+        DROPPED_ETHERTYPE_BLACKLISTED,
+        DROPPED_ARP_REPLY_SPA_NO_HOST;
+
+        // Returns the negative byte offset from the end of the APF data segment for
+        // a given counter.
+        public int offset() {
+            return - this.ordinal() * 4;  // Currently, all counters are 32bit long.
+        }
+
+        // Returns the total size of the data segment in bytes.
+        public static int totalSize() {
+            return (Counter.class.getEnumConstants().length - 1) * 4;
+        }
+    }
+
+    /**
+     * When APFv4 is supported, loads R1 with the offset of the specified counter.
+     */
+    private void maybeSetupCounter(ApfGenerator gen, Counter c) {
+        if (mApfCapabilities.hasDataAccess()) {
+            gen.addLoadImmediate(Register.R1, c.offset());
+        }
+    }
+
+    // When APFv4 is supported, these point to the trampolines generated by emitEpilogue().
+    // Otherwise, they're just aliases for PASS_LABEL and DROP_LABEL.
+    private final String mCountAndPassLabel;
+    private final String mCountAndDropLabel;
+
+    // Thread to listen for RAs.
+    @VisibleForTesting
+    class ReceiveThread extends Thread {
+        private final byte[] mPacket = new byte[1514];
+        private final FileDescriptor mSocket;
+        private final long mStart = SystemClock.elapsedRealtime();
+
+        private int mReceivedRas = 0;
+        private int mMatchingRas = 0;
+        private int mDroppedRas = 0;
+        private int mParseErrors = 0;
+        private int mZeroLifetimeRas = 0;
+        private int mProgramUpdates = 0;
+
+        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);
+                    updateStats(processRa(mPacket, length));
+                } catch (IOException|ErrnoException e) {
+                    if (!mStopped) {
+                        Log.e(TAG, "Read error", e);
+                    }
+                }
+            }
+            logStats();
+        }
+
+        private void updateStats(ProcessRaResult result) {
+            mReceivedRas++;
+            switch(result) {
+                case MATCH:
+                    mMatchingRas++;
+                    return;
+                case DROPPED:
+                    mDroppedRas++;
+                    return;
+                case PARSE_ERROR:
+                    mParseErrors++;
+                    return;
+                case ZERO_LIFETIME:
+                    mZeroLifetimeRas++;
+                    return;
+                case UPDATE_EXPIRY:
+                    mMatchingRas++;
+                    mProgramUpdates++;
+                    return;
+                case UPDATE_NEW_RA:
+                    mProgramUpdates++;
+                    return;
+            }
+        }
+
+        private void logStats() {
+            final long nowMs = SystemClock.elapsedRealtime();
+            synchronized (this) {
+                final ApfStats stats = new ApfStats.Builder()
+                        .setReceivedRas(mReceivedRas)
+                        .setMatchingRas(mMatchingRas)
+                        .setDroppedRas(mDroppedRas)
+                        .setParseErrors(mParseErrors)
+                        .setZeroLifetimeRas(mZeroLifetimeRas)
+                        .setProgramUpdates(mProgramUpdates)
+                        .setDurationMs(nowMs - mStart)
+                        .setMaxProgramSize(mApfCapabilities.maximumApfProgramSize)
+                        .setProgramUpdatesAll(mNumProgramUpdates)
+                        .setProgramUpdatesAllowingMulticast(mNumProgramUpdatesAllowingMulticast)
+                        .build();
+                mMetricsLog.log(stats);
+                logApfProgramEventLocked(nowMs / DateUtils.SECOND_IN_MILLIS);
+            }
+        }
+    }
+
+    private static final String TAG = "ApfFilter";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+
+    private static final int ETH_HEADER_LEN = 14;
+    private static final int ETH_DEST_ADDR_OFFSET = 0;
+    private static final int ETH_ETHERTYPE_OFFSET = 12;
+    private static final int ETH_TYPE_MIN = 0x0600;
+    private static final int ETH_TYPE_MAX = 0xFFFF;
+    private static final byte[] ETH_BROADCAST_MAC_ADDRESS =
+            {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
+    // TODO: Make these offsets relative to end of link-layer header; don't include ETH_HEADER_LEN.
+    private static final int IPV4_FRAGMENT_OFFSET_OFFSET = ETH_HEADER_LEN + 6;
+    // Endianness is not an issue for this constant because the APF interpreter always operates in
+    // network byte order.
+    private static final int IPV4_FRAGMENT_OFFSET_MASK = 0x1fff;
+    private static final int IPV4_PROTOCOL_OFFSET = ETH_HEADER_LEN + 9;
+    private static final int IPV4_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 16;
+    private static final int IPV4_ANY_HOST_ADDRESS = 0;
+    private static final int IPV4_BROADCAST_ADDRESS = -1; // 255.255.255.255
+
+    // Traffic class and Flow label are not byte aligned. Luckily we
+    // don't care about either value so we'll consider bytes 1-3 of the
+    // IPv6 header as don't care.
+    private static final int IPV6_FLOW_LABEL_OFFSET = ETH_HEADER_LEN + 1;
+    private static final int IPV6_FLOW_LABEL_LEN = 3;
+    private static final int IPV6_NEXT_HEADER_OFFSET = ETH_HEADER_LEN + 6;
+    private static final int IPV6_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 8;
+    private static final int IPV6_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 24;
+    private static final int IPV6_HEADER_LEN = 40;
+    // The IPv6 all nodes address ff02::1
+    private static final byte[] IPV6_ALL_NODES_ADDRESS =
+            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
+
+    private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
+
+    // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
+    private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
+    private static final int UDP_HEADER_LEN = 8;
+
+    private static final int DHCP_CLIENT_PORT = 68;
+    // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
+    private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 28;
+
+    private static final int ARP_HEADER_OFFSET = ETH_HEADER_LEN;
+    private static final byte[] ARP_IPV4_HEADER = {
+            0, 1, // Hardware type: Ethernet (1)
+            8, 0, // Protocol type: IP (0x0800)
+            6,    // Hardware size: 6
+            4,    // Protocol size: 4
+    };
+    private static final int ARP_OPCODE_OFFSET = ARP_HEADER_OFFSET + 6;
+    // Opcode: ARP request (0x0001), ARP reply (0x0002)
+    private static final short ARP_OPCODE_REQUEST = 1;
+    private static final short ARP_OPCODE_REPLY = 2;
+    private static final int ARP_SOURCE_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 14;
+    private static final int ARP_TARGET_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 24;
+    // Do not log ApfProgramEvents whose actual lifetimes was less than this.
+    private static final int APF_PROGRAM_EVENT_LIFETIME_THRESHOLD = 2;
+    // Limit on the Black List size to cap on program usage for this
+    // TODO: Select a proper max length
+    private static final int APF_MAX_ETH_TYPE_BLACK_LIST_LEN = 20;
+
+    private final ApfCapabilities mApfCapabilities;
+    private final IpClientCallbacksWrapper mIpClientCallback;
+    private final InterfaceParams mInterfaceParams;
+    private final IpConnectivityLog mMetricsLog;
+
+    @VisibleForTesting
+    byte[] mHardwareAddress;
+    @VisibleForTesting
+    ReceiveThread mReceiveThread;
+    @GuardedBy("this")
+    private long mUniqueCounter;
+    @GuardedBy("this")
+    private boolean mMulticastFilter;
+    @GuardedBy("this")
+    private boolean mInDozeMode;
+    private final boolean mDrop802_3Frames;
+    private final int[] mEthTypeBlackList;
+
+    // Detects doze mode state transitions.
+    private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)) {
+                PowerManager powerManager =
+                        (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+                final boolean deviceIdle = powerManager.isDeviceIdleMode();
+                setDozeMode(deviceIdle);
+            }
+        }
+    };
+    private final Context mContext;
+
+    // Our IPv4 address, if we have just one, otherwise null.
+    @GuardedBy("this")
+    private byte[] mIPv4Address;
+    // The subnet prefix length of our IPv4 network. Only valid if mIPv4Address is not null.
+    @GuardedBy("this")
+    private int mIPv4PrefixLength;
+
+    @VisibleForTesting
+    ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
+            IpClientCallbacksWrapper ipClientCallback, IpConnectivityLog log) {
+        mApfCapabilities = config.apfCapabilities;
+        mIpClientCallback = ipClientCallback;
+        mInterfaceParams = ifParams;
+        mMulticastFilter = config.multicastFilter;
+        mDrop802_3Frames = config.ieee802_3Filter;
+        mContext = context;
+
+        if (mApfCapabilities.hasDataAccess()) {
+            mCountAndPassLabel = "countAndPass";
+            mCountAndDropLabel = "countAndDrop";
+        } else {
+            // APFv4 unsupported: turn jumps to the counter trampolines to immediately PASS or DROP,
+            // preserving the original pre-APFv4 behavior.
+            mCountAndPassLabel = ApfGenerator.PASS_LABEL;
+            mCountAndDropLabel = ApfGenerator.DROP_LABEL;
+        }
+
+        // Now fill the black list from the passed array
+        mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList);
+
+        mMetricsLog = log;
+
+        // TODO: ApfFilter should not generate programs until IpClient sends provisioning success.
+        maybeStartFilter();
+
+        // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
+        mContext.registerReceiver(mDeviceIdleReceiver,
+                new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
+    }
+
+    public synchronized void setDataSnapshot(byte[] data) {
+        mDataSnapshot = data;
+    }
+
+    private void log(String s) {
+        Log.d(TAG, "(" + mInterfaceParams.name + "): " + s);
+    }
+
+    @GuardedBy("this")
+    private long getUniqueNumberLocked() {
+        return mUniqueCounter++;
+    }
+
+    @GuardedBy("this")
+    private static int[] filterEthTypeBlackList(int[] ethTypeBlackList) {
+        ArrayList<Integer> bl = new ArrayList<Integer>();
+
+        for (int p : ethTypeBlackList) {
+            // Check if the protocol is a valid ether type
+            if ((p < ETH_TYPE_MIN) || (p > ETH_TYPE_MAX)) {
+                continue;
+            }
+
+            // Check if the protocol is not repeated in the passed array
+            if (bl.contains(p)) {
+                continue;
+            }
+
+            // Check if list reach its max size
+            if (bl.size() == APF_MAX_ETH_TYPE_BLACK_LIST_LEN) {
+                Log.w(TAG, "Passed EthType Black List size too large (" + bl.size() +
+                        ") using top " + APF_MAX_ETH_TYPE_BLACK_LIST_LEN + " protocols");
+                break;
+            }
+
+            // Now add the protocol to the list
+            bl.add(p);
+        }
+
+        return bl.stream().mapToInt(Integer::intValue).toArray();
+    }
+
+    /**
+     * Attempt to start listening for RAs and, if RAs are received, generating and installing
+     * filters to ignore useless RAs.
+     */
+    @VisibleForTesting
+    void maybeStartFilter() {
+        FileDescriptor socket;
+        try {
+            mHardwareAddress = mInterfaceParams.macAddr.toByteArray();
+            synchronized(this) {
+                // Clear the APF memory to reset all counters upon connecting to the first AP
+                // in an SSID. This is limited to APFv4 devices because this large write triggers
+                // a crash on some older devices (b/78905546).
+                if (mApfCapabilities.hasDataAccess()) {
+                    byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize];
+                    mIpClientCallback.installPacketFilter(zeroes);
+                }
+
+                // Install basic filters
+                installNewProgramLocked();
+            }
+            socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6);
+            SocketAddress addr = makePacketSocketAddress(
+                    (short) ETH_P_IPV6, mInterfaceParams.index);
+            Os.bind(socket, addr);
+            NetworkUtils.attachRaFilter(socket, mApfCapabilities.apfPacketFormat);
+        } catch(SocketException|ErrnoException e) {
+            Log.e(TAG, "Error starting filter", e);
+            return;
+        }
+        mReceiveThread = new ReceiveThread(socket);
+        mReceiveThread.start();
+    }
+
+    // Returns seconds since device boot.
+    @VisibleForTesting
+    protected long currentTimeSeconds() {
+        return SystemClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS;
+    }
+
+    public static class InvalidRaException extends Exception {
+        public InvalidRaException(String m) {
+            super(m);
+        }
+    }
+
+    // A class to hold information about an RA.
+    @VisibleForTesting
+    class Ra {
+        // From RFC4861:
+        private static final int ICMP6_RA_HEADER_LEN = 16;
+        private static final int ICMP6_RA_CHECKSUM_OFFSET =
+                ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
+        private static final int ICMP6_RA_CHECKSUM_LEN = 2;
+        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;
+
+        // Note: mPacket's position() cannot be assumed to be reset.
+        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;
+
+        // For debugging only. Offsets into the packet where PIOs are.
+        private final ArrayList<Integer> mPrefixOptionOffsets = new ArrayList<>();
+
+        // For debugging only. Offsets into the packet where RDNSS options are.
+        private final ArrayList<Integer> mRdnssOptionOffsets = new ArrayList<>();
+
+        // For debugging only. How many times this RA was seen.
+        int seenCount = 0;
+
+        // For debugging only. Returns the hex representation of the last matching packet.
+        String getLastMatchingPacket() {
+            return HexDump.toHexString(mPacket.array(), 0, mPacket.capacity(),
+                    false /* lowercase */);
+        }
+
+        // For debugging only. Returns the string representation of the IPv6 address starting at
+        // position pos in the packet.
+        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 static once RA is its own class.
+        private void prefixOptionToString(StringBuffer sb, int offset) {
+            String prefix = IPv6AddresstoString(offset + 16);
+            int length = getUint8(mPacket, offset + 2);
+            long valid = getUint32(mPacket, offset + 4);
+            long preferred = getUint32(mPacket, offset + 8);
+            sb.append(String.format("%s/%d %ds/%ds ", prefix, length, valid, preferred));
+        }
+
+        private void rdnssOptionToString(StringBuffer sb, int offset) {
+            int optLen = getUint8(mPacket, offset + 1) * 8;
+            if (optLen < 24) return;  // Malformed or empty.
+            long lifetime = getUint32(mPacket, offset + 4);
+            int numServers = (optLen - 8) / 16;
+            sb.append("DNS ").append(lifetime).append("s");
+            for (int server = 0; server < numServers; server++) {
+                sb.append(" ").append(IPv6AddresstoString(offset + 8 + 16 * server));
+            }
+        }
+
+        public String toString() {
+            try {
+                StringBuffer sb = new StringBuffer();
+                sb.append(String.format("RA %s -> %s %ds ",
+                        IPv6AddresstoString(IPV6_SRC_ADDR_OFFSET),
+                        IPv6AddresstoString(IPV6_DEST_ADDR_OFFSET),
+                        getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET)));
+                for (int i: mPrefixOptionOffsets) {
+                    prefixOptionToString(sb, i);
+                }
+                for (int i: mRdnssOptionOffsets) {
+                    rdnssOptionToString(sb, i);
+                }
+                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.
+         * @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;
+        }
+
+        private int addNonLifetimeU32(int lastNonLifetimeStart) {
+            return addNonLifetime(lastNonLifetimeStart,
+                    ICMP6_4_BYTE_LIFETIME_OFFSET, ICMP6_4_BYTE_LIFETIME_LEN);
+        }
+
+        // Note that this parses RA and may throw InvalidRaException (from
+        // Buffer.position(int) or due to an invalid-length option) or IndexOutOfBoundsException
+        // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with
+        // specifications.
+        Ra(byte[] packet, int length) throws InvalidRaException {
+            if (length < ICMP6_RA_OPTION_OFFSET) {
+                throw new InvalidRaException("Not an ICMP6 router advertisement");
+            }
+
+            mPacket = ByteBuffer.wrap(Arrays.copyOf(packet, length));
+            mLastSeen = currentTimeSeconds();
+
+            // Sanity check packet in case a packet arrives before we attach RA filter
+            // to our packet socket. b/29586253
+            if (getUint16(mPacket, ETH_ETHERTYPE_OFFSET) != ETH_P_IPV6 ||
+                    getUint8(mPacket, IPV6_NEXT_HEADER_OFFSET) != IPPROTO_ICMPV6 ||
+                    getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMPV6_ROUTER_ADVERTISEMENT) {
+                throw new InvalidRaException("Not an ICMP6 router advertisement");
+            }
+
+
+            RaEvent.Builder builder = new RaEvent.Builder();
+
+            // Ignore the flow label and low 4 bits of traffic class.
+            int lastNonLifetimeStart = addNonLifetime(0,
+                    IPV6_FLOW_LABEL_OFFSET,
+                    IPV6_FLOW_LABEL_LEN);
+
+            // Ignore the checksum.
+            lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
+                    ICMP6_RA_CHECKSUM_OFFSET,
+                    ICMP6_RA_CHECKSUM_LEN);
+
+            // Parse router lifetime
+            lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
+                    ICMP6_RA_ROUTER_LIFETIME_OFFSET,
+                    ICMP6_RA_ROUTER_LIFETIME_LEN);
+            builder.updateRouterLifetime(getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET));
+
+            // Ensures that the RA is not truncated.
+            mPacket.position(ICMP6_RA_OPTION_OFFSET);
+            while (mPacket.hasRemaining()) {
+                final int position = mPacket.position();
+                final int optionType = getUint8(mPacket, position);
+                final int optionLength = getUint8(mPacket, position + 1) * 8;
+                long lifetime;
+                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);
+                        lifetime = getUint32(mPacket,
+                                position + ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET);
+                        builder.updatePrefixValidLifetime(lifetime);
+                        // Parse preferred lifetime
+                        lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
+                                ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET,
+                                ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN);
+                        lifetime = getUint32(mPacket,
+                                position + ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET);
+                        builder.updatePrefixPreferredLifetime(lifetime);
+                        mPrefixOptionOffsets.add(position);
+                        break;
+                    // These three options have the same lifetime offset and size, and
+                    // are processed with the same specialized addNonLifetimeU32:
+                    case ICMP6_RDNSS_OPTION_TYPE:
+                        mRdnssOptionOffsets.add(position);
+                        lastNonLifetimeStart = addNonLifetimeU32(lastNonLifetimeStart);
+                        lifetime = getUint32(mPacket, position + ICMP6_4_BYTE_LIFETIME_OFFSET);
+                        builder.updateRdnssLifetime(lifetime);
+                        break;
+                    case ICMP6_ROUTE_INFO_OPTION_TYPE:
+                        lastNonLifetimeStart = addNonLifetimeU32(lastNonLifetimeStart);
+                        lifetime = getUint32(mPacket, position + ICMP6_4_BYTE_LIFETIME_OFFSET);
+                        builder.updateRouteInfoLifetime(lifetime);
+                        break;
+                    case ICMP6_DNSSL_OPTION_TYPE:
+                        lastNonLifetimeStart = addNonLifetimeU32(lastNonLifetimeStart);
+                        lifetime = getUint32(mPacket, position + ICMP6_4_BYTE_LIFETIME_OFFSET);
+                        builder.updateDnsslLifetime(lifetime);
+                        break;
+                    default:
+                        // RFC4861 section 4.2 dictates we ignore unknown options for fowards
+                        // compatibility.
+                        break;
+                }
+                if (optionLength <= 0) {
+                    throw new InvalidRaException(String.format(
+                        "Invalid option length opt=%d len=%d", optionType, optionLength));
+                }
+                mPacket.position(position + optionLength);
+            }
+            // Mark non-lifetime bytes since last lifetime.
+            addNonLifetime(lastNonLifetimeStart, 0, 0);
+            mMinLifetime = minLifetime(packet, length);
+            mMetricsLog.log(builder.build());
+        }
+
+        // Ignoring lifetimes (which may change) does {@code packet} match this RA?
+        boolean matches(byte[] packet, int length) {
+            if (length != mPacket.capacity()) return false;
+            byte[] referencePacket = mPacket.array();
+            for (Pair<Integer, Integer> nonLifetime : mNonLifetimes) {
+                for (int i = nonLifetime.first; i < (nonLifetime.first + nonLifetime.second); i++) {
+                    if (packet[i] != referencePacket[i]) 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;
+
+                // The flow label is in mNonLifetimes, but it's not a lifetime.
+                if (offset == IPV6_FLOW_LABEL_OFFSET) {
+                    continue;
+                }
+
+                // The checksum is in mNonLifetimes, but it's not a lifetime.
+                if (offset == ICMP6_RA_CHECKSUM_OFFSET) {
+                    continue;
+                }
+
+                final int lifetimeLength = mNonLifetimes.get(i+1).first - offset;
+                final long optionLifetime;
+                switch (lifetimeLength) {
+                    case 2:
+                        optionLifetime = getUint16(byteBuffer, offset);
+                        break;
+                    case 4:
+                        optionLifetime = getUint32(byteBuffer, offset);
+                        break;
+                    default:
+                        throw new IllegalStateException("bogus lifetime size " + lifetimeLength);
+                }
+                minLifetime = Math.min(minLifetime, optionLifetime);
+            }
+            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 - (currentTimeSeconds() - mLastSeen);
+        }
+
+        boolean isExpired() {
+            // TODO: We may want to handle 0 lifetime RAs differently, if they are common. We'll
+            // have to calculte the filter lifetime specially as a fraction of 0 is still 0.
+            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.
+        @GuardedBy("ApfFilter.this")
+        long generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+            String nextFilterLabel = "Ra" + getUniqueNumberLocked();
+            // Skip if packet is not the right size
+            gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT);
+            gen.addJumpIfR0NotEquals(mPacket.capacity(), 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);
+                // Don't generate JNEBS instruction for 0 bytes as it always fails the
+                // ASSERT_FORWARD_IN_PROGRAM(pc + cmp_imm - 1) check where cmp_imm is
+                // the number of bytes to compare. nonLifetime is zero between the
+                // valid and preferred lifetimes in the prefix option.
+                if (nonLifetime.second != 0) {
+                    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;
+
+                    // Skip the Flow label.
+                    if (offset == IPV6_FLOW_LABEL_OFFSET) {
+                        continue;
+                    }
+                    // Skip the checksum.
+                    if (offset == ICMP6_RA_CHECKSUM_OFFSET) {
+                        continue;
+                    }
+                    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);
+                }
+            }
+            maybeSetupCounter(gen, Counter.DROPPED_RA);
+            gen.addJump(mCountAndDropLabel);
+            gen.defineLabel(nextFilterLabel);
+            return filterLifetime;
+        }
+    }
+
+    // Maximum number of RAs to filter for.
+    private static final int MAX_RAS = 10;
+
+    @GuardedBy("this")
+    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.
+    @GuardedBy("this")
+    private long mLastTimeInstalledProgram;
+    // How long should the last installed filter program live for? In seconds.
+    @GuardedBy("this")
+    private long mLastInstalledProgramMinLifetime;
+    @GuardedBy("this")
+    private ApfProgramEvent.Builder mLastInstallEvent;
+
+    // For debugging only. The last program installed.
+    @GuardedBy("this")
+    private byte[] mLastInstalledProgram;
+
+    /**
+     * For debugging only. Contains the latest APF buffer snapshot captured from the firmware.
+     *
+     * A typical size for this buffer is 4KB. It is present only if the WiFi HAL supports
+     * IWifiStaIface#readApfPacketFilterData(), and the APF interpreter advertised support for
+     * the opcodes to access the data buffer (LDDW and STDW).
+     */
+    @GuardedBy("this") @Nullable
+    private byte[] mDataSnapshot;
+
+    // How many times the program was updated since we started.
+    @GuardedBy("this")
+    private int mNumProgramUpdates = 0;
+    // How many times the program was updated since we started for allowing multicast traffic.
+    @GuardedBy("this")
+    private int mNumProgramUpdatesAllowingMulticast = 0;
+
+    /**
+     * Generate filter code to process ARP packets. Execution of this code ends in either the
+     * DROP_LABEL or PASS_LABEL and does not fall off the end.
+     * Preconditions:
+     *  - Packet being filtered is ARP
+     */
+    @GuardedBy("this")
+    private void generateArpFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+        // Here's a basic summary of what the ARP filter program does:
+        //
+        // if not ARP IPv4
+        //   pass
+        // if not ARP IPv4 reply or request
+        //   pass
+        // if ARP reply source ip is 0.0.0.0
+        //   drop
+        // if unicast ARP reply
+        //   pass
+        // if interface has no IPv4 address
+        //   if target ip is 0.0.0.0
+        //      drop
+        // else
+        //   if target ip is not the interface ip
+        //      drop
+        // pass
+
+        final String checkTargetIPv4 = "checkTargetIPv4";
+
+        // Pass if not ARP IPv4.
+        gen.addLoadImmediate(Register.R0, ARP_HEADER_OFFSET);
+        maybeSetupCounter(gen, Counter.PASSED_ARP_NON_IPV4);
+        gen.addJumpIfBytesNotEqual(Register.R0, ARP_IPV4_HEADER, mCountAndPassLabel);
+
+        // Pass if unknown ARP opcode.
+        gen.addLoad16(Register.R0, ARP_OPCODE_OFFSET);
+        gen.addJumpIfR0Equals(ARP_OPCODE_REQUEST, checkTargetIPv4); // Skip to unicast check
+        maybeSetupCounter(gen, Counter.PASSED_ARP_UNKNOWN);
+        gen.addJumpIfR0NotEquals(ARP_OPCODE_REPLY, mCountAndPassLabel);
+
+        // Drop if ARP reply source IP is 0.0.0.0
+        gen.addLoad32(Register.R0, ARP_SOURCE_IP_ADDRESS_OFFSET);
+        maybeSetupCounter(gen, Counter.DROPPED_ARP_REPLY_SPA_NO_HOST);
+        gen.addJumpIfR0Equals(IPV4_ANY_HOST_ADDRESS, mCountAndDropLabel);
+
+        // Pass if unicast reply.
+        gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
+        maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY);
+        gen.addJumpIfBytesNotEqual(Register.R0, ETH_BROADCAST_MAC_ADDRESS, mCountAndPassLabel);
+
+        // Either a unicast request, a unicast reply, or a broadcast reply.
+        gen.defineLabel(checkTargetIPv4);
+        if (mIPv4Address == null) {
+            // When there is no IPv4 address, drop GARP replies (b/29404209).
+            gen.addLoad32(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET);
+            maybeSetupCounter(gen, Counter.DROPPED_GARP_REPLY);
+            gen.addJumpIfR0Equals(IPV4_ANY_HOST_ADDRESS, mCountAndDropLabel);
+        } else {
+            // When there is an IPv4 address, drop unicast/broadcast requests
+            // and broadcast replies with a different target IPv4 address.
+            gen.addLoadImmediate(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET);
+            maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST);
+            gen.addJumpIfBytesNotEqual(Register.R0, mIPv4Address, mCountAndDropLabel);
+        }
+
+        maybeSetupCounter(gen, Counter.PASSED_ARP);
+        gen.addJump(mCountAndPassLabel);
+    }
+
+    /**
+     * Generate filter code to process IPv4 packets. Execution of this code ends in either the
+     * DROP_LABEL or PASS_LABEL and does not fall off the end.
+     * Preconditions:
+     *  - Packet being filtered is IPv4
+     */
+    @GuardedBy("this")
+    private void generateIPv4FilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+        // Here's a basic summary of what the IPv4 filter program does:
+        //
+        // if filtering multicast (i.e. multicast lock not held):
+        //   if it's DHCP destined to our MAC:
+        //     pass
+        //   if it's L2 broadcast:
+        //     drop
+        //   if it's IPv4 multicast:
+        //     drop
+        //   if it's IPv4 broadcast:
+        //     drop
+        // pass
+
+        if (mMulticastFilter) {
+            final String skipDhcpv4Filter = "skip_dhcp_v4_filter";
+
+            // Pass DHCP addressed to us.
+            // Check it's UDP.
+            gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET);
+            gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipDhcpv4Filter);
+            // Check it's not a fragment. This matches the BPF filter installed by the DHCP client.
+            gen.addLoad16(Register.R0, IPV4_FRAGMENT_OFFSET_OFFSET);
+            gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, skipDhcpv4Filter);
+            // Check it's addressed to DHCP client port.
+            gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+            gen.addLoad16Indexed(Register.R0, UDP_DESTINATION_PORT_OFFSET);
+            gen.addJumpIfR0NotEquals(DHCP_CLIENT_PORT, skipDhcpv4Filter);
+            // Check it's DHCP to our MAC address.
+            gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET);
+            // NOTE: Relies on R1 containing IPv4 header offset.
+            gen.addAddR1();
+            gen.addJumpIfBytesNotEqual(Register.R0, mHardwareAddress, skipDhcpv4Filter);
+            maybeSetupCounter(gen, Counter.PASSED_DHCP);
+            gen.addJump(mCountAndPassLabel);
+
+            // Drop all multicasts/broadcasts.
+            gen.defineLabel(skipDhcpv4Filter);
+
+            // If IPv4 destination address is in multicast range, drop.
+            gen.addLoad8(Register.R0, IPV4_DEST_ADDR_OFFSET);
+            gen.addAnd(0xf0);
+            maybeSetupCounter(gen, Counter.DROPPED_IPV4_MULTICAST);
+            gen.addJumpIfR0Equals(0xe0, mCountAndDropLabel);
+
+            // If IPv4 broadcast packet, drop regardless of L2 (b/30231088).
+            maybeSetupCounter(gen, Counter.DROPPED_IPV4_BROADCAST_ADDR);
+            gen.addLoad32(Register.R0, IPV4_DEST_ADDR_OFFSET);
+            gen.addJumpIfR0Equals(IPV4_BROADCAST_ADDRESS, mCountAndDropLabel);
+            if (mIPv4Address != null && mIPv4PrefixLength < 31) {
+                maybeSetupCounter(gen, Counter.DROPPED_IPV4_BROADCAST_NET);
+                int broadcastAddr = ipv4BroadcastAddress(mIPv4Address, mIPv4PrefixLength);
+                gen.addJumpIfR0Equals(broadcastAddr, mCountAndDropLabel);
+            }
+
+            // If L2 broadcast packet, drop.
+            // TODO: can we invert this condition to fall through to the common pass case below?
+            maybeSetupCounter(gen, Counter.PASSED_IPV4_UNICAST);
+            gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
+            gen.addJumpIfBytesNotEqual(Register.R0, ETH_BROADCAST_MAC_ADDRESS, mCountAndPassLabel);
+            maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST);
+            gen.addJump(mCountAndDropLabel);
+        }
+
+        // Otherwise, pass
+        maybeSetupCounter(gen, Counter.PASSED_IPV4);
+        gen.addJump(mCountAndPassLabel);
+    }
+
+
+    /**
+     * Generate filter code to process IPv6 packets. Execution of this code ends in either the
+     * DROP_LABEL or PASS_LABEL, or falls off the end for ICMPv6 packets.
+     * Preconditions:
+     *  - Packet being filtered is IPv6
+     */
+    @GuardedBy("this")
+    private void generateIPv6FilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+        // Here's a basic summary of what the IPv6 filter program does:
+        //
+        // if we're dropping multicast
+        //   if it's not IPCMv6 or it's ICMPv6 but we're in doze mode:
+        //     if it's multicast:
+        //       drop
+        //     pass
+        // if it's ICMPv6 RS to any:
+        //   drop
+        // if it's ICMPv6 NA to ff02::1:
+        //   drop
+
+        gen.addLoad8(Register.R0, IPV6_NEXT_HEADER_OFFSET);
+
+        // Drop multicast if the multicast filter is enabled.
+        if (mMulticastFilter) {
+            final String skipIPv6MulticastFilterLabel = "skipIPv6MulticastFilter";
+            final String dropAllIPv6MulticastsLabel = "dropAllIPv6Multicast";
+
+            // While in doze mode, drop ICMPv6 multicast pings, let the others pass.
+            // While awake, let all ICMPv6 multicasts through.
+            if (mInDozeMode) {
+                // Not ICMPv6? -> Proceed to multicast filtering
+                gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, dropAllIPv6MulticastsLabel);
+
+                // ICMPv6 but not ECHO? -> Skip the multicast filter.
+                // (ICMPv6 ECHO requests will go through the multicast filter below).
+                gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
+                gen.addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipIPv6MulticastFilterLabel);
+            } else {
+                gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIPv6MulticastFilterLabel);
+            }
+
+            // Drop all other packets sent to ff00::/8 (multicast prefix).
+            gen.defineLabel(dropAllIPv6MulticastsLabel);
+            maybeSetupCounter(gen, Counter.DROPPED_IPV6_NON_ICMP_MULTICAST);
+            gen.addLoad8(Register.R0, IPV6_DEST_ADDR_OFFSET);
+            gen.addJumpIfR0Equals(0xff, mCountAndDropLabel);
+            // Not multicast. Pass.
+            maybeSetupCounter(gen, Counter.PASSED_IPV6_UNICAST_NON_ICMP);
+            gen.addJump(mCountAndPassLabel);
+            gen.defineLabel(skipIPv6MulticastFilterLabel);
+        } else {
+            // If not ICMPv6, pass.
+            maybeSetupCounter(gen, Counter.PASSED_IPV6_NON_ICMP);
+            gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, mCountAndPassLabel);
+        }
+
+        // If we got this far, the packet is ICMPv6.  Drop some specific types.
+
+        // Add unsolicited multicast neighbor announcements filter
+        String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA";
+        gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
+        // Drop all router solicitations (b/32833400)
+        maybeSetupCounter(gen, Counter.DROPPED_IPV6_ROUTER_SOLICITATION);
+        gen.addJumpIfR0Equals(ICMPV6_ROUTER_SOLICITATION, mCountAndDropLabel);
+        // If not neighbor announcements, skip filter.
+        gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel);
+        // If to ff02::1, drop.
+        // TODO: Drop only if they don't contain the address of on-link neighbours.
+        gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET);
+        gen.addJumpIfBytesNotEqual(Register.R0, IPV6_ALL_NODES_ADDRESS,
+                skipUnsolicitedMulticastNALabel);
+        maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA);
+        gen.addJump(mCountAndDropLabel);
+        gen.defineLabel(skipUnsolicitedMulticastNALabel);
+    }
+
+    /**
+     * Begin generating an APF program to:
+     * <ul>
+     * <li>Drop/Pass 802.3 frames (based on policy)
+     * <li>Drop packets with EtherType within the Black List
+     * <li>Drop ARP requests not for us, if mIPv4Address is set,
+     * <li>Drop IPv4 broadcast packets, except DHCP destined to our MAC,
+     * <li>Drop IPv4 multicast packets, if mMulticastFilter,
+     * <li>Pass all other IPv4 packets,
+     * <li>Drop all broadcast non-IP non-ARP packets.
+     * <li>Pass all non-ICMPv6 IPv6 packets,
+     * <li>Pass all non-IPv4 and non-IPv6 packets,
+     * <li>Drop IPv6 ICMPv6 NAs to ff02::1.
+     * <li>Drop IPv6 ICMPv6 RSs.
+     * <li>Let execution continue off the end of the program for IPv6 ICMPv6 packets. This allows
+     *     insertion of RA filters here, or if there aren't any, just passes the packets.
+     * </ul>
+     */
+    @GuardedBy("this")
+    private ApfGenerator emitPrologueLocked() throws IllegalInstructionException {
+        // This is guaranteed to succeed because of the check in maybeCreate.
+        ApfGenerator gen = new ApfGenerator(mApfCapabilities.apfVersionSupported);
+
+        if (mApfCapabilities.hasDataAccess()) {
+            // Increment TOTAL_PACKETS
+            maybeSetupCounter(gen, Counter.TOTAL_PACKETS);
+            gen.addLoadData(Register.R0, 0);  // load counter
+            gen.addAdd(1);
+            gen.addStoreData(Register.R0, 0);  // write-back counter
+        }
+
+        // Here's a basic summary of what the initial program does:
+        //
+        // if it's a 802.3 Frame (ethtype < 0x0600):
+        //    drop or pass based on configurations
+        // if it has a ether-type that belongs to the black list
+        //    drop
+        // if it's ARP:
+        //   insert ARP filter to drop or pass these appropriately
+        // if it's IPv4:
+        //   insert IPv4 filter to drop or pass these appropriately
+        // if it's not IPv6:
+        //   if it's broadcast:
+        //     drop
+        //   pass
+        // insert IPv6 filter to drop, pass, or fall off the end for ICMPv6 packets
+
+        gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
+
+        if (mDrop802_3Frames) {
+            // drop 802.3 frames (ethtype < 0x0600)
+            maybeSetupCounter(gen, Counter.DROPPED_802_3_FRAME);
+            gen.addJumpIfR0LessThan(ETH_TYPE_MIN, mCountAndDropLabel);
+        }
+
+        // Handle ether-type black list
+        maybeSetupCounter(gen, Counter.DROPPED_ETHERTYPE_BLACKLISTED);
+        for (int p : mEthTypeBlackList) {
+            gen.addJumpIfR0Equals(p, mCountAndDropLabel);
+        }
+
+        // Add ARP filters:
+        String skipArpFiltersLabel = "skipArpFilters";
+        gen.addJumpIfR0NotEquals(ETH_P_ARP, skipArpFiltersLabel);
+        generateArpFilterLocked(gen);
+        gen.defineLabel(skipArpFiltersLabel);
+
+        // Add IPv4 filters:
+        String skipIPv4FiltersLabel = "skipIPv4Filters";
+        // NOTE: Relies on R0 containing ethertype. This is safe because if we got here, we did not
+        // execute the ARP filter, since that filter does not fall through, but either drops or
+        // passes.
+        gen.addJumpIfR0NotEquals(ETH_P_IP, skipIPv4FiltersLabel);
+        generateIPv4FilterLocked(gen);
+        gen.defineLabel(skipIPv4FiltersLabel);
+
+        // Check for IPv6:
+        // NOTE: Relies on R0 containing ethertype. This is safe because if we got here, we did not
+        // execute the ARP or IPv4 filters, since those filters do not fall through, but either
+        // drop or pass.
+        String ipv6FilterLabel = "IPv6Filters";
+        gen.addJumpIfR0Equals(ETH_P_IPV6, ipv6FilterLabel);
+
+        // Drop non-IP non-ARP broadcasts, pass the rest
+        gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
+        maybeSetupCounter(gen, Counter.PASSED_NON_IP_UNICAST);
+        gen.addJumpIfBytesNotEqual(Register.R0, ETH_BROADCAST_MAC_ADDRESS, mCountAndPassLabel);
+        maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST);
+        gen.addJump(mCountAndDropLabel);
+
+        // Add IPv6 filters:
+        gen.defineLabel(ipv6FilterLabel);
+        generateIPv6FilterLocked(gen);
+        return gen;
+    }
+
+    /**
+     * Append packet counting epilogue to the APF program.
+     *
+     * Currently, the epilogue consists of two trampolines which count passed and dropped packets
+     * before jumping to the actual PASS and DROP labels.
+     */
+    @GuardedBy("this")
+    private void emitEpilogue(ApfGenerator gen) throws IllegalInstructionException {
+        // If APFv4 is unsupported, no epilogue is necessary: if execution reached this far, it
+        // will just fall-through to the PASS label.
+        if (!mApfCapabilities.hasDataAccess()) return;
+
+        // Execution will reach the bottom of the program if none of the filters match,
+        // which will pass the packet to the application processor.
+        maybeSetupCounter(gen, Counter.PASSED_IPV6_ICMP);
+
+        // Append the count & pass trampoline, which increments the counter at the data address
+        // pointed to by R1, then jumps to the pass label. This saves a few bytes over inserting
+        // the entire sequence inline for every counter.
+        gen.defineLabel(mCountAndPassLabel);
+        gen.addLoadData(Register.R0, 0);   // R0 = *(R1 + 0)
+        gen.addAdd(1);                     // R0++
+        gen.addStoreData(Register.R0, 0);  // *(R1 + 0) = R0
+        gen.addJump(gen.PASS_LABEL);
+
+        // Same as above for the count & drop trampoline.
+        gen.defineLabel(mCountAndDropLabel);
+        gen.addLoadData(Register.R0, 0);   // R0 = *(R1 + 0)
+        gen.addAdd(1);                     // R0++
+        gen.addStoreData(Register.R0, 0);  // *(R1 + 0) = R0
+        gen.addJump(gen.DROP_LABEL);
+    }
+
+    /**
+     * Generate and install a new filter program.
+     */
+    @GuardedBy("this")
+    @VisibleForTesting
+    void installNewProgramLocked() {
+        purgeExpiredRasLocked();
+        ArrayList<Ra> rasToFilter = new ArrayList<>();
+        final byte[] program;
+        long programMinLifetime = Long.MAX_VALUE;
+        long maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize;
+        if (mApfCapabilities.hasDataAccess()) {
+            // Reserve space for the counters.
+            maximumApfProgramSize -= Counter.totalSize();
+        }
+
+        try {
+            // Step 1: Determine how many RA filters we can fit in the program.
+            ApfGenerator gen = emitPrologueLocked();
+
+            // The epilogue normally goes after the RA filters, but add it early to include its
+            // length when estimating the total.
+            emitEpilogue(gen);
+
+            // Can't fit the program even without any RA filters?
+            if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
+                Log.e(TAG, "Program exceeds maximum size " + maximumApfProgramSize);
+                return;
+            }
+
+            for (Ra ra : mRas) {
+                ra.generateFilterLocked(gen);
+                // Stop if we get too big.
+                if (gen.programLengthOverEstimate() > maximumApfProgramSize) break;
+                rasToFilter.add(ra);
+            }
+
+            // Step 2: Actually generate the program
+            gen = emitPrologueLocked();
+            for (Ra ra : rasToFilter) {
+                programMinLifetime = Math.min(programMinLifetime, ra.generateFilterLocked(gen));
+            }
+            emitEpilogue(gen);
+            program = gen.generate();
+        } catch (IllegalInstructionException|IllegalStateException e) {
+            Log.e(TAG, "Failed to generate APF program.", e);
+            return;
+        }
+        final long now = currentTimeSeconds();
+        mLastTimeInstalledProgram = now;
+        mLastInstalledProgramMinLifetime = programMinLifetime;
+        mLastInstalledProgram = program;
+        mNumProgramUpdates++;
+
+        if (VDBG) {
+            hexDump("Installing filter: ", program, program.length);
+        }
+        mIpClientCallback.installPacketFilter(program);
+        logApfProgramEventLocked(now);
+        mLastInstallEvent = new ApfProgramEvent.Builder()
+                .setLifetime(programMinLifetime)
+                .setFilteredRas(rasToFilter.size())
+                .setCurrentRas(mRas.size())
+                .setProgramLength(program.length)
+                .setFlags(mIPv4Address != null, mMulticastFilter);
+    }
+
+    @GuardedBy("this")
+    private void logApfProgramEventLocked(long now) {
+        if (mLastInstallEvent == null) {
+            return;
+        }
+        ApfProgramEvent.Builder ev = mLastInstallEvent;
+        mLastInstallEvent = null;
+        final long actualLifetime = now - mLastTimeInstalledProgram;
+        ev.setActualLifetime(actualLifetime);
+        if (actualLifetime < APF_PROGRAM_EVENT_LIFETIME_THRESHOLD) {
+            return;
+        }
+        mMetricsLog.log(ev.build());
+    }
+
+    /**
+     * Returns {@code true} if a new program should be installed because the current one dies soon.
+     */
+    private boolean shouldInstallnewProgram() {
+        long expiry = mLastTimeInstalledProgram + mLastInstalledProgramMinLifetime;
+        return expiry < currentTimeSeconds() + MAX_PROGRAM_LIFETIME_WORTH_REFRESHING;
+    }
+
+    private void hexDump(String msg, byte[] packet, int length) {
+        log(msg + HexDump.toHexString(packet, 0, length, false /* lowercase */));
+    }
+
+    @GuardedBy("this")
+    private void purgeExpiredRasLocked() {
+        for (int i = 0; i < mRas.size();) {
+            if (mRas.get(i).isExpired()) {
+                log("Expiring " + mRas.get(i));
+                mRas.remove(i);
+            } else {
+                i++;
+            }
+        }
+    }
+
+    /**
+     * Process an RA packet, updating the list of known RAs and installing a new APF program
+     * if the current APF program should be updated.
+     * @return a ProcessRaResult enum describing what action was performed.
+     */
+    @VisibleForTesting
+    synchronized ProcessRaResult processRa(byte[] packet, int 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)) {
+                if (VDBG) log("matched RA " + ra);
+                // Update lifetimes.
+                ra.mLastSeen = currentTimeSeconds();
+                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
+                // 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));
+
+                // If the current program doesn't expire for a while, don't update.
+                if (shouldInstallnewProgram()) {
+                    installNewProgramLocked();
+                    return ProcessRaResult.UPDATE_EXPIRY;
+                }
+                return ProcessRaResult.MATCH;
+            }
+        }
+        purgeExpiredRasLocked();
+        // TODO: figure out how to proceed when we've received more then MAX_RAS RAs.
+        if (mRas.size() >= MAX_RAS) {
+            return ProcessRaResult.DROPPED;
+        }
+        final Ra ra;
+        try {
+            ra = new Ra(packet, length);
+        } catch (Exception e) {
+            Log.e(TAG, "Error parsing RA", e);
+            return ProcessRaResult.PARSE_ERROR;
+        }
+        // Ignore 0 lifetime RAs.
+        if (ra.isExpired()) {
+            return ProcessRaResult.ZERO_LIFETIME;
+        }
+        log("Adding " + ra);
+        mRas.add(ra);
+        installNewProgramLocked();
+        return ProcessRaResult.UPDATE_NEW_RA;
+    }
+
+    /**
+     * Create an {@link ApfFilter} if {@code apfCapabilities} indicates support for packet
+     * filtering using APF programs.
+     */
+    public static ApfFilter maybeCreate(Context context, ApfConfiguration config,
+            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback) {
+        if (context == null || config == null || ifParams == null) return null;
+        ApfCapabilities apfCapabilities =  config.apfCapabilities;
+        if (apfCapabilities == null) return null;
+        if (apfCapabilities.apfVersionSupported == 0) return null;
+        if (apfCapabilities.maximumApfProgramSize < 512) {
+            Log.e(TAG, "Unacceptably small APF limit: " + apfCapabilities.maximumApfProgramSize);
+            return null;
+        }
+        // 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 (apfCapabilities.apfPacketFormat != ARPHRD_ETHER) return null;
+        if (!ApfGenerator.supportsVersion(apfCapabilities.apfVersionSupported)) {
+            Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
+            return null;
+        }
+
+        return new ApfFilter(context, config, ifParams, ipClientCallback, new IpConnectivityLog());
+    }
+
+    public synchronized void shutdown() {
+        if (mReceiveThread != null) {
+            log("shutting down");
+            mReceiveThread.halt();  // Also closes socket.
+            mReceiveThread = null;
+        }
+        mRas.clear();
+        mContext.unregisterReceiver(mDeviceIdleReceiver);
+    }
+
+    public synchronized void setMulticastFilter(boolean isEnabled) {
+        if (mMulticastFilter == isEnabled) return;
+        mMulticastFilter = isEnabled;
+        if (!isEnabled) {
+            mNumProgramUpdatesAllowingMulticast++;
+        }
+        installNewProgramLocked();
+    }
+
+    @VisibleForTesting
+    public synchronized void setDozeMode(boolean isEnabled) {
+        if (mInDozeMode == isEnabled) return;
+        mInDozeMode = isEnabled;
+        installNewProgramLocked();
+    }
+
+    /** Find the single IPv4 LinkAddress if there is one, otherwise return null. */
+    private static LinkAddress findIPv4LinkAddress(LinkProperties lp) {
+        LinkAddress ipv4Address = null;
+        for (LinkAddress address : lp.getLinkAddresses()) {
+            if (!(address.getAddress() instanceof Inet4Address)) {
+                continue;
+            }
+            if (ipv4Address != null && !ipv4Address.isSameAddressAs(address)) {
+                // More than one IPv4 address, abort.
+                return null;
+            }
+            ipv4Address = address;
+        }
+        return ipv4Address;
+    }
+
+    public synchronized void setLinkProperties(LinkProperties lp) {
+        // NOTE: Do not keep a copy of LinkProperties as it would further duplicate state.
+        final LinkAddress ipv4Address = findIPv4LinkAddress(lp);
+        final byte[] addr = (ipv4Address != null) ? ipv4Address.getAddress().getAddress() : null;
+        final int prefix = (ipv4Address != null) ? ipv4Address.getPrefixLength() : 0;
+        if ((prefix == mIPv4PrefixLength) && Arrays.equals(addr, mIPv4Address)) {
+            return;
+        }
+        mIPv4Address = addr;
+        mIPv4PrefixLength = prefix;
+        installNewProgramLocked();
+    }
+
+    static public long counterValue(byte[] data, Counter counter)
+            throws ArrayIndexOutOfBoundsException {
+        // Follow the same wrap-around addressing scheme of the interpreter.
+        int offset = counter.offset();
+        if (offset < 0) {
+            offset = data.length + offset;
+        }
+
+        // Decode 32bit big-endian integer into a long so we can count up beyond 2^31.
+        long value = 0;
+        for (int i = 0; i < 4; i++) {
+            value = value << 8 | (data[offset] & 0xFF);
+            offset++;
+        }
+        return value;
+    }
+
+    public synchronized void dump(IndentingPrintWriter pw) {
+        pw.println("Capabilities: " + mApfCapabilities);
+        pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED"));
+        pw.println("Multicast: " + (mMulticastFilter ? "DROP" : "ALLOW"));
+        try {
+            pw.println("IPv4 address: " + InetAddress.getByAddress(mIPv4Address).getHostAddress());
+        } catch (UnknownHostException|NullPointerException e) {}
+
+        if (mLastTimeInstalledProgram == 0) {
+            pw.println("No program installed.");
+            return;
+        }
+        pw.println("Program updates: " + mNumProgramUpdates);
+        pw.println(String.format(
+                "Last program length %d, installed %ds ago, lifetime %ds",
+                mLastInstalledProgram.length, currentTimeSeconds() - 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, currentTimeSeconds() - ra.mLastSeen));
+            if (DBG) {
+                pw.println("Last match:");
+                pw.increaseIndent();
+                pw.println(ra.getLastMatchingPacket());
+                pw.decreaseIndent();
+            }
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+
+        if (DBG) {
+            pw.println("Last program:");
+            pw.increaseIndent();
+            pw.println(HexDump.toHexString(mLastInstalledProgram, false /* lowercase */));
+            pw.decreaseIndent();
+        }
+
+        pw.println("APF packet counters: ");
+        pw.increaseIndent();
+        if (!mApfCapabilities.hasDataAccess()) {
+            pw.println("APF counters not supported");
+        } else if (mDataSnapshot == null) {
+            pw.println("No last snapshot.");
+        } else {
+            try {
+                Counter[] counters = Counter.class.getEnumConstants();
+                for (Counter c : Arrays.asList(counters).subList(1, counters.length)) {
+                    long value = counterValue(mDataSnapshot, c);
+                    // Only print non-zero counters
+                    if (value != 0) {
+                        pw.println(c.toString() + ": " + value);
+                    }
+                }
+            } catch (ArrayIndexOutOfBoundsException e) {
+                pw.println("Uh-oh: " + e);
+            }
+            if (VDBG) {
+                pw.println("Raw data dump: ");
+                pw.println(HexDump.dumpHexString(mDataSnapshot));
+            }
+        }
+        pw.decreaseIndent();
+    }
+
+    // TODO: move to android.net.NetworkUtils
+    @VisibleForTesting
+    public static int ipv4BroadcastAddress(byte[] addrBytes, int prefixLength) {
+        return bytesToBEInt(addrBytes) | (int) (uint32(-1) >>> prefixLength);
+    }
+}
diff --git a/src/android/net/apf/ApfGenerator.java b/src/android/net/apf/ApfGenerator.java
new file mode 100644
index 0000000..87a1b5e
--- /dev/null
+++ b/src/android/net/apf/ApfGenerator.java
@@ -0,0 +1,937 @@
+/*
+ * 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 android.net.apf;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * APF assembler/generator.  A tool for generating an APF program.
+ *
+ * Call add*() functions to add instructions to the program, then call
+ * {@link generate} to get the APF bytecode for the program.
+ *
+ * @hide
+ */
+public class ApfGenerator {
+    /**
+     * This exception is thrown when an attempt is made to generate an illegal instruction.
+     */
+    public static class IllegalInstructionException extends Exception {
+        IllegalInstructionException(String msg) {
+            super(msg);
+        }
+    }
+    private enum Opcodes {
+        LABEL(-1),
+        LDB(1),    // Load 1 byte from immediate offset, e.g. "ldb R0, [5]"
+        LDH(2),    // Load 2 bytes from immediate offset, e.g. "ldh R0, [5]"
+        LDW(3),    // Load 4 bytes from immediate offset, e.g. "ldw R0, [5]"
+        LDBX(4),   // Load 1 byte from immediate offset plus register, e.g. "ldbx R0, [5]R0"
+        LDHX(5),   // Load 2 byte from immediate offset plus register, e.g. "ldhx R0, [5]R0"
+        LDWX(6),   // Load 4 byte from immediate offset plus register, e.g. "ldwx R0, [5]R0"
+        ADD(7),    // Add, e.g. "add R0,5"
+        MUL(8),    // Multiply, e.g. "mul R0,5"
+        DIV(9),    // Divide, e.g. "div R0,5"
+        AND(10),   // And, e.g. "and R0,5"
+        OR(11),    // Or, e.g. "or R0,5"
+        SH(12),    // Left shift, e.g, "sh R0, 5" or "sh R0, -5" (shifts right)
+        LI(13),    // Load immediate, e.g. "li R0,5" (immediate encoded as signed value)
+        JMP(14),   // Jump, e.g. "jmp label"
+        JEQ(15),   // Compare equal and branch, e.g. "jeq R0,5,label"
+        JNE(16),   // Compare not equal and branch, e.g. "jne R0,5,label"
+        JGT(17),   // Compare greater than and branch, e.g. "jgt R0,5,label"
+        JLT(18),   // Compare less than and branch, e.g. "jlt R0,5,label"
+        JSET(19),  // Compare any bits set and branch, e.g. "jset R0,5,label"
+        JNEBS(20), // Compare not equal byte sequence, e.g. "jnebs R0,5,label,0x1122334455"
+        EXT(21),   // Followed by immediate indicating ExtendedOpcodes.
+        LDDW(22),  // Load 4 bytes from data memory address (register + immediate): "lddw R0, [5]R1"
+        STDW(23);  // Store 4 bytes to data memory address (register + immediate): "stdw R0, [5]R1"
+
+        final int value;
+
+        private Opcodes(int value) {
+            this.value = value;
+        }
+    }
+    // Extended opcodes. Primary opcode is Opcodes.EXT. ExtendedOpcodes are encoded in the immediate
+    // field.
+    private enum ExtendedOpcodes {
+        LDM(0),   // Load from memory, e.g. "ldm R0,5"
+        STM(16),  // Store to memory, e.g. "stm R0,5"
+        NOT(32),  // Not, e.g. "not R0"
+        NEG(33),  // Negate, e.g. "neg R0"
+        SWAP(34), // Swap, e.g. "swap R0,R1"
+        MOVE(35);  // Move, e.g. "move R0,R1"
+
+        final int value;
+
+        private ExtendedOpcodes(int value) {
+            this.value = value;
+        }
+    }
+    public enum Register {
+        R0(0),
+        R1(1);
+
+        final int value;
+
+        private Register(int value) {
+            this.value = value;
+        }
+    }
+    private class Instruction {
+        private final byte mOpcode;   // A "Opcode" value.
+        private final byte mRegister; // A "Register" value.
+        private boolean mHasImm;
+        private byte mImmSize;
+        private boolean mImmSigned;
+        private int mImm;
+        // When mOpcode is a jump:
+        private byte mTargetLabelSize;
+        private String mTargetLabel;
+        // When mOpcode == Opcodes.LABEL:
+        private String mLabel;
+        // When mOpcode == Opcodes.JNEBS:
+        private byte[] mCompareBytes;
+        // Offset in bytes from the begining of this program. Set by {@link ApfGenerator#generate}.
+        int offset;
+
+        Instruction(Opcodes opcode, Register register) {
+            mOpcode = (byte)opcode.value;
+            mRegister = (byte)register.value;
+        }
+
+        Instruction(Opcodes opcode) {
+            this(opcode, Register.R0);
+        }
+
+        void setImm(int imm, boolean signed) {
+            mHasImm = true;
+            mImm = imm;
+            mImmSigned = signed;
+            mImmSize = calculateImmSize(imm, signed);
+        }
+
+        void setUnsignedImm(int imm) {
+            setImm(imm, false);
+        }
+
+        void setSignedImm(int imm) {
+            setImm(imm, true);
+        }
+
+        void setLabel(String label) throws IllegalInstructionException {
+            if (mLabels.containsKey(label)) {
+                throw new IllegalInstructionException("duplicate label " + label);
+            }
+            if (mOpcode != Opcodes.LABEL.value) {
+                throw new IllegalStateException("adding label to non-label instruction");
+            }
+            mLabel = label;
+            mLabels.put(label, this);
+        }
+
+        void setTargetLabel(String label) {
+            mTargetLabel = label;
+            mTargetLabelSize = 4; // May shrink later on in generate().
+        }
+
+        void setCompareBytes(byte[] bytes) {
+            if (mOpcode != Opcodes.JNEBS.value) {
+                throw new IllegalStateException("adding compare bytes to non-JNEBS instruction");
+            }
+            mCompareBytes = bytes;
+        }
+
+        /**
+         * @return size of instruction in bytes.
+         */
+        int size() {
+            if (mOpcode == Opcodes.LABEL.value) {
+                return 0;
+            }
+            int size = 1;
+            if (mHasImm) {
+                size += generatedImmSize();
+            }
+            if (mTargetLabel != null) {
+                size += generatedImmSize();
+            }
+            if (mCompareBytes != null) {
+                size += mCompareBytes.length;
+            }
+            return size;
+        }
+
+        /**
+         * Resize immediate value field so that it's only as big as required to
+         * contain the offset of the jump destination.
+         * @return {@code true} if shrunk.
+         */
+        boolean shrink() throws IllegalInstructionException {
+            if (mTargetLabel == null) {
+                return false;
+            }
+            int oldSize = size();
+            int oldTargetLabelSize = mTargetLabelSize;
+            mTargetLabelSize = calculateImmSize(calculateTargetLabelOffset(), false);
+            if (mTargetLabelSize > oldTargetLabelSize) {
+                throw new IllegalStateException("instruction grew");
+            }
+            return size() < oldSize;
+        }
+
+        /**
+         * Assemble value for instruction size field.
+         */
+        private byte generateImmSizeField() {
+            byte immSize = generatedImmSize();
+            // Encode size field to fit in 2 bits: 0->0, 1->1, 2->2, 3->4.
+            return immSize == 4 ? 3 : immSize;
+        }
+
+        /**
+         * Assemble first byte of generated instruction.
+         */
+        private byte generateInstructionByte() {
+            byte sizeField = generateImmSizeField();
+            return (byte)((mOpcode << 3) | (sizeField << 1) | mRegister);
+        }
+
+        /**
+         * Write {@code value} at offset {@code writingOffset} into {@code bytecode}.
+         * {@link generatedImmSize} bytes are written. {@code value} is truncated to
+         * {@code generatedImmSize} bytes. {@code value} is treated simply as a
+         * 32-bit value, so unsigned values should be zero extended and the truncation
+         * should simply throw away their zero-ed upper bits, and signed values should
+         * be sign extended and the truncation should simply throw away their signed
+         * upper bits.
+         */
+        private int writeValue(int value, byte[] bytecode, int writingOffset) {
+            for (int i = generatedImmSize() - 1; i >= 0; i--) {
+                bytecode[writingOffset++] = (byte)((value >> (i * 8)) & 255);
+            }
+            return writingOffset;
+        }
+
+        /**
+         * Generate bytecode for this instruction at offset {@link offset}.
+         */
+        void generate(byte[] bytecode) throws IllegalInstructionException {
+            if (mOpcode == Opcodes.LABEL.value) {
+                return;
+            }
+            int writingOffset = offset;
+            bytecode[writingOffset++] = generateInstructionByte();
+            if (mTargetLabel != null) {
+                writingOffset = writeValue(calculateTargetLabelOffset(), bytecode, writingOffset);
+            }
+            if (mHasImm) {
+                writingOffset = writeValue(mImm, bytecode, writingOffset);
+            }
+            if (mCompareBytes != null) {
+                System.arraycopy(mCompareBytes, 0, bytecode, writingOffset, mCompareBytes.length);
+                writingOffset += mCompareBytes.length;
+            }
+            if ((writingOffset - offset) != size()) {
+                throw new IllegalStateException("wrote " + (writingOffset - offset) +
+                        " but should have written " + size());
+            }
+        }
+
+        /**
+         * Calculate the size of either the immediate field or the target label field, if either is
+         * present. Most instructions have either an immediate or a target label field, but for the
+         * instructions that have both, the size of the target label field must be the same as the
+         * size of the immediate field, because there is only one length field in the instruction
+         * byte, hence why this function simply takes the maximum of the two sizes, so neither is
+         * truncated.
+         */
+        private byte generatedImmSize() {
+            return mImmSize > mTargetLabelSize ? mImmSize : mTargetLabelSize;
+        }
+
+        private int calculateTargetLabelOffset() throws IllegalInstructionException {
+            Instruction targetLabelInstruction;
+            if (mTargetLabel == DROP_LABEL) {
+                targetLabelInstruction = mDropLabel;
+            } else if (mTargetLabel == PASS_LABEL) {
+                targetLabelInstruction = mPassLabel;
+            } else {
+                targetLabelInstruction = mLabels.get(mTargetLabel);
+            }
+            if (targetLabelInstruction == null) {
+                throw new IllegalInstructionException("label not found: " + mTargetLabel);
+            }
+            // Calculate distance from end of this instruction to instruction.offset.
+            final int targetLabelOffset = targetLabelInstruction.offset - (offset + size());
+            if (targetLabelOffset < 0) {
+                throw new IllegalInstructionException("backward branches disallowed; label: " +
+                        mTargetLabel);
+            }
+            return targetLabelOffset;
+        }
+
+        private byte calculateImmSize(int imm, boolean signed) {
+            if (imm == 0) {
+                return 0;
+            }
+            if (signed && (imm >= -128 && imm <= 127) ||
+                    !signed && (imm >= 0 && imm <= 255)) {
+                return 1;
+            }
+            if (signed && (imm >= -32768 && imm <= 32767) ||
+                    !signed && (imm >= 0 && imm <= 65535)) {
+                return 2;
+            }
+            return 4;
+        }
+    }
+
+    /**
+     * Jump to this label to terminate the program and indicate the packet
+     * should be dropped.
+     */
+    public static final String DROP_LABEL = "__DROP__";
+
+    /**
+     * Jump to this label to terminate the program and indicate the packet
+     * should be passed to the AP.
+     */
+    public static final String PASS_LABEL = "__PASS__";
+
+    /**
+     * Number of memory slots available for access via APF stores to memory and loads from memory.
+     * The memory slots are numbered 0 to {@code MEMORY_SLOTS} - 1. This must be kept in sync with
+     * the APF interpreter.
+     */
+    public static final int MEMORY_SLOTS = 16;
+
+    /**
+     * Memory slot number that is prefilled with the IPv4 header length.
+     * Note that this memory slot may be overwritten by a program that
+     * executes stores to this memory slot. This must be kept in sync with
+     * the APF interpreter.
+     */
+    public static final int IPV4_HEADER_SIZE_MEMORY_SLOT = 13;
+
+    /**
+     * Memory slot number that is prefilled with the size of the packet being filtered in bytes.
+     * Note that this memory slot may be overwritten by a program that
+     * executes stores to this memory slot. This must be kept in sync with the APF interpreter.
+     */
+    public static final int PACKET_SIZE_MEMORY_SLOT = 14;
+
+    /**
+     * Memory slot number that is prefilled with the age of the filter in seconds. The age of the
+     * filter is the time since the filter was installed until now.
+     * Note that this memory slot may be overwritten by a program that
+     * executes stores to this memory slot. This must be kept in sync with the APF interpreter.
+     */
+    public static final int FILTER_AGE_MEMORY_SLOT = 15;
+
+    /**
+     * First memory slot containing prefilled values. Can be used in range comparisons to determine
+     * if memory slot index is within prefilled slots.
+     */
+    public static final int FIRST_PREFILLED_MEMORY_SLOT = IPV4_HEADER_SIZE_MEMORY_SLOT;
+
+    /**
+     * Last memory slot containing prefilled values. Can be used in range comparisons to determine
+     * if memory slot index is within prefilled slots.
+     */
+    public static final int LAST_PREFILLED_MEMORY_SLOT = FILTER_AGE_MEMORY_SLOT;
+
+    // This version number syncs up with APF_VERSION in hardware/google/apf/apf_interpreter.h
+    private static final int MIN_APF_VERSION = 2;
+
+    private final ArrayList<Instruction> mInstructions = new ArrayList<Instruction>();
+    private final HashMap<String, Instruction> mLabels = new HashMap<String, Instruction>();
+    private final Instruction mDropLabel = new Instruction(Opcodes.LABEL);
+    private final Instruction mPassLabel = new Instruction(Opcodes.LABEL);
+    private final int mVersion;
+    private boolean mGenerated;
+
+    /**
+     * Creates an ApfGenerator instance which is able to emit instructions for the specified
+     * {@code version} of the APF interpreter. Throws {@code IllegalInstructionException} if
+     * the requested version is unsupported.
+     */
+    ApfGenerator(int version) throws IllegalInstructionException {
+        mVersion = version;
+        requireApfVersion(MIN_APF_VERSION);
+    }
+
+    /**
+     * Returns true if the ApfGenerator supports the specified {@code version}, otherwise false.
+     */
+    public static boolean supportsVersion(int version) {
+        return version >= MIN_APF_VERSION;
+    }
+
+    private void requireApfVersion(int minimumVersion) throws IllegalInstructionException {
+        if (mVersion < minimumVersion) {
+            throw new IllegalInstructionException("Requires APF >= " + minimumVersion);
+        }
+    }
+
+    private void addInstruction(Instruction instruction) {
+        if (mGenerated) {
+            throw new IllegalStateException("Program already generated");
+        }
+        mInstructions.add(instruction);
+    }
+
+    /**
+     * Define a label at the current end of the program. Jumps can jump to this label. Labels are
+     * their own separate instructions, though with size 0. This facilitates having labels with
+     * no corresponding code to execute, for example a label at the end of a program. For example
+     * an {@link ApfGenerator} might be passed to a function that adds a filter like so:
+     * <pre>
+     *   load from packet
+     *   compare loaded data, jump if not equal to "next_filter"
+     *   load from packet
+     *   compare loaded data, jump if not equal to "next_filter"
+     *   jump to drop label
+     *   define "next_filter" here
+     * </pre>
+     * In this case "next_filter" may not have any generated code associated with it.
+     */
+    public ApfGenerator defineLabel(String name) throws IllegalInstructionException {
+        Instruction instruction = new Instruction(Opcodes.LABEL);
+        instruction.setLabel(name);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an unconditional jump instruction to the end of the program.
+     */
+    public ApfGenerator addJump(String target) {
+        Instruction instruction = new Instruction(Opcodes.JMP);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to load the byte at offset {@code offset}
+     * bytes from the begining of the packet into {@code register}.
+     */
+    public ApfGenerator addLoad8(Register register, int offset) {
+        Instruction instruction = new Instruction(Opcodes.LDB, register);
+        instruction.setUnsignedImm(offset);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to load 16-bits at offset {@code offset}
+     * bytes from the begining of the packet into {@code register}.
+     */
+    public ApfGenerator addLoad16(Register register, int offset) {
+        Instruction instruction = new Instruction(Opcodes.LDH, register);
+        instruction.setUnsignedImm(offset);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to load 32-bits at offset {@code offset}
+     * bytes from the begining of the packet into {@code register}.
+     */
+    public ApfGenerator addLoad32(Register register, int offset) {
+        Instruction instruction = new Instruction(Opcodes.LDW, register);
+        instruction.setUnsignedImm(offset);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to load a byte from the packet into
+     * {@code register}. The offset of the loaded byte from the begining of the packet is
+     * the sum of {@code offset} and the value in register R1.
+     */
+    public ApfGenerator addLoad8Indexed(Register register, int offset) {
+        Instruction instruction = new Instruction(Opcodes.LDBX, register);
+        instruction.setUnsignedImm(offset);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to load 16-bits from the packet into
+     * {@code register}. The offset of the loaded 16-bits from the begining of the packet is
+     * the sum of {@code offset} and the value in register R1.
+     */
+    public ApfGenerator addLoad16Indexed(Register register, int offset) {
+        Instruction instruction = new Instruction(Opcodes.LDHX, register);
+        instruction.setUnsignedImm(offset);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to load 32-bits from the packet into
+     * {@code register}. The offset of the loaded 32-bits from the begining of the packet is
+     * the sum of {@code offset} and the value in register R1.
+     */
+    public ApfGenerator addLoad32Indexed(Register register, int offset) {
+        Instruction instruction = new Instruction(Opcodes.LDWX, register);
+        instruction.setUnsignedImm(offset);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to add {@code value} to register R0.
+     */
+    public ApfGenerator addAdd(int value) {
+        Instruction instruction = new Instruction(Opcodes.ADD);
+        instruction.setSignedImm(value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to multiply register R0 by {@code value}.
+     */
+    public ApfGenerator addMul(int value) {
+        Instruction instruction = new Instruction(Opcodes.MUL);
+        instruction.setSignedImm(value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to divide register R0 by {@code value}.
+     */
+    public ApfGenerator addDiv(int value) {
+        Instruction instruction = new Instruction(Opcodes.DIV);
+        instruction.setSignedImm(value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to logically and register R0 with {@code value}.
+     */
+    public ApfGenerator addAnd(int value) {
+        Instruction instruction = new Instruction(Opcodes.AND);
+        instruction.setUnsignedImm(value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to logically or register R0 with {@code value}.
+     */
+    public ApfGenerator addOr(int value) {
+        Instruction instruction = new Instruction(Opcodes.OR);
+        instruction.setUnsignedImm(value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to shift left register R0 by {@code value} bits.
+     */
+    public ApfGenerator addLeftShift(int value) {
+        Instruction instruction = new Instruction(Opcodes.SH);
+        instruction.setSignedImm(value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to shift right register R0 by {@code value}
+     * bits.
+     */
+    public ApfGenerator addRightShift(int value) {
+        Instruction instruction = new Instruction(Opcodes.SH);
+        instruction.setSignedImm(-value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to add register R1 to register R0.
+     */
+    public ApfGenerator addAddR1() {
+        Instruction instruction = new Instruction(Opcodes.ADD, Register.R1);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to multiply register R0 by register R1.
+     */
+    public ApfGenerator addMulR1() {
+        Instruction instruction = new Instruction(Opcodes.MUL, Register.R1);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to divide register R0 by register R1.
+     */
+    public ApfGenerator addDivR1() {
+        Instruction instruction = new Instruction(Opcodes.DIV, Register.R1);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to logically and register R0 with register R1
+     * and store the result back into register R0.
+     */
+    public ApfGenerator addAndR1() {
+        Instruction instruction = new Instruction(Opcodes.AND, Register.R1);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to logically or register R0 with register R1
+     * and store the result back into register R0.
+     */
+    public ApfGenerator addOrR1() {
+        Instruction instruction = new Instruction(Opcodes.OR, Register.R1);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to shift register R0 left by the value in
+     * register R1.
+     */
+    public ApfGenerator addLeftShiftR1() {
+        Instruction instruction = new Instruction(Opcodes.SH, Register.R1);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to move {@code value} into {@code register}.
+     */
+    public ApfGenerator addLoadImmediate(Register register, int value) {
+        Instruction instruction = new Instruction(Opcodes.LI, register);
+        instruction.setSignedImm(value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if register R0's
+     * value equals {@code value}.
+     */
+    public ApfGenerator addJumpIfR0Equals(int value, String target) {
+        Instruction instruction = new Instruction(Opcodes.JEQ);
+        instruction.setUnsignedImm(value);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if register R0's
+     * value does not equal {@code value}.
+     */
+    public ApfGenerator addJumpIfR0NotEquals(int value, String target) {
+        Instruction instruction = new Instruction(Opcodes.JNE);
+        instruction.setUnsignedImm(value);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if register R0's
+     * value is greater than {@code value}.
+     */
+    public ApfGenerator addJumpIfR0GreaterThan(int value, String target) {
+        Instruction instruction = new Instruction(Opcodes.JGT);
+        instruction.setUnsignedImm(value);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if register R0's
+     * value is less than {@code value}.
+     */
+    public ApfGenerator addJumpIfR0LessThan(int value, String target) {
+        Instruction instruction = new Instruction(Opcodes.JLT);
+        instruction.setUnsignedImm(value);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if register R0's
+     * value has any bits set that are also set in {@code value}.
+     */
+    public ApfGenerator addJumpIfR0AnyBitsSet(int value, String target) {
+        Instruction instruction = new Instruction(Opcodes.JSET);
+        instruction.setUnsignedImm(value);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if register R0's
+     * value equals register R1's value.
+     */
+    public ApfGenerator addJumpIfR0EqualsR1(String target) {
+        Instruction instruction = new Instruction(Opcodes.JEQ, Register.R1);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if register R0's
+     * value does not equal register R1's value.
+     */
+    public ApfGenerator addJumpIfR0NotEqualsR1(String target) {
+        Instruction instruction = new Instruction(Opcodes.JNE, Register.R1);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if register R0's
+     * value is greater than register R1's value.
+     */
+    public ApfGenerator addJumpIfR0GreaterThanR1(String target) {
+        Instruction instruction = new Instruction(Opcodes.JGT, Register.R1);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if register R0's
+     * value is less than register R1's value.
+     */
+    public ApfGenerator addJumpIfR0LessThanR1(String target) {
+        Instruction instruction = new Instruction(Opcodes.JLT, Register.R1);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if register R0's
+     * value has any bits set that are also set in R1's value.
+     */
+    public ApfGenerator addJumpIfR0AnyBitsSetR1(String target) {
+        Instruction instruction = new Instruction(Opcodes.JSET, Register.R1);
+        instruction.setTargetLabel(target);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code target} if the bytes of the
+     * packet at an offset specified by {@code register} match {@code bytes}.
+     */
+    public ApfGenerator addJumpIfBytesNotEqual(Register register, byte[] bytes, String target)
+            throws IllegalInstructionException {
+        if (register == Register.R1) {
+            throw new IllegalInstructionException("JNEBS fails with R1");
+        }
+        Instruction instruction = new Instruction(Opcodes.JNEBS, register);
+        instruction.setUnsignedImm(bytes.length);
+        instruction.setTargetLabel(target);
+        instruction.setCompareBytes(bytes);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to load memory slot {@code slot} into
+     * {@code register}.
+     */
+    public ApfGenerator addLoadFromMemory(Register register, int slot)
+            throws IllegalInstructionException {
+        if (slot < 0 || slot > (MEMORY_SLOTS - 1)) {
+            throw new IllegalInstructionException("illegal memory slot number: " + slot);
+        }
+        Instruction instruction = new Instruction(Opcodes.EXT, register);
+        instruction.setUnsignedImm(ExtendedOpcodes.LDM.value + slot);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to store {@code register} into memory slot
+     * {@code slot}.
+     */
+    public ApfGenerator addStoreToMemory(Register register, int slot)
+            throws IllegalInstructionException {
+        if (slot < 0 || slot > (MEMORY_SLOTS - 1)) {
+            throw new IllegalInstructionException("illegal memory slot number: " + slot);
+        }
+        Instruction instruction = new Instruction(Opcodes.EXT, register);
+        instruction.setUnsignedImm(ExtendedOpcodes.STM.value + slot);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to logically not {@code register}.
+     */
+    public ApfGenerator addNot(Register register) {
+        Instruction instruction = new Instruction(Opcodes.EXT, register);
+        instruction.setUnsignedImm(ExtendedOpcodes.NOT.value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to negate {@code register}.
+     */
+    public ApfGenerator addNeg(Register register) {
+        Instruction instruction = new Instruction(Opcodes.EXT, register);
+        instruction.setUnsignedImm(ExtendedOpcodes.NEG.value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to swap the values in register R0 and register R1.
+     */
+    public ApfGenerator addSwap() {
+        Instruction instruction = new Instruction(Opcodes.EXT);
+        instruction.setUnsignedImm(ExtendedOpcodes.SWAP.value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to move the value into
+     * {@code register} from the other register.
+     */
+    public ApfGenerator addMove(Register register) {
+        Instruction instruction = new Instruction(Opcodes.EXT, register);
+        instruction.setUnsignedImm(ExtendedOpcodes.MOVE.value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to load 32 bits from the data memory into
+     * {@code register}. The source address is computed by adding the signed immediate
+     * @{code offset} to the other register.
+     * Requires APF v3 or greater.
+     */
+    public ApfGenerator addLoadData(Register destinationRegister, int offset)
+            throws IllegalInstructionException {
+        requireApfVersion(3);
+        Instruction instruction = new Instruction(Opcodes.LDDW, destinationRegister);
+        instruction.setSignedImm(offset);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to store 32 bits from {@code register} into the
+     * data memory. The destination address is computed by adding the signed immediate
+     * @{code offset} to the other register.
+     * Requires APF v3 or greater.
+     */
+    public ApfGenerator addStoreData(Register sourceRegister, int offset)
+            throws IllegalInstructionException {
+        requireApfVersion(3);
+        Instruction instruction = new Instruction(Opcodes.STDW, sourceRegister);
+        instruction.setSignedImm(offset);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Updates instruction offset fields using latest instruction sizes.
+     * @return current program length in bytes.
+     */
+    private int updateInstructionOffsets() {
+        int offset = 0;
+        for (Instruction instruction : mInstructions) {
+            instruction.offset = offset;
+            offset += instruction.size();
+        }
+        return offset;
+    }
+
+    /**
+     * Returns an overestimate of the size of the generated program. {@link #generate} may return
+     * a program that is smaller.
+     */
+    public int programLengthOverEstimate() {
+        return updateInstructionOffsets();
+    }
+
+    /**
+     * Generate the bytecode for the APF program.
+     * @return the bytecode.
+     * @throws IllegalStateException if a label is referenced but not defined.
+     */
+    public byte[] generate() throws IllegalInstructionException {
+        // Enforce that we can only generate once because we cannot unshrink instructions and
+        // PASS/DROP labels may move further away requiring unshrinking if we add further
+        // instructions.
+        if (mGenerated) {
+            throw new IllegalStateException("Can only generate() once!");
+        }
+        mGenerated = true;
+        int total_size;
+        boolean shrunk;
+        // Shrink the immediate value fields of instructions.
+        // As we shrink the instructions some branch offset
+        // fields may shrink also, thereby shrinking the
+        // instructions further. Loop until we've reached the
+        // minimum size. Rarely will this loop more than a few times.
+        // Limit iterations to avoid O(n^2) behavior.
+        int iterations_remaining = 10;
+        do {
+            total_size = updateInstructionOffsets();
+            // Update drop and pass label offsets.
+            mDropLabel.offset = total_size + 1;
+            mPassLabel.offset = total_size;
+            // Limit run-time in aberant circumstances.
+            if (iterations_remaining-- == 0) break;
+            // Attempt to shrink instructions.
+            shrunk = false;
+            for (Instruction instruction : mInstructions) {
+                if (instruction.shrink()) {
+                    shrunk = true;
+                }
+            }
+        } while (shrunk);
+        // Generate bytecode for instructions.
+        byte[] bytecode = new byte[total_size];
+        for (Instruction instruction : mInstructions) {
+            instruction.generate(bytecode);
+        }
+        return bytecode;
+    }
+}
+
diff --git a/src/android/net/dhcp/DhcpAckPacket.java b/src/android/net/dhcp/DhcpAckPacket.java
new file mode 100644
index 0000000..b2eb4e2
--- /dev/null
+++ b/src/android/net/dhcp/DhcpAckPacket.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2010 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 android.net.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-ACK packet.
+ */
+class DhcpAckPacket extends DhcpPacket {
+
+    /**
+     * The address of the server which sent this packet.
+     */
+    private final Inet4Address mSrcIp;
+
+    DhcpAckPacket(int transId, short secs, boolean broadcast, Inet4Address serverAddress,
+            Inet4Address relayIp, Inet4Address clientIp, Inet4Address yourIp, byte[] clientMac) {
+        super(transId, secs, clientIp, yourIp, serverAddress, relayIp, clientMac, broadcast);
+        mBroadcast = broadcast;
+        mSrcIp = serverAddress;
+    }
+
+    public String toString() {
+        String s = super.toString();
+        String dnsServers = " DNS servers: ";
+
+        for (Inet4Address dnsServer: mDnsServers) {
+            dnsServers += dnsServer.toString() + " ";
+        }
+
+        return s + " ACK: your new IP " + mYourIp +
+                ", netmask " + mSubnetMask +
+                ", gateways " + mGateways + dnsServers +
+                ", lease time " + mLeaseTime;
+    }
+
+    /**
+     * Fills in a packet with the requested ACK parameters.
+     */
+    public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+        ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+        Inet4Address destIp = mBroadcast ? INADDR_BROADCAST : mYourIp;
+        Inet4Address srcIp = mBroadcast ? INADDR_ANY : mSrcIp;
+
+        fillInPacket(encap, destIp, srcIp, destUdp, srcUdp, result,
+            DHCP_BOOTREPLY, mBroadcast);
+        result.flip();
+        return result;
+    }
+
+    /**
+     * Adds the optional parameters to the client-generated ACK packet.
+     */
+    void finishPacket(ByteBuffer buffer) {
+        addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_ACK);
+        addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier);
+
+        addCommonServerTlvs(buffer);
+        addTlvEnd(buffer);
+    }
+
+    /**
+     * Un-boxes an Integer, returning 0 if a null reference is supplied.
+     */
+    private static final int getInt(Integer v) {
+        if (v == null) {
+            return 0;
+        } else {
+            return v.intValue();
+        }
+    }
+}
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
new file mode 100644
index 0000000..04ac9a3
--- /dev/null
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -0,0 +1,1052 @@
+/*
+ * Copyright (C) 2015 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 android.net.dhcp;
+
+import static android.net.dhcp.DhcpPacket.DHCP_BROADCAST_ADDRESS;
+import static android.net.dhcp.DhcpPacket.DHCP_DNS_SERVER;
+import static android.net.dhcp.DhcpPacket.DHCP_DOMAIN_NAME;
+import static android.net.dhcp.DhcpPacket.DHCP_LEASE_TIME;
+import static android.net.dhcp.DhcpPacket.DHCP_MTU;
+import static android.net.dhcp.DhcpPacket.DHCP_REBINDING_TIME;
+import static android.net.dhcp.DhcpPacket.DHCP_RENEWAL_TIME;
+import static android.net.dhcp.DhcpPacket.DHCP_ROUTER;
+import static android.net.dhcp.DhcpPacket.DHCP_SUBNET_MASK;
+import static android.net.dhcp.DhcpPacket.DHCP_VENDOR_INFO;
+import static android.net.dhcp.DhcpPacket.INADDR_ANY;
+import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
+import static android.net.util.SocketUtils.makePacketSocketAddress;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_RAW;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_BROADCAST;
+import static android.system.OsConstants.SO_RCVBUF;
+import static android.system.OsConstants.SO_REUSEADDR;
+
+import android.content.Context;
+import android.net.DhcpResults;
+import android.net.NetworkUtils;
+import android.net.TrafficStats;
+import android.net.ip.IpClient;
+import android.net.metrics.DhcpClientEvent;
+import android.net.metrics.DhcpErrorEvent;
+import android.net.metrics.IpConnectivityLog;
+import android.net.util.InterfaceParams;
+import android.net.util.SocketUtils;
+import android.os.Message;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.util.HexDump;
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.internal.util.WakeupMessage;
+
+import libcore.io.IoBridge;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * A DHCPv4 client.
+ *
+ * Written to behave similarly to the DhcpStateMachine + dhcpcd 5.5.6 combination used in Android
+ * 5.1 and below, as configured on Nexus 6. The interface is the same as DhcpStateMachine.
+ *
+ * TODO:
+ *
+ * - Exponential backoff when receiving NAKs (not specified by the RFC, but current behaviour).
+ * - Support persisting lease state and support INIT-REBOOT. Android 5.1 does this, but it does not
+ *   do so correctly: instead of requesting the lease last obtained on a particular network (e.g., a
+ *   given SSID), it requests the last-leased IP address on the same interface, causing a delay if
+ *   the server NAKs or a timeout if it doesn't.
+ *
+ * Known differences from current behaviour:
+ *
+ * - Does not request the "static routes" option.
+ * - Does not support BOOTP servers. DHCP has been around since 1993, should be everywhere now.
+ * - Requests the "broadcast" option, but does nothing with it.
+ * - Rejects invalid subnet masks such as 255.255.255.1 (current code treats that as 255.255.255.0).
+ *
+ * @hide
+ */
+public class DhcpClient extends StateMachine {
+
+    private static final String TAG = "DhcpClient";
+    private static final boolean DBG = true;
+    private static final boolean STATE_DBG = false;
+    private static final boolean MSG_DBG = false;
+    private static final boolean PACKET_DBG = false;
+
+    // Timers and timeouts.
+    private static final int SECONDS = 1000;
+    private static final int FIRST_TIMEOUT_MS   =   2 * SECONDS;
+    private static final int MAX_TIMEOUT_MS     = 128 * SECONDS;
+
+    // This is not strictly needed, since the client is asynchronous and implements exponential
+    // backoff. It's maintained for backwards compatibility with the previous DHCP code, which was
+    // a blocking operation with a 30-second timeout. We pick 36 seconds so we can send packets at
+    // t=0, t=2, t=6, t=14, t=30, allowing for 10% jitter.
+    private static final int DHCP_TIMEOUT_MS    =  36 * SECONDS;
+
+    // DhcpClient uses IpClient's handler.
+    private static final int PUBLIC_BASE = IpClient.DHCPCLIENT_CMD_BASE;
+
+    /* Commands from controller to start/stop DHCP */
+    public static final int CMD_START_DHCP                  = PUBLIC_BASE + 1;
+    public static final int CMD_STOP_DHCP                   = PUBLIC_BASE + 2;
+
+    /* Notification from DHCP state machine prior to DHCP discovery/renewal */
+    public static final int CMD_PRE_DHCP_ACTION             = PUBLIC_BASE + 3;
+    /* Notification from DHCP state machine post DHCP discovery/renewal. Indicates
+     * success/failure */
+    public static final int CMD_POST_DHCP_ACTION            = PUBLIC_BASE + 4;
+    /* Notification from DHCP state machine before quitting */
+    public static final int CMD_ON_QUIT                     = PUBLIC_BASE + 5;
+
+    /* Command from controller to indicate DHCP discovery/renewal can continue
+     * after pre DHCP action is complete */
+    public static final int CMD_PRE_DHCP_ACTION_COMPLETE    = PUBLIC_BASE + 6;
+
+    /* Command and event notification to/from IpManager requesting the setting
+     * (or clearing) of an IPv4 LinkAddress.
+     */
+    public static final int CMD_CLEAR_LINKADDRESS           = PUBLIC_BASE + 7;
+    public static final int CMD_CONFIGURE_LINKADDRESS       = PUBLIC_BASE + 8;
+    public static final int EVENT_LINKADDRESS_CONFIGURED    = PUBLIC_BASE + 9;
+
+    /* Message.arg1 arguments to CMD_POST_DHCP_ACTION notification */
+    public static final int DHCP_SUCCESS = 1;
+    public static final int DHCP_FAILURE = 2;
+
+    // Internal messages.
+    private static final int PRIVATE_BASE         = IpClient.DHCPCLIENT_CMD_BASE + 100;
+    private static final int CMD_KICK             = PRIVATE_BASE + 1;
+    private static final int CMD_RECEIVED_PACKET  = PRIVATE_BASE + 2;
+    private static final int CMD_TIMEOUT          = PRIVATE_BASE + 3;
+    private static final int CMD_RENEW_DHCP       = PRIVATE_BASE + 4;
+    private static final int CMD_REBIND_DHCP      = PRIVATE_BASE + 5;
+    private static final int CMD_EXPIRE_DHCP      = PRIVATE_BASE + 6;
+
+    // For message logging.
+    private static final Class[] sMessageClasses = { DhcpClient.class };
+    private static final SparseArray<String> sMessageNames =
+            MessageUtils.findMessageNames(sMessageClasses);
+
+    // DHCP parameters that we request.
+    /* package */ static final byte[] REQUESTED_PARAMS = new byte[] {
+        DHCP_SUBNET_MASK,
+        DHCP_ROUTER,
+        DHCP_DNS_SERVER,
+        DHCP_DOMAIN_NAME,
+        DHCP_MTU,
+        DHCP_BROADCAST_ADDRESS,  // TODO: currently ignored.
+        DHCP_LEASE_TIME,
+        DHCP_RENEWAL_TIME,
+        DHCP_REBINDING_TIME,
+        DHCP_VENDOR_INFO,
+    };
+
+    // DHCP flag that means "yes, we support unicast."
+    private static final boolean DO_UNICAST   = false;
+
+    // System services / libraries we use.
+    private final Context mContext;
+    private final Random mRandom;
+    private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
+
+    // Sockets.
+    // - We use a packet socket to receive, because servers send us packets bound for IP addresses
+    //   which we have not yet configured, and the kernel protocol stack drops these.
+    // - We use a UDP socket to send, so the kernel handles ARP and routing for us (DHCP servers can
+    //   be off-link as well as on-link).
+    private FileDescriptor mPacketSock;
+    private FileDescriptor mUdpSock;
+    private ReceiveThread mReceiveThread;
+
+    // State variables.
+    private final StateMachine mController;
+    private final WakeupMessage mKickAlarm;
+    private final WakeupMessage mTimeoutAlarm;
+    private final WakeupMessage mRenewAlarm;
+    private final WakeupMessage mRebindAlarm;
+    private final WakeupMessage mExpiryAlarm;
+    private final String mIfaceName;
+
+    private boolean mRegisteredForPreDhcpNotification;
+    private InterfaceParams mIface;
+    // TODO: MacAddress-ify more of this class hierarchy.
+    private byte[] mHwAddr;
+    private SocketAddress mInterfaceBroadcastAddr;
+    private int mTransactionId;
+    private long mTransactionStartMillis;
+    private DhcpResults mDhcpLease;
+    private long mDhcpLeaseExpiry;
+    private DhcpResults mOffer;
+
+    // Milliseconds SystemClock timestamps used to record transition times to DhcpBoundState.
+    private long mLastInitEnterTime;
+    private long mLastBoundExitTime;
+
+    // States.
+    private State mStoppedState = new StoppedState();
+    private State mDhcpState = new DhcpState();
+    private State mDhcpInitState = new DhcpInitState();
+    private State mDhcpSelectingState = new DhcpSelectingState();
+    private State mDhcpRequestingState = new DhcpRequestingState();
+    private State mDhcpHaveLeaseState = new DhcpHaveLeaseState();
+    private State mConfiguringInterfaceState = new ConfiguringInterfaceState();
+    private State mDhcpBoundState = new DhcpBoundState();
+    private State mDhcpRenewingState = new DhcpRenewingState();
+    private State mDhcpRebindingState = new DhcpRebindingState();
+    private State mDhcpInitRebootState = new DhcpInitRebootState();
+    private State mDhcpRebootingState = new DhcpRebootingState();
+    private State mWaitBeforeStartState = new WaitBeforeStartState(mDhcpInitState);
+    private State mWaitBeforeRenewalState = new WaitBeforeRenewalState(mDhcpRenewingState);
+
+    private WakeupMessage makeWakeupMessage(String cmdName, int cmd) {
+        cmdName = DhcpClient.class.getSimpleName() + "." + mIfaceName + "." + cmdName;
+        return new WakeupMessage(mContext, getHandler(), cmdName, cmd);
+    }
+
+    // TODO: Take an InterfaceParams instance instead of an interface name String.
+    private DhcpClient(Context context, StateMachine controller, String iface) {
+        super(TAG, controller.getHandler());
+
+        mContext = context;
+        mController = controller;
+        mIfaceName = iface;
+
+        addState(mStoppedState);
+        addState(mDhcpState);
+            addState(mDhcpInitState, mDhcpState);
+            addState(mWaitBeforeStartState, mDhcpState);
+            addState(mDhcpSelectingState, mDhcpState);
+            addState(mDhcpRequestingState, mDhcpState);
+            addState(mDhcpHaveLeaseState, mDhcpState);
+                addState(mConfiguringInterfaceState, mDhcpHaveLeaseState);
+                addState(mDhcpBoundState, mDhcpHaveLeaseState);
+                addState(mWaitBeforeRenewalState, mDhcpHaveLeaseState);
+                addState(mDhcpRenewingState, mDhcpHaveLeaseState);
+                addState(mDhcpRebindingState, mDhcpHaveLeaseState);
+            addState(mDhcpInitRebootState, mDhcpState);
+            addState(mDhcpRebootingState, mDhcpState);
+
+        setInitialState(mStoppedState);
+
+        mRandom = new Random();
+
+        // Used to schedule packet retransmissions.
+        mKickAlarm = makeWakeupMessage("KICK", CMD_KICK);
+        // Used to time out PacketRetransmittingStates.
+        mTimeoutAlarm = makeWakeupMessage("TIMEOUT", CMD_TIMEOUT);
+        // Used to schedule DHCP reacquisition.
+        mRenewAlarm = makeWakeupMessage("RENEW", CMD_RENEW_DHCP);
+        mRebindAlarm = makeWakeupMessage("REBIND", CMD_REBIND_DHCP);
+        mExpiryAlarm = makeWakeupMessage("EXPIRY", CMD_EXPIRE_DHCP);
+    }
+
+    public void registerForPreDhcpNotification() {
+        mRegisteredForPreDhcpNotification = true;
+    }
+
+    public static DhcpClient makeDhcpClient(
+            Context context, StateMachine controller, InterfaceParams ifParams) {
+        DhcpClient client = new DhcpClient(context, controller, ifParams.name);
+        client.mIface = ifParams;
+        client.start();
+        return client;
+    }
+
+    private boolean initInterface() {
+        if (mIface == null) mIface = InterfaceParams.getByName(mIfaceName);
+        if (mIface == null) {
+            Log.e(TAG, "Can't determine InterfaceParams for " + mIfaceName);
+            return false;
+        }
+
+        mHwAddr = mIface.macAddr.toByteArray();
+        mInterfaceBroadcastAddr = makePacketSocketAddress(mIface.index, DhcpPacket.ETHER_BROADCAST);
+        return true;
+    }
+
+    private void startNewTransaction() {
+        mTransactionId = mRandom.nextInt();
+        mTransactionStartMillis = SystemClock.elapsedRealtime();
+    }
+
+    private boolean initSockets() {
+        return initPacketSocket() && initUdpSocket();
+    }
+
+    private boolean initPacketSocket() {
+        try {
+            mPacketSock = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IP);
+            SocketAddress addr = makePacketSocketAddress((short) ETH_P_IP, mIface.index);
+            Os.bind(mPacketSock, addr);
+            NetworkUtils.attachDhcpFilter(mPacketSock);
+        } catch(SocketException|ErrnoException e) {
+            Log.e(TAG, "Error creating packet socket", e);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean initUdpSocket() {
+        final int oldTag = TrafficStats.getAndSetThreadStatsTag(TrafficStats.TAG_SYSTEM_DHCP);
+        try {
+            mUdpSock = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+            SocketUtils.bindSocketToInterface(mUdpSock, mIfaceName);
+            Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_REUSEADDR, 1);
+            Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_BROADCAST, 1);
+            Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_RCVBUF, 0);
+            Os.bind(mUdpSock, Inet4Address.ANY, DhcpPacket.DHCP_CLIENT);
+        } catch(SocketException|ErrnoException e) {
+            Log.e(TAG, "Error creating UDP socket", e);
+            return false;
+        } finally {
+            TrafficStats.setThreadStatsTag(oldTag);
+        }
+        return true;
+    }
+
+    private boolean connectUdpSock(Inet4Address to) {
+        try {
+            Os.connect(mUdpSock, to, DhcpPacket.DHCP_SERVER);
+            return true;
+        } catch (SocketException|ErrnoException e) {
+            Log.e(TAG, "Error connecting UDP socket", e);
+            return false;
+        }
+    }
+
+    private static void closeQuietly(FileDescriptor fd) {
+        try {
+            IoBridge.closeAndSignalBlockedThreads(fd);
+        } catch (IOException ignored) {}
+    }
+
+    private void closeSockets() {
+        closeQuietly(mUdpSock);
+        closeQuietly(mPacketSock);
+    }
+
+    class ReceiveThread extends Thread {
+
+        private final byte[] mPacket = new byte[DhcpPacket.MAX_LENGTH];
+        private volatile boolean mStopped = false;
+
+        public void halt() {
+            mStopped = true;
+            closeSockets();  // Interrupts the read() call the thread is blocked in.
+        }
+
+        @Override
+        public void run() {
+            if (DBG) Log.d(TAG, "Receive thread started");
+            while (!mStopped) {
+                int length = 0;  // Or compiler can't tell it's initialized if a parse error occurs.
+                try {
+                    length = Os.read(mPacketSock, mPacket, 0, mPacket.length);
+                    DhcpPacket packet = null;
+                    packet = DhcpPacket.decodeFullPacket(mPacket, length, DhcpPacket.ENCAP_L2);
+                    if (DBG) Log.d(TAG, "Received packet: " + packet);
+                    sendMessage(CMD_RECEIVED_PACKET, packet);
+                } catch (IOException|ErrnoException e) {
+                    if (!mStopped) {
+                        Log.e(TAG, "Read error", e);
+                        logError(DhcpErrorEvent.RECEIVE_ERROR);
+                    }
+                } catch (DhcpPacket.ParseException e) {
+                    Log.e(TAG, "Can't parse packet: " + e.getMessage());
+                    if (PACKET_DBG) {
+                        Log.d(TAG, HexDump.dumpHexString(mPacket, 0, length));
+                    }
+                    if (e.errorCode == DhcpErrorEvent.DHCP_NO_COOKIE) {
+                        int snetTagId = 0x534e4554;
+                        String bugId = "31850211";
+                        int uid = -1;
+                        String data = DhcpPacket.ParseException.class.getName();
+                        EventLog.writeEvent(snetTagId, bugId, uid, data);
+                    }
+                    logError(e.errorCode);
+                }
+            }
+            if (DBG) Log.d(TAG, "Receive thread stopped");
+        }
+    }
+
+    private short getSecs() {
+        return (short) ((SystemClock.elapsedRealtime() - mTransactionStartMillis) / 1000);
+    }
+
+    private boolean transmitPacket(ByteBuffer buf, String description, int encap, Inet4Address to) {
+        try {
+            if (encap == DhcpPacket.ENCAP_L2) {
+                if (DBG) Log.d(TAG, "Broadcasting " + description);
+                Os.sendto(mPacketSock, buf.array(), 0, buf.limit(), 0, mInterfaceBroadcastAddr);
+            } else if (encap == DhcpPacket.ENCAP_BOOTP && to.equals(INADDR_BROADCAST)) {
+                if (DBG) Log.d(TAG, "Broadcasting " + description);
+                // We only send L3-encapped broadcasts in DhcpRebindingState,
+                // where we have an IP address and an unconnected UDP socket.
+                //
+                // N.B.: We only need this codepath because DhcpRequestPacket
+                // hardcodes the source IP address to 0.0.0.0. We could reuse
+                // the packet socket if this ever changes.
+                Os.sendto(mUdpSock, buf, 0, to, DhcpPacket.DHCP_SERVER);
+            } else {
+                // It's safe to call getpeername here, because we only send unicast packets if we
+                // have an IP address, and we connect the UDP socket in DhcpBoundState#enter.
+                if (DBG) Log.d(TAG, String.format("Unicasting %s to %s",
+                        description, Os.getpeername(mUdpSock)));
+                Os.write(mUdpSock, buf);
+            }
+        } catch(ErrnoException|IOException e) {
+            Log.e(TAG, "Can't send packet: ", e);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean sendDiscoverPacket() {
+        ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
+                DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
+                DO_UNICAST, REQUESTED_PARAMS);
+        return transmitPacket(packet, "DHCPDISCOVER", DhcpPacket.ENCAP_L2, INADDR_BROADCAST);
+    }
+
+    private boolean sendRequestPacket(
+            Inet4Address clientAddress, Inet4Address requestedAddress,
+            Inet4Address serverAddress, Inet4Address to) {
+        // TODO: should we use the transaction ID from the server?
+        final int encap = INADDR_ANY.equals(clientAddress)
+                ? DhcpPacket.ENCAP_L2 : DhcpPacket.ENCAP_BOOTP;
+
+        ByteBuffer packet = DhcpPacket.buildRequestPacket(
+                encap, mTransactionId, getSecs(), clientAddress,
+                DO_UNICAST, mHwAddr, requestedAddress,
+                serverAddress, REQUESTED_PARAMS, null);
+        String serverStr = (serverAddress != null) ? serverAddress.getHostAddress() : null;
+        String description = "DHCPREQUEST ciaddr=" + clientAddress.getHostAddress() +
+                             " request=" + requestedAddress.getHostAddress() +
+                             " serverid=" + serverStr;
+        return transmitPacket(packet, description, encap, to);
+    }
+
+    private void scheduleLeaseTimers() {
+        if (mDhcpLeaseExpiry == 0) {
+            Log.d(TAG, "Infinite lease, no timer scheduling needed");
+            return;
+        }
+
+        final long now = SystemClock.elapsedRealtime();
+
+        // TODO: consider getting the renew and rebind timers from T1 and T2.
+        // See also:
+        //     https://tools.ietf.org/html/rfc2131#section-4.4.5
+        //     https://tools.ietf.org/html/rfc1533#section-9.9
+        //     https://tools.ietf.org/html/rfc1533#section-9.10
+        final long remainingDelay = mDhcpLeaseExpiry - now;
+        final long renewDelay = remainingDelay / 2;
+        final long rebindDelay = remainingDelay * 7 / 8;
+        mRenewAlarm.schedule(now + renewDelay);
+        mRebindAlarm.schedule(now + rebindDelay);
+        mExpiryAlarm.schedule(now + remainingDelay);
+        Log.d(TAG, "Scheduling renewal in " + (renewDelay / 1000) + "s");
+        Log.d(TAG, "Scheduling rebind in " + (rebindDelay / 1000) + "s");
+        Log.d(TAG, "Scheduling expiry in " + (remainingDelay / 1000) + "s");
+    }
+
+    private void notifySuccess() {
+        mController.sendMessage(
+                CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, new DhcpResults(mDhcpLease));
+    }
+
+    private void notifyFailure() {
+        mController.sendMessage(CMD_POST_DHCP_ACTION, DHCP_FAILURE, 0, null);
+    }
+
+    private void acceptDhcpResults(DhcpResults results, String msg) {
+        mDhcpLease = results;
+        mOffer = null;
+        Log.d(TAG, msg + " lease: " + mDhcpLease);
+        notifySuccess();
+    }
+
+    private void clearDhcpState() {
+        mDhcpLease = null;
+        mDhcpLeaseExpiry = 0;
+        mOffer = null;
+    }
+
+    /**
+     * Quit the DhcpStateMachine.
+     *
+     * @hide
+     */
+    public void doQuit() {
+        Log.d(TAG, "doQuit");
+        quit();
+    }
+
+    @Override
+    protected void onQuitting() {
+        Log.d(TAG, "onQuitting");
+        mController.sendMessage(CMD_ON_QUIT);
+    }
+
+    abstract class LoggingState extends State {
+        private long mEnterTimeMs;
+
+        @Override
+        public void enter() {
+            if (STATE_DBG) Log.d(TAG, "Entering state " + getName());
+            mEnterTimeMs = SystemClock.elapsedRealtime();
+        }
+
+        @Override
+        public void exit() {
+            long durationMs = SystemClock.elapsedRealtime() - mEnterTimeMs;
+            logState(getName(), (int) durationMs);
+        }
+
+        private String messageName(int what) {
+            return sMessageNames.get(what, Integer.toString(what));
+        }
+
+        private String messageToString(Message message) {
+            long now = SystemClock.uptimeMillis();
+            return new StringBuilder(" ")
+                    .append(message.getWhen() - now)
+                    .append(messageName(message.what))
+                    .append(" ").append(message.arg1)
+                    .append(" ").append(message.arg2)
+                    .append(" ").append(message.obj)
+                    .toString();
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            if (MSG_DBG) {
+                Log.d(TAG, getName() + messageToString(message));
+            }
+            return NOT_HANDLED;
+        }
+
+        @Override
+        public String getName() {
+            // All DhcpClient's states are inner classes with a well defined name.
+            // Use getSimpleName() and avoid super's getName() creating new String instances.
+            return getClass().getSimpleName();
+        }
+    }
+
+    // Sends CMD_PRE_DHCP_ACTION to the controller, waits for the controller to respond with
+    // CMD_PRE_DHCP_ACTION_COMPLETE, and then transitions to mOtherState.
+    abstract class WaitBeforeOtherState extends LoggingState {
+        protected State mOtherState;
+
+        @Override
+        public void enter() {
+            super.enter();
+            mController.sendMessage(CMD_PRE_DHCP_ACTION);
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            super.processMessage(message);
+            switch (message.what) {
+                case CMD_PRE_DHCP_ACTION_COMPLETE:
+                    transitionTo(mOtherState);
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    class StoppedState extends State {
+        @Override
+        public boolean processMessage(Message message) {
+            switch (message.what) {
+                case CMD_START_DHCP:
+                    if (mRegisteredForPreDhcpNotification) {
+                        transitionTo(mWaitBeforeStartState);
+                    } else {
+                        transitionTo(mDhcpInitState);
+                    }
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    class WaitBeforeStartState extends WaitBeforeOtherState {
+        public WaitBeforeStartState(State otherState) {
+            super();
+            mOtherState = otherState;
+        }
+    }
+
+    class WaitBeforeRenewalState extends WaitBeforeOtherState {
+        public WaitBeforeRenewalState(State otherState) {
+            super();
+            mOtherState = otherState;
+        }
+    }
+
+    class DhcpState extends State {
+        @Override
+        public void enter() {
+            clearDhcpState();
+            if (initInterface() && initSockets()) {
+                mReceiveThread = new ReceiveThread();
+                mReceiveThread.start();
+            } else {
+                notifyFailure();
+                transitionTo(mStoppedState);
+            }
+        }
+
+        @Override
+        public void exit() {
+            if (mReceiveThread != null) {
+                mReceiveThread.halt();  // Also closes sockets.
+                mReceiveThread = null;
+            }
+            clearDhcpState();
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            super.processMessage(message);
+            switch (message.what) {
+                case CMD_STOP_DHCP:
+                    transitionTo(mStoppedState);
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    public boolean isValidPacket(DhcpPacket packet) {
+        // TODO: check checksum.
+        int xid = packet.getTransactionId();
+        if (xid != mTransactionId) {
+            Log.d(TAG, "Unexpected transaction ID " + xid + ", expected " + mTransactionId);
+            return false;
+        }
+        if (!Arrays.equals(packet.getClientMac(), mHwAddr)) {
+            Log.d(TAG, "MAC addr mismatch: got " +
+                    HexDump.toHexString(packet.getClientMac()) + ", expected " +
+                    HexDump.toHexString(packet.getClientMac()));
+            return false;
+        }
+        return true;
+    }
+
+    public void setDhcpLeaseExpiry(DhcpPacket packet) {
+        long leaseTimeMillis = packet.getLeaseTimeMillis();
+        mDhcpLeaseExpiry =
+                (leaseTimeMillis > 0) ? SystemClock.elapsedRealtime() + leaseTimeMillis : 0;
+    }
+
+    /**
+     * Retransmits packets using jittered exponential backoff with an optional timeout. Packet
+     * transmission is triggered by CMD_KICK, which is sent by an AlarmManager alarm. If a subclass
+     * sets mTimeout to a positive value, then timeout() is called by an AlarmManager alarm mTimeout
+     * milliseconds after entering the state. Kicks and timeouts are cancelled when leaving the
+     * state.
+     *
+     * Concrete subclasses must implement sendPacket, which is called when the alarm fires and a
+     * packet needs to be transmitted, and receivePacket, which is triggered by CMD_RECEIVED_PACKET
+     * sent by the receive thread. They may also set mTimeout and implement timeout.
+     */
+    abstract class PacketRetransmittingState extends LoggingState {
+
+        private int mTimer;
+        protected int mTimeout = 0;
+
+        @Override
+        public void enter() {
+            super.enter();
+            initTimer();
+            maybeInitTimeout();
+            sendMessage(CMD_KICK);
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            super.processMessage(message);
+            switch (message.what) {
+                case CMD_KICK:
+                    sendPacket();
+                    scheduleKick();
+                    return HANDLED;
+                case CMD_RECEIVED_PACKET:
+                    receivePacket((DhcpPacket) message.obj);
+                    return HANDLED;
+                case CMD_TIMEOUT:
+                    timeout();
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+
+        @Override
+        public void exit() {
+            super.exit();
+            mKickAlarm.cancel();
+            mTimeoutAlarm.cancel();
+        }
+
+        abstract protected boolean sendPacket();
+        abstract protected void receivePacket(DhcpPacket packet);
+        protected void timeout() {}
+
+        protected void initTimer() {
+            mTimer = FIRST_TIMEOUT_MS;
+        }
+
+        protected int jitterTimer(int baseTimer) {
+            int maxJitter = baseTimer / 10;
+            int jitter = mRandom.nextInt(2 * maxJitter) - maxJitter;
+            return baseTimer + jitter;
+        }
+
+        protected void scheduleKick() {
+            long now = SystemClock.elapsedRealtime();
+            long timeout = jitterTimer(mTimer);
+            long alarmTime = now + timeout;
+            mKickAlarm.schedule(alarmTime);
+            mTimer *= 2;
+            if (mTimer > MAX_TIMEOUT_MS) {
+                mTimer = MAX_TIMEOUT_MS;
+            }
+        }
+
+        protected void maybeInitTimeout() {
+            if (mTimeout > 0) {
+                long alarmTime = SystemClock.elapsedRealtime() + mTimeout;
+                mTimeoutAlarm.schedule(alarmTime);
+            }
+        }
+    }
+
+    class DhcpInitState extends PacketRetransmittingState {
+        public DhcpInitState() {
+            super();
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+            startNewTransaction();
+            mLastInitEnterTime = SystemClock.elapsedRealtime();
+        }
+
+        protected boolean sendPacket() {
+            return sendDiscoverPacket();
+        }
+
+        protected void receivePacket(DhcpPacket packet) {
+            if (!isValidPacket(packet)) return;
+            if (!(packet instanceof DhcpOfferPacket)) return;
+            mOffer = packet.toDhcpResults();
+            if (mOffer != null) {
+                Log.d(TAG, "Got pending lease: " + mOffer);
+                transitionTo(mDhcpRequestingState);
+            }
+        }
+    }
+
+    // Not implemented. We request the first offer we receive.
+    class DhcpSelectingState extends LoggingState {
+    }
+
+    class DhcpRequestingState extends PacketRetransmittingState {
+        public DhcpRequestingState() {
+            mTimeout = DHCP_TIMEOUT_MS / 2;
+        }
+
+        protected boolean sendPacket() {
+            return sendRequestPacket(
+                    INADDR_ANY,                                    // ciaddr
+                    (Inet4Address) mOffer.ipAddress.getAddress(),  // DHCP_REQUESTED_IP
+                    (Inet4Address) mOffer.serverAddress,           // DHCP_SERVER_IDENTIFIER
+                    INADDR_BROADCAST);                             // packet destination address
+        }
+
+        protected void receivePacket(DhcpPacket packet) {
+            if (!isValidPacket(packet)) return;
+            if ((packet instanceof DhcpAckPacket)) {
+                DhcpResults results = packet.toDhcpResults();
+                if (results != null) {
+                    setDhcpLeaseExpiry(packet);
+                    acceptDhcpResults(results, "Confirmed");
+                    transitionTo(mConfiguringInterfaceState);
+                }
+            } else if (packet instanceof DhcpNakPacket) {
+                // TODO: Wait a while before returning into INIT state.
+                Log.d(TAG, "Received NAK, returning to INIT");
+                mOffer = null;
+                transitionTo(mDhcpInitState);
+            }
+        }
+
+        @Override
+        protected void timeout() {
+            // After sending REQUESTs unsuccessfully for a while, go back to init.
+            transitionTo(mDhcpInitState);
+        }
+    }
+
+    class DhcpHaveLeaseState extends State {
+        @Override
+        public boolean processMessage(Message message) {
+            switch (message.what) {
+                case CMD_EXPIRE_DHCP:
+                    Log.d(TAG, "Lease expired!");
+                    notifyFailure();
+                    transitionTo(mDhcpInitState);
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+
+        @Override
+        public void exit() {
+            // Clear any extant alarms.
+            mRenewAlarm.cancel();
+            mRebindAlarm.cancel();
+            mExpiryAlarm.cancel();
+            clearDhcpState();
+            // Tell IpManager to clear the IPv4 address. There is no need to
+            // wait for confirmation since any subsequent packets are sent from
+            // INADDR_ANY anyway (DISCOVER, REQUEST).
+            mController.sendMessage(CMD_CLEAR_LINKADDRESS);
+        }
+    }
+
+    class ConfiguringInterfaceState extends LoggingState {
+        @Override
+        public void enter() {
+            super.enter();
+            mController.sendMessage(CMD_CONFIGURE_LINKADDRESS, mDhcpLease.ipAddress);
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            super.processMessage(message);
+            switch (message.what) {
+                case EVENT_LINKADDRESS_CONFIGURED:
+                    transitionTo(mDhcpBoundState);
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    class DhcpBoundState extends LoggingState {
+        @Override
+        public void enter() {
+            super.enter();
+            if (mDhcpLease.serverAddress != null && !connectUdpSock(mDhcpLease.serverAddress)) {
+                // There's likely no point in going into DhcpInitState here, we'll probably
+                // just repeat the transaction, get the same IP address as before, and fail.
+                //
+                // NOTE: It is observed that connectUdpSock() basically never fails, due to
+                // SO_BINDTODEVICE. Examining the local socket address shows it will happily
+                // return an IPv4 address from another interface, or even return "0.0.0.0".
+                //
+                // TODO: Consider deleting this check, following testing on several kernels.
+                notifyFailure();
+                transitionTo(mStoppedState);
+            }
+
+            scheduleLeaseTimers();
+            logTimeToBoundState();
+        }
+
+        @Override
+        public void exit() {
+            super.exit();
+            mLastBoundExitTime = SystemClock.elapsedRealtime();
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            super.processMessage(message);
+            switch (message.what) {
+                case CMD_RENEW_DHCP:
+                    if (mRegisteredForPreDhcpNotification) {
+                        transitionTo(mWaitBeforeRenewalState);
+                    } else {
+                        transitionTo(mDhcpRenewingState);
+                    }
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+
+        private void logTimeToBoundState() {
+            long now = SystemClock.elapsedRealtime();
+            if (mLastBoundExitTime > mLastInitEnterTime) {
+                logState(DhcpClientEvent.RENEWING_BOUND, (int)(now - mLastBoundExitTime));
+            } else {
+                logState(DhcpClientEvent.INITIAL_BOUND, (int)(now - mLastInitEnterTime));
+            }
+        }
+    }
+
+    abstract class DhcpReacquiringState extends PacketRetransmittingState {
+        protected String mLeaseMsg;
+
+        @Override
+        public void enter() {
+            super.enter();
+            startNewTransaction();
+        }
+
+        abstract protected Inet4Address packetDestination();
+
+        protected boolean sendPacket() {
+            return sendRequestPacket(
+                    (Inet4Address) mDhcpLease.ipAddress.getAddress(),  // ciaddr
+                    INADDR_ANY,                                        // DHCP_REQUESTED_IP
+                    null,                                              // DHCP_SERVER_IDENTIFIER
+                    packetDestination());                              // packet destination address
+        }
+
+        protected void receivePacket(DhcpPacket packet) {
+            if (!isValidPacket(packet)) return;
+            if ((packet instanceof DhcpAckPacket)) {
+                final DhcpResults results = packet.toDhcpResults();
+                if (results != null) {
+                    if (!mDhcpLease.ipAddress.equals(results.ipAddress)) {
+                        Log.d(TAG, "Renewed lease not for our current IP address!");
+                        notifyFailure();
+                        transitionTo(mDhcpInitState);
+                    }
+                    setDhcpLeaseExpiry(packet);
+                    // Updating our notion of DhcpResults here only causes the
+                    // DNS servers and routes to be updated in LinkProperties
+                    // in IpManager and by any overridden relevant handlers of
+                    // the registered IpManager.Callback.  IP address changes
+                    // are not supported here.
+                    acceptDhcpResults(results, mLeaseMsg);
+                    transitionTo(mDhcpBoundState);
+                }
+            } else if (packet instanceof DhcpNakPacket) {
+                Log.d(TAG, "Received NAK, returning to INIT");
+                notifyFailure();
+                transitionTo(mDhcpInitState);
+            }
+        }
+    }
+
+    class DhcpRenewingState extends DhcpReacquiringState {
+        public DhcpRenewingState() {
+            mLeaseMsg = "Renewed";
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            if (super.processMessage(message) == HANDLED) {
+                return HANDLED;
+            }
+
+            switch (message.what) {
+                case CMD_REBIND_DHCP:
+                    transitionTo(mDhcpRebindingState);
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+
+        @Override
+        protected Inet4Address packetDestination() {
+            // Not specifying a SERVER_IDENTIFIER option is a violation of RFC 2131, but...
+            // http://b/25343517 . Try to make things work anyway by using broadcast renews.
+            return (mDhcpLease.serverAddress != null) ?
+                    mDhcpLease.serverAddress : INADDR_BROADCAST;
+        }
+    }
+
+    class DhcpRebindingState extends DhcpReacquiringState {
+        public DhcpRebindingState() {
+            mLeaseMsg = "Rebound";
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+
+            // We need to broadcast and possibly reconnect the socket to a
+            // completely different server.
+            closeQuietly(mUdpSock);
+            if (!initUdpSocket()) {
+                Log.e(TAG, "Failed to recreate UDP socket");
+                transitionTo(mDhcpInitState);
+            }
+        }
+
+        @Override
+        protected Inet4Address packetDestination() {
+            return INADDR_BROADCAST;
+        }
+    }
+
+    class DhcpInitRebootState extends LoggingState {
+    }
+
+    class DhcpRebootingState extends LoggingState {
+    }
+
+    private void logError(int errorCode) {
+        mMetricsLog.log(mIfaceName, new DhcpErrorEvent(errorCode));
+    }
+
+    private void logState(String name, int durationMs) {
+        final DhcpClientEvent event = new DhcpClientEvent.Builder()
+                .setMsg(name)
+                .setDurationMs(durationMs)
+                .build();
+        mMetricsLog.log(mIfaceName, event);
+    }
+}
diff --git a/src/android/net/dhcp/DhcpDeclinePacket.java b/src/android/net/dhcp/DhcpDeclinePacket.java
new file mode 100644
index 0000000..7ecdea7
--- /dev/null
+++ b/src/android/net/dhcp/DhcpDeclinePacket.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 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 android.net.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-DECLINE packet.
+ */
+class DhcpDeclinePacket extends DhcpPacket {
+    /**
+     * Generates a DECLINE packet with the specified parameters.
+     */
+    DhcpDeclinePacket(int transId, short secs, Inet4Address clientIp, Inet4Address yourIp,
+                      Inet4Address nextIp, Inet4Address relayIp,
+                      byte[] clientMac) {
+        super(transId, secs, clientIp, yourIp, nextIp, relayIp, clientMac, false);
+    }
+
+    public String toString() {
+        String s = super.toString();
+        return s + " DECLINE";
+    }
+
+    /**
+     * Fills in a packet with the requested DECLINE attributes.
+     */
+    public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+        ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+
+        fillInPacket(encap, mClientIp, mYourIp, destUdp, srcUdp, result,
+            DHCP_BOOTREQUEST, false);
+        result.flip();
+        return result;
+    }
+
+    /**
+     * Adds optional parameters to the DECLINE packet.
+     */
+    void finishPacket(ByteBuffer buffer) {
+        addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_DECLINE);
+        addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId());
+        // RFC 2131 says we MUST NOT include our common client TLVs or the parameter request list.
+        addTlvEnd(buffer);
+    }
+}
diff --git a/src/android/net/dhcp/DhcpDiscoverPacket.java b/src/android/net/dhcp/DhcpDiscoverPacket.java
new file mode 100644
index 0000000..11f2b61
--- /dev/null
+++ b/src/android/net/dhcp/DhcpDiscoverPacket.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010 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 android.net.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-DISCOVER packet.
+ */
+class DhcpDiscoverPacket extends DhcpPacket {
+    /**
+     * The IP address of the client which sent this packet.
+     */
+    final Inet4Address mSrcIp;
+
+    /**
+     * Generates a DISCOVER packet with the specified parameters.
+     */
+    DhcpDiscoverPacket(int transId, short secs, Inet4Address relayIp, byte[] clientMac,
+            boolean broadcast, Inet4Address srcIp) {
+        super(transId, secs, INADDR_ANY, INADDR_ANY, INADDR_ANY, relayIp, clientMac, broadcast);
+        mSrcIp = srcIp;
+    }
+
+    public String toString() {
+        String s = super.toString();
+        return s + " DISCOVER " +
+                (mBroadcast ? "broadcast " : "unicast ");
+    }
+
+    /**
+     * Fills in a packet with the requested DISCOVER parameters.
+     */
+    public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+        ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+        fillInPacket(encap, INADDR_BROADCAST, mSrcIp, destUdp, srcUdp, result, DHCP_BOOTREQUEST,
+                mBroadcast);
+        result.flip();
+        return result;
+    }
+
+    /**
+     * Adds optional parameters to a DISCOVER packet.
+     */
+    void finishPacket(ByteBuffer buffer) {
+        addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_DISCOVER);
+        addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId());
+        addCommonClientTlvs(buffer);
+        addTlv(buffer, DHCP_PARAMETER_LIST, mRequestedParams);
+        addTlvEnd(buffer);
+    }
+}
diff --git a/src/android/net/dhcp/DhcpInformPacket.java b/src/android/net/dhcp/DhcpInformPacket.java
new file mode 100644
index 0000000..7a83466
--- /dev/null
+++ b/src/android/net/dhcp/DhcpInformPacket.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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 android.net.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the (unused) DHCP-INFORM packet.
+ */
+class DhcpInformPacket extends DhcpPacket {
+    /**
+     * Generates an INFORM packet with the specified parameters.
+     */
+    DhcpInformPacket(int transId, short secs, Inet4Address clientIp, Inet4Address yourIp,
+                     Inet4Address nextIp, Inet4Address relayIp,
+                     byte[] clientMac) {
+        super(transId, secs, clientIp, yourIp, nextIp, relayIp, clientMac, false);
+    }
+
+    public String toString() {
+        String s = super.toString();
+        return s + " INFORM";
+    }
+
+    /**
+     * Builds an INFORM packet.
+     */
+    public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+        ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+
+        fillInPacket(encap, mClientIp, mYourIp, destUdp, srcUdp, result,
+            DHCP_BOOTREQUEST, false);
+        result.flip();
+        return result;
+    }
+
+    /**
+     * Adds additional parameters to the INFORM packet.
+     */
+    void finishPacket(ByteBuffer buffer) {
+        addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_INFORM);
+        addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId());
+        addCommonClientTlvs(buffer);
+        addTlv(buffer, DHCP_PARAMETER_LIST, mRequestedParams);
+        addTlvEnd(buffer);
+    }
+}
diff --git a/src/android/net/dhcp/DhcpNakPacket.java b/src/android/net/dhcp/DhcpNakPacket.java
new file mode 100644
index 0000000..1da0b73
--- /dev/null
+++ b/src/android/net/dhcp/DhcpNakPacket.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010 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 android.net.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-NAK packet.
+ */
+class DhcpNakPacket extends DhcpPacket {
+    /**
+     * Generates a NAK packet with the specified parameters.
+     */
+    DhcpNakPacket(int transId, short secs, Inet4Address relayIp, byte[] clientMac,
+            boolean broadcast) {
+        super(transId, secs, INADDR_ANY /* clientIp */, INADDR_ANY /* yourIp */,
+                INADDR_ANY /* nextIp */, relayIp, clientMac, broadcast);
+    }
+
+    public String toString() {
+        String s = super.toString();
+        return s + " NAK, reason " + (mMessage == null ? "(none)" : mMessage);
+    }
+
+    /**
+     * Fills in a packet with the requested NAK attributes.
+     */
+    public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+        ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+        // Constructor does not set values for layers <= 3: use empty values
+        Inet4Address destIp = INADDR_ANY;
+        Inet4Address srcIp = INADDR_ANY;
+
+        fillInPacket(encap, destIp, srcIp, destUdp, srcUdp, result, DHCP_BOOTREPLY, mBroadcast);
+        result.flip();
+        return result;
+    }
+
+    /**
+     * Adds the optional parameters to the client-generated NAK packet.
+     */
+    void finishPacket(ByteBuffer buffer) {
+        addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_NAK);
+        addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier);
+        addTlv(buffer, DHCP_MESSAGE, mMessage);
+        addTlvEnd(buffer);
+    }
+}
diff --git a/src/android/net/dhcp/DhcpOfferPacket.java b/src/android/net/dhcp/DhcpOfferPacket.java
new file mode 100644
index 0000000..0eba77e
--- /dev/null
+++ b/src/android/net/dhcp/DhcpOfferPacket.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2010 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 android.net.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-OFFER packet.
+ */
+class DhcpOfferPacket extends DhcpPacket {
+    /**
+     * The IP address of the server which sent this packet.
+     */
+    private final Inet4Address mSrcIp;
+
+    /**
+     * Generates a OFFER packet with the specified parameters.
+     */
+    DhcpOfferPacket(int transId, short secs, boolean broadcast, Inet4Address serverAddress,
+            Inet4Address relayIp, Inet4Address clientIp, Inet4Address yourIp, byte[] clientMac) {
+        super(transId, secs, clientIp, yourIp, serverAddress, relayIp, clientMac, broadcast);
+        mSrcIp = serverAddress;
+    }
+
+    public String toString() {
+        String s = super.toString();
+        String dnsServers = ", DNS servers: ";
+
+        if (mDnsServers != null) {
+            for (Inet4Address dnsServer: mDnsServers) {
+                dnsServers += dnsServer + " ";
+            }
+        }
+
+        return s + " OFFER, ip " + mYourIp + ", mask " + mSubnetMask +
+                dnsServers + ", gateways " + mGateways +
+                " lease time " + mLeaseTime + ", domain " + mDomainName;
+    }
+
+    /**
+     * Fills in a packet with the specified OFFER attributes.
+     */
+    public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+        ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+        Inet4Address destIp = mBroadcast ? INADDR_BROADCAST : mYourIp;
+        Inet4Address srcIp = mBroadcast ? INADDR_ANY : mSrcIp;
+
+        fillInPacket(encap, destIp, srcIp, destUdp, srcUdp, result,
+            DHCP_BOOTREPLY, mBroadcast);
+        result.flip();
+        return result;
+    }
+
+    /**
+     * Adds the optional parameters to the server-generated OFFER packet.
+     */
+    void finishPacket(ByteBuffer buffer) {
+        addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_OFFER);
+        addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier);
+
+        addCommonServerTlvs(buffer);
+        addTlvEnd(buffer);
+    }
+}
diff --git a/src/android/net/dhcp/DhcpPacket.java b/src/android/net/dhcp/DhcpPacket.java
new file mode 100644
index 0000000..ce8b7e7
--- /dev/null
+++ b/src/android/net/dhcp/DhcpPacket.java
@@ -0,0 +1,1364 @@
+package android.net.dhcp;
+
+import android.annotation.Nullable;
+import android.net.DhcpResults;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+import android.net.metrics.DhcpErrorEvent;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.system.OsConstants;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.UnsupportedEncodingException;
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Defines basic data and operations needed to build and use packets for the
+ * DHCP protocol.  Subclasses create the specific packets used at each
+ * stage of the negotiation.
+ *
+ * @hide
+ */
+public abstract class DhcpPacket {
+    protected static final String TAG = "DhcpPacket";
+
+    // TODO: use NetworkStackConstants.IPV4_MIN_MTU once this class is moved to the network stack.
+    private static final int IPV4_MIN_MTU = 68;
+
+    // dhcpcd has a minimum lease of 20 seconds, but DhcpStateMachine would refuse to wake up the
+    // CPU for anything shorter than 5 minutes. For sanity's sake, this must be higher than the
+    // DHCP client timeout.
+    public static final int MINIMUM_LEASE = 60;
+    public static final int INFINITE_LEASE = (int) 0xffffffff;
+
+    public static final Inet4Address INADDR_ANY = (Inet4Address) Inet4Address.ANY;
+    public static final Inet4Address INADDR_BROADCAST = (Inet4Address) Inet4Address.ALL;
+    public static final byte[] ETHER_BROADCAST = new byte[] {
+            (byte) 0xff, (byte) 0xff, (byte) 0xff,
+            (byte) 0xff, (byte) 0xff, (byte) 0xff,
+    };
+
+    /**
+     * Packet encapsulations.
+     */
+    public static final int ENCAP_L2 = 0;    // EthernetII header included
+    public static final int ENCAP_L3 = 1;    // IP/UDP header included
+    public static final int ENCAP_BOOTP = 2; // BOOTP contents only
+
+    /**
+     * Minimum length of a DHCP packet, excluding options, in the above encapsulations.
+     */
+    public static final int MIN_PACKET_LENGTH_BOOTP = 236;  // See diagram in RFC 2131, section 2.
+    public static final int MIN_PACKET_LENGTH_L3 = MIN_PACKET_LENGTH_BOOTP + 20 + 8;
+    public static final int MIN_PACKET_LENGTH_L2 = MIN_PACKET_LENGTH_L3 + 14;
+
+    public static final int HWADDR_LEN = 16;
+    public static final int MAX_OPTION_LEN = 255;
+
+    /**
+     * The minimum and maximum MTU that we are prepared to use. We set the minimum to the minimum
+     * IPv6 MTU because the IPv6 stack enters unusual codepaths when the link MTU drops below 1280,
+     * and does not recover if the MTU is brought above 1280 again. We set the maximum to 1500
+     * because in general it is risky to assume that the hardware is able to send/receive packets
+     * larger than 1500 bytes even if the network supports it.
+     */
+    private static final int MIN_MTU = 1280;
+    private static final int MAX_MTU = 1500;
+
+    /**
+     * IP layer definitions.
+     */
+    private static final byte IP_TYPE_UDP = (byte) 0x11;
+
+    /**
+     * IP: Version 4, Header Length 20 bytes
+     */
+    private static final byte IP_VERSION_HEADER_LEN = (byte) 0x45;
+
+    /**
+     * IP: Flags 0, Fragment Offset 0, Don't Fragment
+     */
+    private static final short IP_FLAGS_OFFSET = (short) 0x4000;
+
+    /**
+     * IP: TOS
+     */
+    private static final byte IP_TOS_LOWDELAY = (byte) 0x10;
+
+    /**
+     * IP: TTL -- use default 64 from RFC1340
+     */
+    private static final byte IP_TTL = (byte) 0x40;
+
+    /**
+     * The client DHCP port.
+     */
+    static final short DHCP_CLIENT = (short) 68;
+
+    /**
+     * The server DHCP port.
+     */
+    static final short DHCP_SERVER = (short) 67;
+
+    /**
+     * The message op code indicating a request from a client.
+     */
+    protected static final byte DHCP_BOOTREQUEST = (byte) 1;
+
+    /**
+     * The message op code indicating a response from the server.
+     */
+    protected static final byte DHCP_BOOTREPLY = (byte) 2;
+
+    /**
+     * The code type used to identify an Ethernet MAC address in the
+     * Client-ID field.
+     */
+    protected static final byte CLIENT_ID_ETHER = (byte) 1;
+
+    /**
+     * The maximum length of a packet that can be constructed.
+     */
+    protected static final int MAX_LENGTH = 1500;
+
+    /**
+     * The magic cookie that identifies this as a DHCP packet instead of BOOTP.
+     */
+    private static final int DHCP_MAGIC_COOKIE = 0x63825363;
+
+    /**
+     * DHCP Optional Type: DHCP Subnet Mask
+     */
+    protected static final byte DHCP_SUBNET_MASK = 1;
+    protected Inet4Address mSubnetMask;
+
+    /**
+     * DHCP Optional Type: DHCP Router
+     */
+    protected static final byte DHCP_ROUTER = 3;
+    protected List <Inet4Address> mGateways;
+
+    /**
+     * DHCP Optional Type: DHCP DNS Server
+     */
+    protected static final byte DHCP_DNS_SERVER = 6;
+    protected List<Inet4Address> mDnsServers;
+
+    /**
+     * DHCP Optional Type: DHCP Host Name
+     */
+    protected static final byte DHCP_HOST_NAME = 12;
+    protected String mHostName;
+
+    /**
+     * DHCP Optional Type: DHCP DOMAIN NAME
+     */
+    protected static final byte DHCP_DOMAIN_NAME = 15;
+    protected String mDomainName;
+
+    /**
+     * DHCP Optional Type: DHCP Interface MTU
+     */
+    protected static final byte DHCP_MTU = 26;
+    protected Short mMtu;
+
+    /**
+     * DHCP Optional Type: DHCP BROADCAST ADDRESS
+     */
+    protected static final byte DHCP_BROADCAST_ADDRESS = 28;
+    protected Inet4Address mBroadcastAddress;
+
+    /**
+     * DHCP Optional Type: Vendor specific information
+     */
+    protected static final byte DHCP_VENDOR_INFO = 43;
+    protected String mVendorInfo;
+
+    /**
+     * Value of the vendor specific option used to indicate that the network is metered
+     */
+    public static final String VENDOR_INFO_ANDROID_METERED = "ANDROID_METERED";
+
+    /**
+     * DHCP Optional Type: DHCP Requested IP Address
+     */
+    protected static final byte DHCP_REQUESTED_IP = 50;
+    protected Inet4Address mRequestedIp;
+
+    /**
+     * DHCP Optional Type: DHCP Lease Time
+     */
+    protected static final byte DHCP_LEASE_TIME = 51;
+    protected Integer mLeaseTime;
+
+    /**
+     * DHCP Optional Type: DHCP Message Type
+     */
+    protected static final byte DHCP_MESSAGE_TYPE = 53;
+    // the actual type values
+    protected static final byte DHCP_MESSAGE_TYPE_DISCOVER = 1;
+    protected static final byte DHCP_MESSAGE_TYPE_OFFER = 2;
+    protected static final byte DHCP_MESSAGE_TYPE_REQUEST = 3;
+    protected static final byte DHCP_MESSAGE_TYPE_DECLINE = 4;
+    protected static final byte DHCP_MESSAGE_TYPE_ACK = 5;
+    protected static final byte DHCP_MESSAGE_TYPE_NAK = 6;
+    protected static final byte DHCP_MESSAGE_TYPE_RELEASE = 7;
+    protected static final byte DHCP_MESSAGE_TYPE_INFORM = 8;
+
+    /**
+     * DHCP Optional Type: DHCP Server Identifier
+     */
+    protected static final byte DHCP_SERVER_IDENTIFIER = 54;
+    protected Inet4Address mServerIdentifier;
+
+    /**
+     * DHCP Optional Type: DHCP Parameter List
+     */
+    protected static final byte DHCP_PARAMETER_LIST = 55;
+    protected byte[] mRequestedParams;
+
+    /**
+     * DHCP Optional Type: DHCP MESSAGE
+     */
+    protected static final byte DHCP_MESSAGE = 56;
+    protected String mMessage;
+
+    /**
+     * DHCP Optional Type: Maximum DHCP Message Size
+     */
+    protected static final byte DHCP_MAX_MESSAGE_SIZE = 57;
+    protected Short mMaxMessageSize;
+
+    /**
+     * DHCP Optional Type: DHCP Renewal Time Value
+     */
+    protected static final byte DHCP_RENEWAL_TIME = 58;
+    protected Integer mT1;
+
+    /**
+     * DHCP Optional Type: Rebinding Time Value
+     */
+    protected static final byte DHCP_REBINDING_TIME = 59;
+    protected Integer mT2;
+
+    /**
+     * DHCP Optional Type: Vendor Class Identifier
+     */
+    protected static final byte DHCP_VENDOR_CLASS_ID = 60;
+    protected String mVendorId;
+
+    /**
+     * DHCP Optional Type: DHCP Client Identifier
+     */
+    protected static final byte DHCP_CLIENT_IDENTIFIER = 61;
+    protected byte[] mClientId;
+
+    /**
+     * DHCP zero-length option code: pad
+     */
+    protected static final byte DHCP_OPTION_PAD = 0x00;
+
+    /**
+     * DHCP zero-length option code: end of options
+     */
+    protected static final byte DHCP_OPTION_END = (byte) 0xff;
+
+    /**
+     * The transaction identifier used in this particular DHCP negotiation
+     */
+    protected final int mTransId;
+
+    /**
+     * The seconds field in the BOOTP header. Per RFC, should be nonzero in client requests only.
+     */
+    protected final short mSecs;
+
+    /**
+     * The IP address of the client host.  This address is typically
+     * proposed by the client (from an earlier DHCP negotiation) or
+     * supplied by the server.
+     */
+    protected final Inet4Address mClientIp;
+    protected final Inet4Address mYourIp;
+    private final Inet4Address mNextIp;
+    protected final Inet4Address mRelayIp;
+
+    /**
+     * Does the client request a broadcast response?
+     */
+    protected boolean mBroadcast;
+
+    /**
+     * The six-octet MAC of the client.
+     */
+    protected final byte[] mClientMac;
+
+    /**
+     * Asks the packet object to create a ByteBuffer serialization of
+     * the packet for transmission.
+     */
+    public abstract ByteBuffer buildPacket(int encap, short destUdp,
+        short srcUdp);
+
+    /**
+     * Allows the concrete class to fill in packet-type-specific details,
+     * typically optional parameters at the end of the packet.
+     */
+    abstract void finishPacket(ByteBuffer buffer);
+
+    // Set in unit tests, to ensure that the test does not break when run on different devices and
+    // on different releases.
+    static String testOverrideVendorId = null;
+    static String testOverrideHostname = null;
+
+    protected DhcpPacket(int transId, short secs, Inet4Address clientIp, Inet4Address yourIp,
+                         Inet4Address nextIp, Inet4Address relayIp,
+                         byte[] clientMac, boolean broadcast) {
+        mTransId = transId;
+        mSecs = secs;
+        mClientIp = clientIp;
+        mYourIp = yourIp;
+        mNextIp = nextIp;
+        mRelayIp = relayIp;
+        mClientMac = clientMac;
+        mBroadcast = broadcast;
+    }
+
+    /**
+     * Returns the transaction ID.
+     */
+    public int getTransactionId() {
+        return mTransId;
+    }
+
+    /**
+     * Returns the client MAC.
+     */
+    public byte[] getClientMac() {
+        return mClientMac;
+    }
+
+    // TODO: refactor DhcpClient to set clientId when constructing packets and remove
+    // hasExplicitClientId logic
+    /**
+     * Returns whether a client ID was set in the options for this packet.
+     */
+    public boolean hasExplicitClientId() {
+        return mClientId != null;
+    }
+
+    /**
+     * Convenience method to return the client ID if it was set explicitly, or null otherwise.
+     */
+    @Nullable
+    public byte[] getExplicitClientIdOrNull() {
+        return hasExplicitClientId() ? getClientId() : null;
+    }
+
+    /**
+     * Returns the client ID. If not set explicitly, this follows RFC 2132 and creates a client ID
+     * based on the hardware address.
+     */
+    public byte[] getClientId() {
+        final byte[] clientId;
+        if (hasExplicitClientId()) {
+            clientId = Arrays.copyOf(mClientId, mClientId.length);
+        } else {
+            clientId = new byte[mClientMac.length + 1];
+            clientId[0] = CLIENT_ID_ETHER;
+            System.arraycopy(mClientMac, 0, clientId, 1, mClientMac.length);
+        }
+        return clientId;
+    }
+
+    /**
+     * Returns whether a parameter is included in the parameter request list option of this packet.
+     *
+     * <p>If there is no parameter request list option in the packet, false is returned.
+     *
+     * @param paramId ID of the parameter, such as {@link #DHCP_MTU} or {@link #DHCP_HOST_NAME}.
+     */
+    public boolean hasRequestedParam(byte paramId) {
+        if (mRequestedParams == null) {
+            return false;
+        }
+
+        for (byte reqParam : mRequestedParams) {
+            if (reqParam == paramId) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Creates a new L3 packet (including IP header) containing the
+     * DHCP udp packet.  This method relies upon the delegated method
+     * finishPacket() to insert the per-packet contents.
+     */
+    protected void fillInPacket(int encap, Inet4Address destIp,
+        Inet4Address srcIp, short destUdp, short srcUdp, ByteBuffer buf,
+        byte requestCode, boolean broadcast) {
+        byte[] destIpArray = destIp.getAddress();
+        byte[] srcIpArray = srcIp.getAddress();
+        int ipHeaderOffset = 0;
+        int ipLengthOffset = 0;
+        int ipChecksumOffset = 0;
+        int endIpHeader = 0;
+        int udpHeaderOffset = 0;
+        int udpLengthOffset = 0;
+        int udpChecksumOffset = 0;
+
+        buf.clear();
+        buf.order(ByteOrder.BIG_ENDIAN);
+
+        if (encap == ENCAP_L2) {
+            buf.put(ETHER_BROADCAST);
+            buf.put(mClientMac);
+            buf.putShort((short) OsConstants.ETH_P_IP);
+        }
+
+        // if a full IP packet needs to be generated, put the IP & UDP
+        // headers in place, and pre-populate with artificial values
+        // needed to seed the IP checksum.
+        if (encap <= ENCAP_L3) {
+            ipHeaderOffset = buf.position();
+            buf.put(IP_VERSION_HEADER_LEN);
+            buf.put(IP_TOS_LOWDELAY);    // tos: IPTOS_LOWDELAY
+            ipLengthOffset = buf.position();
+            buf.putShort((short)0);  // length
+            buf.putShort((short)0);  // id
+            buf.putShort(IP_FLAGS_OFFSET); // ip offset: don't fragment
+            buf.put(IP_TTL);    // TTL: use default 64 from RFC1340
+            buf.put(IP_TYPE_UDP);
+            ipChecksumOffset = buf.position();
+            buf.putShort((short) 0); // checksum
+
+            buf.put(srcIpArray);
+            buf.put(destIpArray);
+            endIpHeader = buf.position();
+
+            // UDP header
+            udpHeaderOffset = buf.position();
+            buf.putShort(srcUdp);
+            buf.putShort(destUdp);
+            udpLengthOffset = buf.position();
+            buf.putShort((short) 0); // length
+            udpChecksumOffset = buf.position();
+            buf.putShort((short) 0); // UDP checksum -- initially zero
+        }
+
+        // DHCP payload
+        buf.put(requestCode);
+        buf.put((byte) 1); // Hardware Type: Ethernet
+        buf.put((byte) mClientMac.length); // Hardware Address Length
+        buf.put((byte) 0); // Hop Count
+        buf.putInt(mTransId);  // Transaction ID
+        buf.putShort(mSecs); // Elapsed Seconds
+
+        if (broadcast) {
+            buf.putShort((short) 0x8000); // Flags
+        } else {
+            buf.putShort((short) 0x0000); // Flags
+        }
+
+        buf.put(mClientIp.getAddress());
+        buf.put(mYourIp.getAddress());
+        buf.put(mNextIp.getAddress());
+        buf.put(mRelayIp.getAddress());
+        buf.put(mClientMac);
+        buf.position(buf.position() +
+                     (HWADDR_LEN - mClientMac.length) // pad addr to 16 bytes
+                     + 64     // empty server host name (64 bytes)
+                     + 128);  // empty boot file name (128 bytes)
+        buf.putInt(DHCP_MAGIC_COOKIE); // magic number
+        finishPacket(buf);
+
+        // round up to an even number of octets
+        if ((buf.position() & 1) == 1) {
+            buf.put((byte) 0);
+        }
+
+        // If an IP packet is being built, the IP & UDP checksums must be
+        // computed.
+        if (encap <= ENCAP_L3) {
+            // fix UDP header: insert length
+            short udpLen = (short)(buf.position() - udpHeaderOffset);
+            buf.putShort(udpLengthOffset, udpLen);
+            // fix UDP header: checksum
+            // checksum for UDP at udpChecksumOffset
+            int udpSeed = 0;
+
+            // apply IPv4 pseudo-header.  Read IP address src and destination
+            // values from the IP header and accumulate checksum.
+            udpSeed += intAbs(buf.getShort(ipChecksumOffset + 2));
+            udpSeed += intAbs(buf.getShort(ipChecksumOffset + 4));
+            udpSeed += intAbs(buf.getShort(ipChecksumOffset + 6));
+            udpSeed += intAbs(buf.getShort(ipChecksumOffset + 8));
+
+            // accumulate extra data for the pseudo-header
+            udpSeed += IP_TYPE_UDP;
+            udpSeed += udpLen;
+            // and compute UDP checksum
+            buf.putShort(udpChecksumOffset, (short) checksum(buf, udpSeed,
+                                                             udpHeaderOffset,
+                                                             buf.position()));
+            // fix IP header: insert length
+            buf.putShort(ipLengthOffset, (short)(buf.position() - ipHeaderOffset));
+            // fixup IP-header checksum
+            buf.putShort(ipChecksumOffset,
+                         (short) checksum(buf, 0, ipHeaderOffset, endIpHeader));
+        }
+    }
+
+    /**
+     * Converts a signed short value to an unsigned int value.  Needed
+     * because Java does not have unsigned types.
+     */
+    private static int intAbs(short v) {
+        return v & 0xFFFF;
+    }
+
+    /**
+     * Performs an IP checksum (used in IP header and across UDP
+     * payload) on the specified portion of a ByteBuffer.  The seed
+     * allows the checksum to commence with a specified value.
+     */
+    private int checksum(ByteBuffer buf, int seed, int start, int end) {
+        int sum = seed;
+        int bufPosition = buf.position();
+
+        // set position of original ByteBuffer, so that the ShortBuffer
+        // will be correctly initialized
+        buf.position(start);
+        ShortBuffer shortBuf = buf.asShortBuffer();
+
+        // re-set ByteBuffer position
+        buf.position(bufPosition);
+
+        short[] shortArray = new short[(end - start) / 2];
+        shortBuf.get(shortArray);
+
+        for (short s : shortArray) {
+            sum += intAbs(s);
+        }
+
+        start += shortArray.length * 2;
+
+        // see if a singleton byte remains
+        if (end != start) {
+            short b = buf.get(start);
+
+            // make it unsigned
+            if (b < 0) {
+                b += 256;
+            }
+
+            sum += b * 256;
+        }
+
+        sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF);
+        sum = ((sum + ((sum >> 16) & 0xFFFF)) & 0xFFFF);
+        int negated = ~sum;
+        return intAbs((short) negated);
+    }
+
+    /**
+     * Adds an optional parameter containing a single byte value.
+     */
+    protected static void addTlv(ByteBuffer buf, byte type, byte value) {
+        buf.put(type);
+        buf.put((byte) 1);
+        buf.put(value);
+    }
+
+    /**
+     * Adds an optional parameter containing an array of bytes.
+     *
+     * <p>This method is a no-op if the payload argument is null.
+     */
+    protected static void addTlv(ByteBuffer buf, byte type, @Nullable byte[] payload) {
+        if (payload != null) {
+            if (payload.length > MAX_OPTION_LEN) {
+                throw new IllegalArgumentException("DHCP option too long: "
+                        + payload.length + " vs. " + MAX_OPTION_LEN);
+            }
+            buf.put(type);
+            buf.put((byte) payload.length);
+            buf.put(payload);
+        }
+    }
+
+    /**
+     * Adds an optional parameter containing an IP address.
+     *
+     * <p>This method is a no-op if the address argument is null.
+     */
+    protected static void addTlv(ByteBuffer buf, byte type, @Nullable Inet4Address addr) {
+        if (addr != null) {
+            addTlv(buf, type, addr.getAddress());
+        }
+    }
+
+    /**
+     * Adds an optional parameter containing a list of IP addresses.
+     *
+     * <p>This method is a no-op if the addresses argument is null or empty.
+     */
+    protected static void addTlv(ByteBuffer buf, byte type, @Nullable List<Inet4Address> addrs) {
+        if (addrs == null || addrs.size() == 0) return;
+
+        int optionLen = 4 * addrs.size();
+        if (optionLen > MAX_OPTION_LEN) {
+            throw new IllegalArgumentException("DHCP option too long: "
+                    + optionLen + " vs. " + MAX_OPTION_LEN);
+        }
+
+        buf.put(type);
+        buf.put((byte)(optionLen));
+
+        for (Inet4Address addr : addrs) {
+            buf.put(addr.getAddress());
+        }
+    }
+
+    /**
+     * Adds an optional parameter containing a short integer.
+     *
+     * <p>This method is a no-op if the value argument is null.
+     */
+    protected static void addTlv(ByteBuffer buf, byte type, @Nullable Short value) {
+        if (value != null) {
+            buf.put(type);
+            buf.put((byte) 2);
+            buf.putShort(value.shortValue());
+        }
+    }
+
+    /**
+     * Adds an optional parameter containing a simple integer.
+     *
+     * <p>This method is a no-op if the value argument is null.
+     */
+    protected static void addTlv(ByteBuffer buf, byte type, @Nullable Integer value) {
+        if (value != null) {
+            buf.put(type);
+            buf.put((byte) 4);
+            buf.putInt(value.intValue());
+        }
+    }
+
+    /**
+     * Adds an optional parameter containing an ASCII string.
+     *
+     * <p>This method is a no-op if the string argument is null.
+     */
+    protected static void addTlv(ByteBuffer buf, byte type, @Nullable String str) {
+        if (str != null) {
+            try {
+                addTlv(buf, type, str.getBytes("US-ASCII"));
+            } catch (UnsupportedEncodingException e) {
+                throw new IllegalArgumentException("String is not US-ASCII: " + str);
+            }
+        }
+    }
+
+    /**
+     * Adds the special end-of-optional-parameters indicator.
+     */
+    protected static void addTlvEnd(ByteBuffer buf) {
+        buf.put((byte) 0xFF);
+    }
+
+    private String getVendorId() {
+        if (testOverrideVendorId != null) return testOverrideVendorId;
+        return "android-dhcp-" + Build.VERSION.RELEASE;
+    }
+
+    private String getHostname() {
+        if (testOverrideHostname != null) return testOverrideHostname;
+        return SystemProperties.get("net.hostname");
+    }
+
+    /**
+     * Adds common client TLVs.
+     *
+     * TODO: Does this belong here? The alternative would be to modify all the buildXyzPacket
+     * methods to take them.
+     */
+    protected void addCommonClientTlvs(ByteBuffer buf) {
+        addTlv(buf, DHCP_MAX_MESSAGE_SIZE, (short) MAX_LENGTH);
+        addTlv(buf, DHCP_VENDOR_CLASS_ID, getVendorId());
+        final String hn = getHostname();
+        if (!TextUtils.isEmpty(hn)) addTlv(buf, DHCP_HOST_NAME, hn);
+    }
+
+    protected void addCommonServerTlvs(ByteBuffer buf) {
+        addTlv(buf, DHCP_LEASE_TIME, mLeaseTime);
+        if (mLeaseTime != null && mLeaseTime != INFINITE_LEASE) {
+            // The client should renew at 1/2 the lease-expiry interval
+            addTlv(buf, DHCP_RENEWAL_TIME, (int) (Integer.toUnsignedLong(mLeaseTime) / 2));
+            // Default rebinding time is set as below by RFC2131
+            addTlv(buf, DHCP_REBINDING_TIME,
+                    (int) (Integer.toUnsignedLong(mLeaseTime) * 875L / 1000L));
+        }
+        addTlv(buf, DHCP_SUBNET_MASK, mSubnetMask);
+        addTlv(buf, DHCP_BROADCAST_ADDRESS, mBroadcastAddress);
+        addTlv(buf, DHCP_ROUTER, mGateways);
+        addTlv(buf, DHCP_DNS_SERVER, mDnsServers);
+        addTlv(buf, DHCP_DOMAIN_NAME, mDomainName);
+        addTlv(buf, DHCP_HOST_NAME, mHostName);
+        addTlv(buf, DHCP_VENDOR_INFO, mVendorInfo);
+        if (mMtu != null && Short.toUnsignedInt(mMtu) >= IPV4_MIN_MTU) {
+            addTlv(buf, DHCP_MTU, mMtu);
+        }
+    }
+
+    /**
+     * Converts a MAC from an array of octets to an ASCII string.
+     */
+    public static String macToString(byte[] mac) {
+        String macAddr = "";
+
+        for (int i = 0; i < mac.length; i++) {
+            String hexString = "0" + Integer.toHexString(mac[i]);
+
+            // substring operation grabs the last 2 digits: this
+            // allows signed bytes to be converted correctly.
+            macAddr += hexString.substring(hexString.length() - 2);
+
+            if (i != (mac.length - 1)) {
+                macAddr += ":";
+            }
+        }
+
+        return macAddr;
+    }
+
+    public String toString() {
+        String macAddr = macToString(mClientMac);
+
+        return macAddr;
+    }
+
+    /**
+     * Reads a four-octet value from a ByteBuffer and construct
+     * an IPv4 address from that value.
+     */
+    private static Inet4Address readIpAddress(ByteBuffer packet) {
+        Inet4Address result = null;
+        byte[] ipAddr = new byte[4];
+        packet.get(ipAddr);
+
+        try {
+            result = (Inet4Address) Inet4Address.getByAddress(ipAddr);
+        } catch (UnknownHostException ex) {
+            // ipAddr is numeric, so this should not be
+            // triggered.  However, if it is, just nullify
+            result = null;
+        }
+
+        return result;
+    }
+
+    /**
+     * Reads a string of specified length from the buffer.
+     */
+    private static String readAsciiString(ByteBuffer buf, int byteCount, boolean nullOk) {
+        byte[] bytes = new byte[byteCount];
+        buf.get(bytes);
+        int length = bytes.length;
+        if (!nullOk) {
+            // Stop at the first null byte. This is because some DHCP options (e.g., the domain
+            // name) are passed to netd via FrameworkListener, which refuses arguments containing
+            // null bytes. We don't do this by default because vendorInfo is an opaque string which
+            // could in theory contain null bytes.
+            for (length = 0; length < bytes.length; length++) {
+                if (bytes[length] == 0) {
+                    break;
+                }
+            }
+        }
+        return new String(bytes, 0, length, StandardCharsets.US_ASCII);
+    }
+
+    private static boolean isPacketToOrFromClient(short udpSrcPort, short udpDstPort) {
+        return (udpSrcPort == DHCP_CLIENT) || (udpDstPort == DHCP_CLIENT);
+    }
+
+    private static boolean isPacketServerToServer(short udpSrcPort, short udpDstPort) {
+        return (udpSrcPort == DHCP_SERVER) && (udpDstPort == DHCP_SERVER);
+    }
+
+    public static class ParseException extends Exception {
+        public final int errorCode;
+        public ParseException(int errorCode, String msg, Object... args) {
+            super(String.format(msg, args));
+            this.errorCode = errorCode;
+        }
+    }
+
+    /**
+     * Creates a concrete DhcpPacket from the supplied ByteBuffer.  The
+     * buffer may have an L2 encapsulation (which is the full EthernetII
+     * format starting with the source-address MAC) or an L3 encapsulation
+     * (which starts with the IP header).
+     * <br>
+     * A subset of the optional parameters are parsed and are stored
+     * in object fields.
+     */
+    @VisibleForTesting
+    static DhcpPacket decodeFullPacket(ByteBuffer packet, int pktType) throws ParseException
+    {
+        // bootp parameters
+        int transactionId;
+        short secs;
+        Inet4Address clientIp;
+        Inet4Address yourIp;
+        Inet4Address nextIp;
+        Inet4Address relayIp;
+        byte[] clientMac;
+        byte[] clientId = null;
+        List<Inet4Address> dnsServers = new ArrayList<>();
+        List<Inet4Address> gateways = new ArrayList<>();  // aka router
+        Inet4Address serverIdentifier = null;
+        Inet4Address netMask = null;
+        String message = null;
+        String vendorId = null;
+        String vendorInfo = null;
+        byte[] expectedParams = null;
+        String hostName = null;
+        String domainName = null;
+        Inet4Address ipSrc = null;
+        Inet4Address ipDst = null;
+        Inet4Address bcAddr = null;
+        Inet4Address requestedIp = null;
+
+        // The following are all unsigned integers. Internally we store them as signed integers of
+        // the same length because that way we're guaranteed that they can't be out of the range of
+        // the unsigned field in the packet. Callers wanting to pass in an unsigned value will need
+        // to cast it.
+        Short mtu = null;
+        Short maxMessageSize = null;
+        Integer leaseTime = null;
+        Integer T1 = null;
+        Integer T2 = null;
+
+        // dhcp options
+        byte dhcpType = (byte) 0xFF;
+
+        packet.order(ByteOrder.BIG_ENDIAN);
+
+        // check to see if we need to parse L2, IP, and UDP encaps
+        if (pktType == ENCAP_L2) {
+            if (packet.remaining() < MIN_PACKET_LENGTH_L2) {
+                throw new ParseException(DhcpErrorEvent.L2_TOO_SHORT,
+                        "L2 packet too short, %d < %d", packet.remaining(), MIN_PACKET_LENGTH_L2);
+            }
+
+            byte[] l2dst = new byte[6];
+            byte[] l2src = new byte[6];
+
+            packet.get(l2dst);
+            packet.get(l2src);
+
+            short l2type = packet.getShort();
+
+            if (l2type != OsConstants.ETH_P_IP) {
+                throw new ParseException(DhcpErrorEvent.L2_WRONG_ETH_TYPE,
+                        "Unexpected L2 type 0x%04x, expected 0x%04x", l2type, OsConstants.ETH_P_IP);
+            }
+        }
+
+        if (pktType <= ENCAP_L3) {
+            if (packet.remaining() < MIN_PACKET_LENGTH_L3) {
+                throw new ParseException(DhcpErrorEvent.L3_TOO_SHORT,
+                        "L3 packet too short, %d < %d", packet.remaining(), MIN_PACKET_LENGTH_L3);
+            }
+
+            byte ipTypeAndLength = packet.get();
+            int ipVersion = (ipTypeAndLength & 0xf0) >> 4;
+            if (ipVersion != 4) {
+                throw new ParseException(
+                        DhcpErrorEvent.L3_NOT_IPV4, "Invalid IP version %d", ipVersion);
+            }
+
+            // System.out.println("ipType is " + ipType);
+            byte ipDiffServicesField = packet.get();
+            short ipTotalLength = packet.getShort();
+            short ipIdentification = packet.getShort();
+            byte ipFlags = packet.get();
+            byte ipFragOffset = packet.get();
+            byte ipTTL = packet.get();
+            byte ipProto = packet.get();
+            short ipChksm = packet.getShort();
+
+            ipSrc = readIpAddress(packet);
+            ipDst = readIpAddress(packet);
+
+            if (ipProto != IP_TYPE_UDP) {
+                throw new ParseException(
+                        DhcpErrorEvent.L4_NOT_UDP, "Protocol not UDP: %d", ipProto);
+            }
+
+            // Skip options. This cannot cause us to read beyond the end of the buffer because the
+            // IPv4 header cannot be more than (0x0f * 4) = 60 bytes long, and that is less than
+            // MIN_PACKET_LENGTH_L3.
+            int optionWords = ((ipTypeAndLength & 0x0f) - 5);
+            for (int i = 0; i < optionWords; i++) {
+                packet.getInt();
+            }
+
+            // assume UDP
+            short udpSrcPort = packet.getShort();
+            short udpDstPort = packet.getShort();
+            short udpLen = packet.getShort();
+            short udpChkSum = packet.getShort();
+
+            // Only accept packets to or from the well-known client port (expressly permitting
+            // packets from ports other than the well-known server port; http://b/24687559), and
+            // server-to-server packets, e.g. for relays.
+            if (!isPacketToOrFromClient(udpSrcPort, udpDstPort) &&
+                !isPacketServerToServer(udpSrcPort, udpDstPort)) {
+                // This should almost never happen because we use SO_ATTACH_FILTER on the packet
+                // socket to drop packets that don't have the right source ports. However, it's
+                // possible that a packet arrives between when the socket is bound and when the
+                // filter is set. http://b/26696823 .
+                throw new ParseException(DhcpErrorEvent.L4_WRONG_PORT,
+                        "Unexpected UDP ports %d->%d", udpSrcPort, udpDstPort);
+            }
+        }
+
+        // We need to check the length even for ENCAP_L3 because the IPv4 header is variable-length.
+        if (pktType > ENCAP_BOOTP || packet.remaining() < MIN_PACKET_LENGTH_BOOTP) {
+            throw new ParseException(DhcpErrorEvent.BOOTP_TOO_SHORT,
+                        "Invalid type or BOOTP packet too short, %d < %d",
+                        packet.remaining(), MIN_PACKET_LENGTH_BOOTP);
+        }
+
+        byte type = packet.get();
+        byte hwType = packet.get();
+        int addrLen = packet.get() & 0xff;
+        byte hops = packet.get();
+        transactionId = packet.getInt();
+        secs = packet.getShort();
+        short bootpFlags = packet.getShort();
+        boolean broadcast = (bootpFlags & 0x8000) != 0;
+        byte[] ipv4addr = new byte[4];
+
+        try {
+            packet.get(ipv4addr);
+            clientIp = (Inet4Address) Inet4Address.getByAddress(ipv4addr);
+            packet.get(ipv4addr);
+            yourIp = (Inet4Address) Inet4Address.getByAddress(ipv4addr);
+            packet.get(ipv4addr);
+            nextIp = (Inet4Address) Inet4Address.getByAddress(ipv4addr);
+            packet.get(ipv4addr);
+            relayIp = (Inet4Address) Inet4Address.getByAddress(ipv4addr);
+        } catch (UnknownHostException ex) {
+            throw new ParseException(DhcpErrorEvent.L3_INVALID_IP,
+                    "Invalid IPv4 address: %s", Arrays.toString(ipv4addr));
+        }
+
+        // Some DHCP servers have been known to announce invalid client hardware address values such
+        // as 0xff. The legacy DHCP client accepted these becuause it does not check the length at
+        // all but only checks that the interface MAC address matches the first bytes of the address
+        // in the packets. We're a bit stricter: if the length is obviously invalid (i.e., bigger
+        // than the size of the field), we fudge it to 6 (Ethernet). http://b/23725795
+        // TODO: evaluate whether to make this test more liberal.
+        if (addrLen > HWADDR_LEN) {
+            addrLen = ETHER_BROADCAST.length;
+        }
+
+        clientMac = new byte[addrLen];
+        packet.get(clientMac);
+
+        // skip over address padding (16 octets allocated)
+        packet.position(packet.position() + (16 - addrLen)
+                        + 64    // skip server host name (64 chars)
+                        + 128); // skip boot file name (128 chars)
+
+        // Ensure this is a DHCP packet with a magic cookie, and not BOOTP. http://b/31850211
+        if (packet.remaining() < 4) {
+            throw new ParseException(DhcpErrorEvent.DHCP_NO_COOKIE, "not a DHCP message");
+        }
+
+        int dhcpMagicCookie = packet.getInt();
+        if (dhcpMagicCookie != DHCP_MAGIC_COOKIE) {
+            throw new ParseException(DhcpErrorEvent.DHCP_BAD_MAGIC_COOKIE,
+                    "Bad magic cookie 0x%08x, should be 0x%08x",
+                    dhcpMagicCookie, DHCP_MAGIC_COOKIE);
+        }
+
+        // parse options
+        boolean notFinishedOptions = true;
+
+        while ((packet.position() < packet.limit()) && notFinishedOptions) {
+            final byte optionType = packet.get(); // cannot underflow because position < limit
+            try {
+                if (optionType == DHCP_OPTION_END) {
+                    notFinishedOptions = false;
+                } else if (optionType == DHCP_OPTION_PAD) {
+                    // The pad option doesn't have a length field. Nothing to do.
+                } else {
+                    int optionLen = packet.get() & 0xFF;
+                    int expectedLen = 0;
+
+                    switch(optionType) {
+                        case DHCP_SUBNET_MASK:
+                            netMask = readIpAddress(packet);
+                            expectedLen = 4;
+                            break;
+                        case DHCP_ROUTER:
+                            for (expectedLen = 0; expectedLen < optionLen; expectedLen += 4) {
+                                gateways.add(readIpAddress(packet));
+                            }
+                            break;
+                        case DHCP_DNS_SERVER:
+                            for (expectedLen = 0; expectedLen < optionLen; expectedLen += 4) {
+                                dnsServers.add(readIpAddress(packet));
+                            }
+                            break;
+                        case DHCP_HOST_NAME:
+                            expectedLen = optionLen;
+                            hostName = readAsciiString(packet, optionLen, false);
+                            break;
+                        case DHCP_MTU:
+                            expectedLen = 2;
+                            mtu = packet.getShort();
+                            break;
+                        case DHCP_DOMAIN_NAME:
+                            expectedLen = optionLen;
+                            domainName = readAsciiString(packet, optionLen, false);
+                            break;
+                        case DHCP_BROADCAST_ADDRESS:
+                            bcAddr = readIpAddress(packet);
+                            expectedLen = 4;
+                            break;
+                        case DHCP_REQUESTED_IP:
+                            requestedIp = readIpAddress(packet);
+                            expectedLen = 4;
+                            break;
+                        case DHCP_LEASE_TIME:
+                            leaseTime = Integer.valueOf(packet.getInt());
+                            expectedLen = 4;
+                            break;
+                        case DHCP_MESSAGE_TYPE:
+                            dhcpType = packet.get();
+                            expectedLen = 1;
+                            break;
+                        case DHCP_SERVER_IDENTIFIER:
+                            serverIdentifier = readIpAddress(packet);
+                            expectedLen = 4;
+                            break;
+                        case DHCP_PARAMETER_LIST:
+                            expectedParams = new byte[optionLen];
+                            packet.get(expectedParams);
+                            expectedLen = optionLen;
+                            break;
+                        case DHCP_MESSAGE:
+                            expectedLen = optionLen;
+                            message = readAsciiString(packet, optionLen, false);
+                            break;
+                        case DHCP_MAX_MESSAGE_SIZE:
+                            expectedLen = 2;
+                            maxMessageSize = Short.valueOf(packet.getShort());
+                            break;
+                        case DHCP_RENEWAL_TIME:
+                            expectedLen = 4;
+                            T1 = Integer.valueOf(packet.getInt());
+                            break;
+                        case DHCP_REBINDING_TIME:
+                            expectedLen = 4;
+                            T2 = Integer.valueOf(packet.getInt());
+                            break;
+                        case DHCP_VENDOR_CLASS_ID:
+                            expectedLen = optionLen;
+                            // Embedded nulls are safe as this does not get passed to netd.
+                            vendorId = readAsciiString(packet, optionLen, true);
+                            break;
+                        case DHCP_CLIENT_IDENTIFIER: { // Client identifier
+                            byte[] id = new byte[optionLen];
+                            packet.get(id);
+                            expectedLen = optionLen;
+                        } break;
+                        case DHCP_VENDOR_INFO:
+                            expectedLen = optionLen;
+                            // Embedded nulls are safe as this does not get passed to netd.
+                            vendorInfo = readAsciiString(packet, optionLen, true);
+                            break;
+                        default:
+                            // ignore any other parameters
+                            for (int i = 0; i < optionLen; i++) {
+                                expectedLen++;
+                                byte throwaway = packet.get();
+                            }
+                    }
+
+                    if (expectedLen != optionLen) {
+                        final int errorCode = DhcpErrorEvent.errorCodeWithOption(
+                                DhcpErrorEvent.DHCP_INVALID_OPTION_LENGTH, optionType);
+                        throw new ParseException(errorCode,
+                                "Invalid length %d for option %d, expected %d",
+                                optionLen, optionType, expectedLen);
+                    }
+                }
+            } catch (BufferUnderflowException e) {
+                final int errorCode = DhcpErrorEvent.errorCodeWithOption(
+                        DhcpErrorEvent.BUFFER_UNDERFLOW, optionType);
+                throw new ParseException(errorCode, "BufferUnderflowException");
+            }
+        }
+
+        DhcpPacket newPacket;
+
+        switch(dhcpType) {
+            case (byte) 0xFF:
+                throw new ParseException(DhcpErrorEvent.DHCP_NO_MSG_TYPE,
+                        "No DHCP message type option");
+            case DHCP_MESSAGE_TYPE_DISCOVER:
+                newPacket = new DhcpDiscoverPacket(transactionId, secs, relayIp, clientMac,
+                        broadcast, ipSrc);
+                break;
+            case DHCP_MESSAGE_TYPE_OFFER:
+                newPacket = new DhcpOfferPacket(
+                    transactionId, secs, broadcast, ipSrc, relayIp, clientIp, yourIp, clientMac);
+                break;
+            case DHCP_MESSAGE_TYPE_REQUEST:
+                newPacket = new DhcpRequestPacket(
+                    transactionId, secs, clientIp, relayIp, clientMac, broadcast);
+                break;
+            case DHCP_MESSAGE_TYPE_DECLINE:
+                newPacket = new DhcpDeclinePacket(
+                    transactionId, secs, clientIp, yourIp, nextIp, relayIp,
+                    clientMac);
+                break;
+            case DHCP_MESSAGE_TYPE_ACK:
+                newPacket = new DhcpAckPacket(
+                    transactionId, secs, broadcast, ipSrc, relayIp, clientIp, yourIp, clientMac);
+                break;
+            case DHCP_MESSAGE_TYPE_NAK:
+                newPacket = new DhcpNakPacket(
+                        transactionId, secs, relayIp, clientMac, broadcast);
+                break;
+            case DHCP_MESSAGE_TYPE_RELEASE:
+                if (serverIdentifier == null) {
+                    throw new ParseException(DhcpErrorEvent.MISC_ERROR,
+                            "DHCPRELEASE without server identifier");
+                }
+                newPacket = new DhcpReleasePacket(
+                        transactionId, serverIdentifier, clientIp, relayIp, clientMac);
+                break;
+            case DHCP_MESSAGE_TYPE_INFORM:
+                newPacket = new DhcpInformPacket(
+                    transactionId, secs, clientIp, yourIp, nextIp, relayIp,
+                    clientMac);
+                break;
+            default:
+                throw new ParseException(DhcpErrorEvent.DHCP_UNKNOWN_MSG_TYPE,
+                        "Unimplemented DHCP type %d", dhcpType);
+        }
+
+        newPacket.mBroadcastAddress = bcAddr;
+        newPacket.mClientId = clientId;
+        newPacket.mDnsServers = dnsServers;
+        newPacket.mDomainName = domainName;
+        newPacket.mGateways = gateways;
+        newPacket.mHostName = hostName;
+        newPacket.mLeaseTime = leaseTime;
+        newPacket.mMessage = message;
+        newPacket.mMtu = mtu;
+        newPacket.mRequestedIp = requestedIp;
+        newPacket.mRequestedParams = expectedParams;
+        newPacket.mServerIdentifier = serverIdentifier;
+        newPacket.mSubnetMask = netMask;
+        newPacket.mMaxMessageSize = maxMessageSize;
+        newPacket.mT1 = T1;
+        newPacket.mT2 = T2;
+        newPacket.mVendorId = vendorId;
+        newPacket.mVendorInfo = vendorInfo;
+        return newPacket;
+    }
+
+    /**
+     * Parse a packet from an array of bytes, stopping at the given length.
+     */
+    public static DhcpPacket decodeFullPacket(byte[] packet, int length, int pktType)
+            throws ParseException {
+        ByteBuffer buffer = ByteBuffer.wrap(packet, 0, length).order(ByteOrder.BIG_ENDIAN);
+        try {
+            return decodeFullPacket(buffer, pktType);
+        } catch (ParseException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ParseException(DhcpErrorEvent.PARSING_ERROR, e.getMessage());
+        }
+    }
+
+    /**
+     *  Construct a DhcpResults object from a DHCP reply packet.
+     */
+    public DhcpResults toDhcpResults() {
+        Inet4Address ipAddress = mYourIp;
+        if (ipAddress.equals(Inet4Address.ANY)) {
+            ipAddress = mClientIp;
+            if (ipAddress.equals(Inet4Address.ANY)) {
+                return null;
+            }
+        }
+
+        int prefixLength;
+        if (mSubnetMask != null) {
+            try {
+                prefixLength = NetworkUtils.netmaskToPrefixLength(mSubnetMask);
+            } catch (IllegalArgumentException e) {
+                // Non-contiguous netmask.
+                return null;
+            }
+        } else {
+            prefixLength = NetworkUtils.getImplicitNetmask(ipAddress);
+        }
+
+        DhcpResults results = new DhcpResults();
+        try {
+            results.ipAddress = new LinkAddress(ipAddress, prefixLength);
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+
+        if (mGateways.size() > 0) {
+            results.gateway = mGateways.get(0);
+        }
+
+        results.dnsServers.addAll(mDnsServers);
+        results.domains = mDomainName;
+        results.serverAddress = mServerIdentifier;
+        results.vendorInfo = mVendorInfo;
+        results.leaseDuration = (mLeaseTime != null) ? mLeaseTime : INFINITE_LEASE;
+        results.mtu = (mMtu != null && MIN_MTU <= mMtu && mMtu <= MAX_MTU) ? mMtu : 0;
+
+        return results;
+    }
+
+    /**
+     * Returns the parsed lease time, in milliseconds, or 0 for infinite.
+     */
+    public long getLeaseTimeMillis() {
+        // dhcpcd treats the lack of a lease time option as an infinite lease.
+        if (mLeaseTime == null || mLeaseTime == INFINITE_LEASE) {
+            return 0;
+        } else if (0 <= mLeaseTime && mLeaseTime < MINIMUM_LEASE) {
+            return MINIMUM_LEASE * 1000;
+        } else {
+            return (mLeaseTime & 0xffffffffL) * 1000;
+        }
+    }
+
+    /**
+     * Builds a DHCP-DISCOVER packet from the required specified
+     * parameters.
+     */
+    public static ByteBuffer buildDiscoverPacket(int encap, int transactionId,
+        short secs, byte[] clientMac, boolean broadcast, byte[] expectedParams) {
+        DhcpPacket pkt = new DhcpDiscoverPacket(transactionId, secs, INADDR_ANY /* relayIp */,
+                clientMac, broadcast, INADDR_ANY /* srcIp */);
+        pkt.mRequestedParams = expectedParams;
+        return pkt.buildPacket(encap, DHCP_SERVER, DHCP_CLIENT);
+    }
+
+    /**
+     * Builds a DHCP-OFFER packet from the required specified
+     * parameters.
+     */
+    public static ByteBuffer buildOfferPacket(int encap, int transactionId,
+        boolean broadcast, Inet4Address serverIpAddr, Inet4Address relayIp,
+        Inet4Address yourIp, byte[] mac, Integer timeout, Inet4Address netMask,
+        Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
+        Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
+        short mtu) {
+        DhcpPacket pkt = new DhcpOfferPacket(
+                transactionId, (short) 0, broadcast, serverIpAddr, relayIp,
+                INADDR_ANY /* clientIp */, yourIp, mac);
+        pkt.mGateways = gateways;
+        pkt.mDnsServers = dnsServers;
+        pkt.mLeaseTime = timeout;
+        pkt.mDomainName = domainName;
+        pkt.mHostName = hostname;
+        pkt.mServerIdentifier = dhcpServerIdentifier;
+        pkt.mSubnetMask = netMask;
+        pkt.mBroadcastAddress = bcAddr;
+        pkt.mMtu = mtu;
+        if (metered) {
+            pkt.mVendorInfo = VENDOR_INFO_ANDROID_METERED;
+        }
+        return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER);
+    }
+
+    /**
+     * Builds a DHCP-ACK packet from the required specified parameters.
+     */
+    public static ByteBuffer buildAckPacket(int encap, int transactionId,
+        boolean broadcast, Inet4Address serverIpAddr, Inet4Address relayIp, Inet4Address yourIp,
+        Inet4Address requestClientIp, byte[] mac, Integer timeout, Inet4Address netMask,
+        Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
+        Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
+        short mtu) {
+        DhcpPacket pkt = new DhcpAckPacket(
+                transactionId, (short) 0, broadcast, serverIpAddr, relayIp, requestClientIp, yourIp,
+                mac);
+        pkt.mGateways = gateways;
+        pkt.mDnsServers = dnsServers;
+        pkt.mLeaseTime = timeout;
+        pkt.mDomainName = domainName;
+        pkt.mHostName = hostname;
+        pkt.mSubnetMask = netMask;
+        pkt.mServerIdentifier = dhcpServerIdentifier;
+        pkt.mBroadcastAddress = bcAddr;
+        pkt.mMtu = mtu;
+        if (metered) {
+            pkt.mVendorInfo = VENDOR_INFO_ANDROID_METERED;
+        }
+        return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER);
+    }
+
+    /**
+     * Builds a DHCP-NAK packet from the required specified parameters.
+     */
+    public static ByteBuffer buildNakPacket(int encap, int transactionId, Inet4Address serverIpAddr,
+            Inet4Address relayIp, byte[] mac, boolean broadcast, String message) {
+        DhcpPacket pkt = new DhcpNakPacket(
+                transactionId, (short) 0, relayIp, mac, broadcast);
+        pkt.mMessage = message;
+        pkt.mServerIdentifier = serverIpAddr;
+        return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER);
+    }
+
+    /**
+     * Builds a DHCP-REQUEST packet from the required specified parameters.
+     */
+    public static ByteBuffer buildRequestPacket(int encap,
+        int transactionId, short secs, Inet4Address clientIp, boolean broadcast,
+        byte[] clientMac, Inet4Address requestedIpAddress,
+        Inet4Address serverIdentifier, byte[] requestedParams, String hostName) {
+        DhcpPacket pkt = new DhcpRequestPacket(transactionId, secs, clientIp,
+                INADDR_ANY /* relayIp */, clientMac, broadcast);
+        pkt.mRequestedIp = requestedIpAddress;
+        pkt.mServerIdentifier = serverIdentifier;
+        pkt.mHostName = hostName;
+        pkt.mRequestedParams = requestedParams;
+        ByteBuffer result = pkt.buildPacket(encap, DHCP_SERVER, DHCP_CLIENT);
+        return result;
+    }
+}
diff --git a/src/android/net/dhcp/DhcpReleasePacket.java b/src/android/net/dhcp/DhcpReleasePacket.java
new file mode 100644
index 0000000..3958303
--- /dev/null
+++ b/src/android/net/dhcp/DhcpReleasePacket.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 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 android.net.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * Implements DHCP-RELEASE
+ */
+class DhcpReleasePacket extends DhcpPacket {
+
+    final Inet4Address mClientAddr;
+
+    /**
+     * Generates a RELEASE packet with the specified parameters.
+     */
+    public DhcpReleasePacket(int transId, Inet4Address serverId, Inet4Address clientAddr,
+            Inet4Address relayIp, byte[] clientMac) {
+        super(transId, (short)0, clientAddr, INADDR_ANY /* yourIp */, INADDR_ANY /* nextIp */,
+                relayIp, clientMac, false /* broadcast */);
+        mServerIdentifier = serverId;
+        mClientAddr = clientAddr;
+    }
+
+
+    @Override
+    public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+        ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+        fillInPacket(encap, mServerIdentifier /* destIp */, mClientIp /* srcIp */, destUdp, srcUdp,
+                result, DHCP_BOOTREPLY, mBroadcast);
+        result.flip();
+        return result;
+    }
+
+    @Override
+    void finishPacket(ByteBuffer buffer) {
+        addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_RELEASE);
+        addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId());
+        addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier);
+        addCommonClientTlvs(buffer);
+        addTlvEnd(buffer);
+    }
+}
diff --git a/src/android/net/dhcp/DhcpRequestPacket.java b/src/android/net/dhcp/DhcpRequestPacket.java
new file mode 100644
index 0000000..231d045
--- /dev/null
+++ b/src/android/net/dhcp/DhcpRequestPacket.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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 android.net.dhcp;
+
+import android.util.Log;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-REQUEST packet.
+ */
+class DhcpRequestPacket extends DhcpPacket {
+    /**
+     * Generates a REQUEST packet with the specified parameters.
+     */
+    DhcpRequestPacket(int transId, short secs, Inet4Address clientIp, Inet4Address relayIp,
+            byte[] clientMac, boolean broadcast) {
+        super(transId, secs, clientIp, INADDR_ANY, INADDR_ANY, relayIp, clientMac, broadcast);
+    }
+
+    public String toString() {
+        String s = super.toString();
+        return s + " REQUEST, desired IP " + mRequestedIp + " from host '"
+            + mHostName + "', param list length "
+            + (mRequestedParams == null ? 0 : mRequestedParams.length);
+    }
+
+    /**
+     * Fills in a packet with the requested REQUEST attributes.
+     */
+    public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+        ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+
+        fillInPacket(encap, INADDR_BROADCAST, INADDR_ANY, destUdp, srcUdp,
+            result, DHCP_BOOTREQUEST, mBroadcast);
+        result.flip();
+        return result;
+    }
+
+    /**
+     * Adds the optional parameters to the client-generated REQUEST packet.
+     */
+    void finishPacket(ByteBuffer buffer) {
+        addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_REQUEST);
+        addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId());
+        if (!INADDR_ANY.equals(mRequestedIp)) {
+            addTlv(buffer, DHCP_REQUESTED_IP, mRequestedIp);
+        }
+        if (!INADDR_ANY.equals(mServerIdentifier)) {
+            addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier);
+        }
+        addCommonClientTlvs(buffer);
+        addTlv(buffer, DHCP_PARAMETER_LIST, mRequestedParams);
+        addTlvEnd(buffer);
+    }
+}
diff --git a/src/android/net/ip/ConnectivityPacketTracker.java b/src/android/net/ip/ConnectivityPacketTracker.java
new file mode 100644
index 0000000..385dd52
--- /dev/null
+++ b/src/android/net/ip/ConnectivityPacketTracker.java
@@ -0,0 +1,153 @@
+/*
+ * 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 android.net.ip;
+
+import static android.net.util.SocketUtils.makePacketSocketAddress;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ARPHRD_ETHER;
+import static android.system.OsConstants.ETH_P_ALL;
+import static android.system.OsConstants.SOCK_RAW;
+
+import android.net.NetworkUtils;
+import android.net.util.ConnectivityPacketSummary;
+import android.net.util.InterfaceParams;
+import android.net.util.PacketReader;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.Log;
+
+import libcore.util.HexEncoding;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+
+/**
+ * Critical connectivity packet tracking daemon.
+ *
+ * Tracks ARP, DHCPv4, and IPv6 RS/RA/NS/NA packets.
+ *
+ * This class's constructor, start() and stop() methods must only be called
+ * from the same thread on which the passed in |log| is accessed.
+ *
+ * Log lines include a hexdump of the packet, which can be decoded via:
+ *
+ *     echo -n H3XSTR1NG | sed -e 's/\([0-9A-F][0-9A-F]\)/\1 /g' -e 's/^/000000 /'
+ *                       | text2pcap - -
+ *                       | tcpdump -n -vv -e -r -
+ *
+ * @hide
+ */
+public class ConnectivityPacketTracker {
+    private static final String TAG = ConnectivityPacketTracker.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final String MARK_START = "--- START ---";
+    private static final String MARK_STOP = "--- STOP ---";
+    private static final String MARK_NAMED_START = "--- START (%s) ---";
+    private static final String MARK_NAMED_STOP = "--- STOP (%s) ---";
+
+    private final String mTag;
+    private final LocalLog mLog;
+    private final PacketReader mPacketListener;
+    private boolean mRunning;
+    private String mDisplayName;
+
+    public ConnectivityPacketTracker(Handler h, InterfaceParams ifParams, LocalLog log) {
+        if (ifParams == null) throw new IllegalArgumentException("null InterfaceParams");
+
+        mTag = TAG + "." + ifParams.name;
+        mLog = log;
+        mPacketListener = new PacketListener(h, ifParams);
+    }
+
+    public void start(String displayName) {
+        mRunning = true;
+        mDisplayName = displayName;
+        mPacketListener.start();
+    }
+
+    public void stop() {
+        mPacketListener.stop();
+        mRunning = false;
+        mDisplayName = null;
+    }
+
+    private final class PacketListener extends PacketReader {
+        private final InterfaceParams mInterface;
+
+        PacketListener(Handler h, InterfaceParams ifParams) {
+            super(h, ifParams.defaultMtu);
+            mInterface = ifParams;
+        }
+
+        @Override
+        protected FileDescriptor createFd() {
+            FileDescriptor s = null;
+            try {
+                s = Os.socket(AF_PACKET, SOCK_RAW, 0);
+                NetworkUtils.attachControlPacketFilter(s, ARPHRD_ETHER);
+                Os.bind(s, makePacketSocketAddress((short) ETH_P_ALL, mInterface.index));
+            } catch (ErrnoException | IOException e) {
+                logError("Failed to create packet tracking socket: ", e);
+                closeFd(s);
+                return null;
+            }
+            return s;
+        }
+
+        @Override
+        protected void handlePacket(byte[] recvbuf, int length) {
+            final String summary = ConnectivityPacketSummary.summarize(
+                    mInterface.macAddr, recvbuf, length);
+            if (summary == null) return;
+
+            if (DBG) Log.d(mTag, summary);
+            addLogEntry(summary +
+                        "\n[" + new String(HexEncoding.encode(recvbuf, 0, length)) + "]");
+        }
+
+        @Override
+        protected void onStart() {
+            final String msg = TextUtils.isEmpty(mDisplayName)
+                    ? MARK_START
+                    : String.format(MARK_NAMED_START, mDisplayName);
+            mLog.log(msg);
+        }
+
+        @Override
+        protected void onStop() {
+            String msg = TextUtils.isEmpty(mDisplayName)
+                    ? MARK_STOP
+                    : String.format(MARK_NAMED_STOP, mDisplayName);
+            if (!mRunning) msg += " (packet listener stopped unexpectedly)";
+            mLog.log(msg);
+        }
+
+        @Override
+        protected void logError(String msg, Exception e) {
+            Log.e(mTag, msg, e);
+            addLogEntry(msg + e);
+        }
+
+        private void addLogEntry(String entry) {
+            mLog.log(entry);
+        }
+    }
+}
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
new file mode 100644
index 0000000..ad7f85d
--- /dev/null
+++ b/src/android/net/ip/IpClient.java
@@ -0,0 +1,1691 @@
+/*
+ * Copyright (C) 2017 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 android.net.ip;
+
+import static android.net.shared.IpConfigurationParcelableUtil.toStableParcelable;
+import static android.net.shared.LinkPropertiesParcelableUtil.fromStableParcelable;
+import static android.net.shared.LinkPropertiesParcelableUtil.toStableParcelable;
+
+import static com.android.server.util.PermissionUtil.checkNetworkStackCallingPermission;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.DhcpResults;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.ProvisioningConfigurationParcelable;
+import android.net.ProxyInfo;
+import android.net.ProxyInfoParcelable;
+import android.net.RouteInfo;
+import android.net.apf.ApfCapabilities;
+import android.net.apf.ApfFilter;
+import android.net.dhcp.DhcpClient;
+import android.net.ip.IIpClientCallbacks;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.IpManagerEvent;
+import android.net.shared.InitialConfiguration;
+import android.net.shared.NetdService;
+import android.net.shared.ProvisioningConfiguration;
+import android.net.util.InterfaceParams;
+import android.net.util.SharedLog;
+import android.os.ConditionVariable;
+import android.os.INetworkManagementService;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IState;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.internal.util.WakeupMessage;
+import com.android.server.net.NetlinkTracker;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+
+/**
+ * IpClient
+ *
+ * This class provides the interface to IP-layer provisioning and maintenance
+ * functionality that can be used by transport layers like Wi-Fi, Ethernet,
+ * et cetera.
+ *
+ * [ Lifetime ]
+ * IpClient is designed to be instantiated as soon as the interface name is
+ * known and can be as long-lived as the class containing it (i.e. declaring
+ * it "private final" is okay).
+ *
+ * @hide
+ */
+public class IpClient extends StateMachine {
+    private static final boolean DBG = false;
+
+    // For message logging.
+    private static final Class[] sMessageClasses = { IpClient.class, DhcpClient.class };
+    private static final SparseArray<String> sWhatToString =
+            MessageUtils.findMessageNames(sMessageClasses);
+    // Two static concurrent hashmaps of interface name to logging classes.
+    // One holds StateMachine logs and the other connectivity packet logs.
+    private static final ConcurrentHashMap<String, SharedLog> sSmLogs = new ConcurrentHashMap<>();
+    private static final ConcurrentHashMap<String, LocalLog> sPktLogs = new ConcurrentHashMap<>();
+
+    /**
+     * Dump all state machine and connectivity packet logs to the specified writer.
+     * @param skippedIfaces Interfaces for which logs should not be dumped.
+     */
+    public static void dumpAllLogs(PrintWriter writer, Set<String> skippedIfaces) {
+        for (String ifname : sSmLogs.keySet()) {
+            if (skippedIfaces.contains(ifname)) continue;
+
+            writer.println(String.format("--- BEGIN %s ---", ifname));
+
+            final SharedLog smLog = sSmLogs.get(ifname);
+            if (smLog != null) {
+                writer.println("State machine log:");
+                smLog.dump(null, writer, null);
+            }
+
+            writer.println("");
+
+            final LocalLog pktLog = sPktLogs.get(ifname);
+            if (pktLog != null) {
+                writer.println("Connectivity packet log:");
+                pktLog.readOnlyLocalLog().dump(null, writer, null);
+            }
+
+            writer.println(String.format("--- END %s ---", ifname));
+        }
+    }
+
+    // Use a wrapper class to log in order to ensure complete and detailed
+    // logging. This method is lighter weight than annotations/reflection
+    // and has the following benefits:
+    //
+    //     - No invoked method can be forgotten.
+    //       Any new method added to IpClient.Callback must be overridden
+    //       here or it will never be called.
+    //
+    //     - No invoking call site can be forgotten.
+    //       Centralized logging in this way means call sites don't need to
+    //       remember to log, and therefore no call site can be forgotten.
+    //
+    //     - No variation in log format among call sites.
+    //       Encourages logging of any available arguments, and all call sites
+    //       are necessarily logged identically.
+    //
+    // NOTE: Log first because passed objects may or may not be thread-safe and
+    // once passed on to the callback they may be modified by another thread.
+    //
+    // TODO: Find an lighter weight approach.
+    public static class IpClientCallbacksWrapper {
+        private static final String PREFIX = "INVOKE ";
+        private final IIpClientCallbacks mCallback;
+        private final SharedLog mLog;
+
+        @VisibleForTesting
+        protected IpClientCallbacksWrapper(IIpClientCallbacks callback, SharedLog log) {
+            mCallback = callback;
+            mLog = log;
+        }
+
+        private void log(String msg) {
+            mLog.log(PREFIX + msg);
+        }
+
+        private void log(String msg, Throwable e) {
+            mLog.e(PREFIX + msg, e);
+        }
+
+        public void onPreDhcpAction() {
+            log("onPreDhcpAction()");
+            try {
+                mCallback.onPreDhcpAction();
+            } catch (RemoteException e) {
+                log("Failed to call onPreDhcpAction", e);
+            }
+        }
+
+        public void onPostDhcpAction() {
+            log("onPostDhcpAction()");
+            try {
+                mCallback.onPostDhcpAction();
+            } catch (RemoteException e) {
+                log("Failed to call onPostDhcpAction", e);
+            }
+        }
+
+        public void onNewDhcpResults(DhcpResults dhcpResults) {
+            log("onNewDhcpResults({" + dhcpResults + "})");
+            try {
+                mCallback.onNewDhcpResults(toStableParcelable(dhcpResults));
+            } catch (RemoteException e) {
+                log("Failed to call onNewDhcpResults", e);
+            }
+        }
+
+        public void onProvisioningSuccess(LinkProperties newLp) {
+            log("onProvisioningSuccess({" + newLp + "})");
+            try {
+                mCallback.onProvisioningSuccess(toStableParcelable(newLp));
+            } catch (RemoteException e) {
+                log("Failed to call onProvisioningSuccess", e);
+            }
+        }
+
+        public void onProvisioningFailure(LinkProperties newLp) {
+            log("onProvisioningFailure({" + newLp + "})");
+            try {
+                mCallback.onProvisioningFailure(toStableParcelable(newLp));
+            } catch (RemoteException e) {
+                log("Failed to call onProvisioningFailure", e);
+            }
+        }
+
+        public void onLinkPropertiesChange(LinkProperties newLp) {
+            log("onLinkPropertiesChange({" + newLp + "})");
+            try {
+                mCallback.onLinkPropertiesChange(toStableParcelable(newLp));
+            } catch (RemoteException e) {
+                log("Failed to call onLinkPropertiesChange", e);
+            }
+        }
+
+        public void onReachabilityLost(String logMsg) {
+            log("onReachabilityLost(" + logMsg + ")");
+            try {
+                mCallback.onReachabilityLost(logMsg);
+            } catch (RemoteException e) {
+                log("Failed to call onReachabilityLost", e);
+            }
+        }
+
+        public void onQuit() {
+            log("onQuit()");
+            try {
+                mCallback.onQuit();
+            } catch (RemoteException e) {
+                log("Failed to call onQuit", e);
+            }
+        }
+
+        public void installPacketFilter(byte[] filter) {
+            log("installPacketFilter(byte[" + filter.length + "])");
+            try {
+                mCallback.installPacketFilter(filter);
+            } catch (RemoteException e) {
+                log("Failed to call installPacketFilter", e);
+            }
+        }
+
+        public void startReadPacketFilter() {
+            log("startReadPacketFilter()");
+            try {
+                mCallback.startReadPacketFilter();
+            } catch (RemoteException e) {
+                log("Failed to call startReadPacketFilter", e);
+            }
+        }
+
+        public void setFallbackMulticastFilter(boolean enabled) {
+            log("setFallbackMulticastFilter(" + enabled + ")");
+            try {
+                mCallback.setFallbackMulticastFilter(enabled);
+            } catch (RemoteException e) {
+                log("Failed to call setFallbackMulticastFilter", e);
+            }
+        }
+
+        public void setNeighborDiscoveryOffload(boolean enable) {
+            log("setNeighborDiscoveryOffload(" + enable + ")");
+            try {
+                mCallback.setNeighborDiscoveryOffload(enable);
+            } catch (RemoteException e) {
+                log("Failed to call setNeighborDiscoveryOffload", e);
+            }
+        }
+    }
+
+    public static final String DUMP_ARG_CONFIRM = "confirm";
+
+    private static final int CMD_TERMINATE_AFTER_STOP             = 1;
+    private static final int CMD_STOP                             = 2;
+    private static final int CMD_START                            = 3;
+    private static final int CMD_CONFIRM                          = 4;
+    private static final int EVENT_PRE_DHCP_ACTION_COMPLETE       = 5;
+    // Triggered by NetlinkTracker to communicate netlink events.
+    private static final int EVENT_NETLINK_LINKPROPERTIES_CHANGED = 6;
+    private static final int CMD_UPDATE_TCP_BUFFER_SIZES          = 7;
+    private static final int CMD_UPDATE_HTTP_PROXY                = 8;
+    private static final int CMD_SET_MULTICAST_FILTER             = 9;
+    private static final int EVENT_PROVISIONING_TIMEOUT           = 10;
+    private static final int EVENT_DHCPACTION_TIMEOUT             = 11;
+    private static final int EVENT_READ_PACKET_FILTER_COMPLETE    = 12;
+
+    // Internal commands to use instead of trying to call transitionTo() inside
+    // a given State's enter() method. Calling transitionTo() from enter/exit
+    // encounters a Log.wtf() that can cause trouble on eng builds.
+    private static final int CMD_JUMP_STARTED_TO_RUNNING          = 100;
+    private static final int CMD_JUMP_RUNNING_TO_STOPPING         = 101;
+    private static final int CMD_JUMP_STOPPING_TO_STOPPED         = 102;
+
+    // IpClient shares a handler with DhcpClient: commands must not overlap
+    public static final int DHCPCLIENT_CMD_BASE = 1000;
+
+    private static final int MAX_LOG_RECORDS = 500;
+    private static final int MAX_PACKET_RECORDS = 100;
+
+    private static final boolean NO_CALLBACKS = false;
+    private static final boolean SEND_CALLBACKS = true;
+
+    // This must match the interface prefix in clatd.c.
+    // TODO: Revert this hack once IpClient and Nat464Xlat work in concert.
+    private static final String CLAT_PREFIX = "v4-";
+
+    private static final int IMMEDIATE_FAILURE_DURATION = 0;
+
+    private static final int PROV_CHANGE_STILL_NOT_PROVISIONED = 1;
+    private static final int PROV_CHANGE_LOST_PROVISIONING = 2;
+    private static final int PROV_CHANGE_GAINED_PROVISIONING = 3;
+    private static final int PROV_CHANGE_STILL_PROVISIONED = 4;
+
+    private final State mStoppedState = new StoppedState();
+    private final State mStoppingState = new StoppingState();
+    private final State mStartedState = new StartedState();
+    private final State mRunningState = new RunningState();
+
+    private final String mTag;
+    private final Context mContext;
+    private final String mInterfaceName;
+    private final String mClatInterfaceName;
+    @VisibleForTesting
+    protected final IpClientCallbacksWrapper mCallback;
+    private final Dependencies mDependencies;
+    private final CountDownLatch mShutdownLatch;
+    private final ConnectivityManager mCm;
+    private final INetworkManagementService mNwService;
+    private final NetlinkTracker mNetlinkTracker;
+    private final WakeupMessage mProvisioningTimeoutAlarm;
+    private final WakeupMessage mDhcpActionTimeoutAlarm;
+    private final SharedLog mLog;
+    private final LocalLog mConnectivityPacketLog;
+    private final MessageHandlingLogger mMsgStateLogger;
+    private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
+    private final InterfaceController mInterfaceCtrl;
+
+    private InterfaceParams mInterfaceParams;
+
+    /**
+     * Non-final member variables accessed only from within our StateMachine.
+     */
+    private LinkProperties mLinkProperties;
+    private android.net.shared.ProvisioningConfiguration mConfiguration;
+    private IpReachabilityMonitor mIpReachabilityMonitor;
+    private DhcpClient mDhcpClient;
+    private DhcpResults mDhcpResults;
+    private String mTcpBufferSizes;
+    private ProxyInfo mHttpProxy;
+    private ApfFilter mApfFilter;
+    private boolean mMulticastFiltering;
+    private long mStartTimeMillis;
+
+    /**
+     * Reading the snapshot is an asynchronous operation initiated by invoking
+     * Callback.startReadPacketFilter() and completed when the WiFi Service responds with an
+     * EVENT_READ_PACKET_FILTER_COMPLETE message. The mApfDataSnapshotComplete condition variable
+     * signals when a new snapshot is ready.
+     */
+    private final ConditionVariable mApfDataSnapshotComplete = new ConditionVariable();
+
+    public static class Dependencies {
+        public INetworkManagementService getNMS() {
+            return INetworkManagementService.Stub.asInterface(
+                    ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
+        }
+
+        public INetd getNetd() {
+            return NetdService.getInstance();
+        }
+
+        /**
+         * Get interface parameters for the specified interface.
+         */
+        public InterfaceParams getInterfaceParams(String ifname) {
+            return InterfaceParams.getByName(ifname);
+        }
+    }
+
+    public IpClient(Context context, String ifName, IIpClientCallbacks callback) {
+        this(context, ifName, callback, new Dependencies());
+    }
+
+    /**
+     * An expanded constructor, useful for dependency injection.
+     * TODO: migrate all test users to mock IpClient directly and remove this ctor.
+     */
+    public IpClient(Context context, String ifName, IIpClientCallbacks callback,
+            INetworkManagementService nwService) {
+        this(context, ifName, callback, new Dependencies() {
+            @Override
+            public INetworkManagementService getNMS() {
+                return nwService;
+            }
+        });
+    }
+
+    @VisibleForTesting
+    IpClient(Context context, String ifName, IIpClientCallbacks callback, Dependencies deps) {
+        super(IpClient.class.getSimpleName() + "." + ifName);
+        Preconditions.checkNotNull(ifName);
+        Preconditions.checkNotNull(callback);
+
+        mTag = getName();
+
+        mContext = context;
+        mInterfaceName = ifName;
+        mClatInterfaceName = CLAT_PREFIX + ifName;
+        mDependencies = deps;
+        mShutdownLatch = new CountDownLatch(1);
+        mCm = mContext.getSystemService(ConnectivityManager.class);
+        mNwService = deps.getNMS();
+
+        sSmLogs.putIfAbsent(mInterfaceName, new SharedLog(MAX_LOG_RECORDS, mTag));
+        mLog = sSmLogs.get(mInterfaceName);
+        sPktLogs.putIfAbsent(mInterfaceName, new LocalLog(MAX_PACKET_RECORDS));
+        mConnectivityPacketLog = sPktLogs.get(mInterfaceName);
+        mMsgStateLogger = new MessageHandlingLogger();
+        mCallback = new IpClientCallbacksWrapper(callback, mLog);
+
+        // TODO: Consider creating, constructing, and passing in some kind of
+        // InterfaceController.Dependencies class.
+        mInterfaceCtrl = new InterfaceController(mInterfaceName, deps.getNetd(), mLog);
+
+        mNetlinkTracker = new NetlinkTracker(
+                mInterfaceName,
+                new NetlinkTracker.Callback() {
+                    @Override
+                    public void update() {
+                        sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED);
+                    }
+                }) {
+            @Override
+            public void interfaceAdded(String iface) {
+                super.interfaceAdded(iface);
+                if (mClatInterfaceName.equals(iface)) {
+                    mCallback.setNeighborDiscoveryOffload(false);
+                } else if (!mInterfaceName.equals(iface)) {
+                    return;
+                }
+
+                final String msg = "interfaceAdded(" + iface + ")";
+                logMsg(msg);
+            }
+
+            @Override
+            public void interfaceRemoved(String iface) {
+                super.interfaceRemoved(iface);
+                // TODO: Also observe mInterfaceName going down and take some
+                // kind of appropriate action.
+                if (mClatInterfaceName.equals(iface)) {
+                    // TODO: consider sending a message to the IpClient main
+                    // StateMachine thread, in case "NDO enabled" state becomes
+                    // tied to more things that 464xlat operation.
+                    mCallback.setNeighborDiscoveryOffload(true);
+                } else if (!mInterfaceName.equals(iface)) {
+                    return;
+                }
+
+                final String msg = "interfaceRemoved(" + iface + ")";
+                logMsg(msg);
+            }
+
+            private void logMsg(String msg) {
+                Log.d(mTag, msg);
+                getHandler().post(() -> mLog.log("OBSERVED " + msg));
+            }
+        };
+
+        mLinkProperties = new LinkProperties();
+        mLinkProperties.setInterfaceName(mInterfaceName);
+
+        mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
+                mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT);
+        mDhcpActionTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
+                mTag + ".EVENT_DHCPACTION_TIMEOUT", EVENT_DHCPACTION_TIMEOUT);
+
+        // Anything the StateMachine may access must have been instantiated
+        // before this point.
+        configureAndStartStateMachine();
+
+        // Anything that may send messages to the StateMachine must only be
+        // configured to do so after the StateMachine has started (above).
+        startStateMachineUpdaters();
+    }
+
+    /**
+     * Make a IIpClient connector to communicate with this IpClient.
+     */
+    public IIpClient makeConnector() {
+        return new IpClientConnector();
+    }
+
+    class IpClientConnector extends IIpClient.Stub {
+        @Override
+        public void completedPreDhcpAction() {
+            checkNetworkStackCallingPermission();
+            IpClient.this.completedPreDhcpAction();
+        }
+        @Override
+        public void confirmConfiguration() {
+            checkNetworkStackCallingPermission();
+            IpClient.this.confirmConfiguration();
+        }
+        @Override
+        public void readPacketFilterComplete(byte[] data) {
+            checkNetworkStackCallingPermission();
+            IpClient.this.readPacketFilterComplete(data);
+        }
+        @Override
+        public void shutdown() {
+            checkNetworkStackCallingPermission();
+            IpClient.this.shutdown();
+        }
+        @Override
+        public void startProvisioning(ProvisioningConfigurationParcelable req) {
+            checkNetworkStackCallingPermission();
+            IpClient.this.startProvisioning(ProvisioningConfiguration.fromStableParcelable(req));
+        }
+        @Override
+        public void stop() {
+            checkNetworkStackCallingPermission();
+            IpClient.this.stop();
+        }
+        @Override
+        public void setTcpBufferSizes(String tcpBufferSizes) {
+            checkNetworkStackCallingPermission();
+            IpClient.this.setTcpBufferSizes(tcpBufferSizes);
+        }
+        @Override
+        public void setHttpProxy(ProxyInfoParcelable proxyInfo) {
+            checkNetworkStackCallingPermission();
+            IpClient.this.setHttpProxy(fromStableParcelable(proxyInfo));
+        }
+        @Override
+        public void setMulticastFilter(boolean enabled) {
+            checkNetworkStackCallingPermission();
+            IpClient.this.setMulticastFilter(enabled);
+        }
+    }
+
+    public String getInterfaceName() {
+        return mInterfaceName;
+    }
+
+    private void configureAndStartStateMachine() {
+        // CHECKSTYLE:OFF IndentationCheck
+        addState(mStoppedState);
+        addState(mStartedState);
+            addState(mRunningState, mStartedState);
+        addState(mStoppingState);
+        // CHECKSTYLE:ON IndentationCheck
+
+        setInitialState(mStoppedState);
+
+        super.start();
+    }
+
+    private void startStateMachineUpdaters() {
+        try {
+            mNwService.registerObserver(mNetlinkTracker);
+        } catch (RemoteException e) {
+            logError("Couldn't register NetlinkTracker: %s", e);
+        }
+    }
+
+    private void stopStateMachineUpdaters() {
+        try {
+            mNwService.unregisterObserver(mNetlinkTracker);
+        } catch (RemoteException e) {
+            logError("Couldn't unregister NetlinkTracker: %s", e);
+        }
+    }
+
+    @Override
+    protected void onQuitting() {
+        mCallback.onQuit();
+        mShutdownLatch.countDown();
+    }
+
+    /**
+     * Shut down this IpClient instance altogether.
+     */
+    public void shutdown() {
+        stop();
+        sendMessage(CMD_TERMINATE_AFTER_STOP);
+    }
+
+    /**
+     * Start provisioning with the provided parameters.
+     */
+    public void startProvisioning(ProvisioningConfiguration req) {
+        if (!req.isValid()) {
+            doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
+            return;
+        }
+
+        mInterfaceParams = mDependencies.getInterfaceParams(mInterfaceName);
+        if (mInterfaceParams == null) {
+            logError("Failed to find InterfaceParams for " + mInterfaceName);
+            doImmediateProvisioningFailure(IpManagerEvent.ERROR_INTERFACE_NOT_FOUND);
+            return;
+        }
+
+        mCallback.setNeighborDiscoveryOffload(true);
+        sendMessage(CMD_START, new android.net.shared.ProvisioningConfiguration(req));
+    }
+
+    /**
+     * Stop this IpClient.
+     *
+     * <p>This does not shut down the StateMachine itself, which is handled by {@link #shutdown()}.
+     */
+    public void stop() {
+        sendMessage(CMD_STOP);
+    }
+
+    /**
+     * Confirm the provisioning configuration.
+     */
+    public void confirmConfiguration() {
+        sendMessage(CMD_CONFIRM);
+    }
+
+    /**
+     * For clients using {@link ProvisioningConfiguration.Builder#withPreDhcpAction()}, must be
+     * called after {@link IIpClientCallbacks#onPreDhcpAction} to indicate that DHCP is clear to
+     * proceed.
+     */
+    public void completedPreDhcpAction() {
+        sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE);
+    }
+
+    /**
+     * Indicate that packet filter read is complete.
+     */
+    public void readPacketFilterComplete(byte[] data) {
+        sendMessage(EVENT_READ_PACKET_FILTER_COMPLETE, data);
+    }
+
+    /**
+     * Set the TCP buffer sizes to use.
+     *
+     * This may be called, repeatedly, at any time before or after a call to
+     * #startProvisioning(). The setting is cleared upon calling #stop().
+     */
+    public void setTcpBufferSizes(String tcpBufferSizes) {
+        sendMessage(CMD_UPDATE_TCP_BUFFER_SIZES, tcpBufferSizes);
+    }
+
+    /**
+     * Set the HTTP Proxy configuration to use.
+     *
+     * This may be called, repeatedly, at any time before or after a call to
+     * #startProvisioning(). The setting is cleared upon calling #stop().
+     */
+    public void setHttpProxy(ProxyInfo proxyInfo) {
+        sendMessage(CMD_UPDATE_HTTP_PROXY, proxyInfo);
+    }
+
+    /**
+     * Enable or disable the multicast filter.  Attempts to use APF to accomplish the filtering,
+     * if not, Callback.setFallbackMulticastFilter() is called.
+     */
+    public void setMulticastFilter(boolean enabled) {
+        sendMessage(CMD_SET_MULTICAST_FILTER, enabled);
+    }
+
+    /**
+     * Dump logs of this IpClient.
+     */
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        if (args != null && args.length > 0 && DUMP_ARG_CONFIRM.equals(args[0])) {
+            // Execute confirmConfiguration() and take no further action.
+            confirmConfiguration();
+            return;
+        }
+
+        // Thread-unsafe access to mApfFilter but just used for debugging.
+        final ApfFilter apfFilter = mApfFilter;
+        final android.net.shared.ProvisioningConfiguration provisioningConfig = mConfiguration;
+        final ApfCapabilities apfCapabilities = (provisioningConfig != null)
+                ? provisioningConfig.mApfCapabilities : null;
+
+        IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+        pw.println(mTag + " APF dump:");
+        pw.increaseIndent();
+        if (apfFilter != null) {
+            if (apfCapabilities.hasDataAccess()) {
+                // Request a new snapshot, then wait for it.
+                mApfDataSnapshotComplete.close();
+                mCallback.startReadPacketFilter();
+                if (!mApfDataSnapshotComplete.block(1000)) {
+                    pw.print("TIMEOUT: DUMPING STALE APF SNAPSHOT");
+                }
+            }
+            apfFilter.dump(pw);
+
+        } else {
+            pw.print("No active ApfFilter; ");
+            if (provisioningConfig == null) {
+                pw.println("IpClient not yet started.");
+            } else if (apfCapabilities == null || apfCapabilities.apfVersionSupported == 0) {
+                pw.println("Hardware does not support APF.");
+            } else {
+                pw.println("ApfFilter not yet started, APF capabilities: " + apfCapabilities);
+            }
+        }
+        pw.decreaseIndent();
+        pw.println();
+        pw.println(mTag + " current ProvisioningConfiguration:");
+        pw.increaseIndent();
+        pw.println(Objects.toString(provisioningConfig, "N/A"));
+        pw.decreaseIndent();
+
+        final IpReachabilityMonitor iprm = mIpReachabilityMonitor;
+        if (iprm != null) {
+            pw.println();
+            pw.println(mTag + " current IpReachabilityMonitor state:");
+            pw.increaseIndent();
+            iprm.dump(pw);
+            pw.decreaseIndent();
+        }
+
+        pw.println();
+        pw.println(mTag + " StateMachine dump:");
+        pw.increaseIndent();
+        mLog.dump(fd, pw, args);
+        pw.decreaseIndent();
+
+        pw.println();
+        pw.println(mTag + " connectivity packet log:");
+        pw.println();
+        pw.println("Debug with python and scapy via:");
+        pw.println("shell$ python");
+        pw.println(">>> from scapy import all as scapy");
+        pw.println(">>> scapy.Ether(\"<paste_hex_string>\".decode(\"hex\")).show2()");
+        pw.println();
+
+        pw.increaseIndent();
+        mConnectivityPacketLog.readOnlyLocalLog().dump(fd, pw, args);
+        pw.decreaseIndent();
+    }
+
+
+    /**
+     * Internals.
+     */
+
+    @Override
+    protected String getWhatToString(int what) {
+        return sWhatToString.get(what, "UNKNOWN: " + Integer.toString(what));
+    }
+
+    @Override
+    protected String getLogRecString(Message msg) {
+        final String logLine = String.format(
+                "%s/%d %d %d %s [%s]",
+                mInterfaceName, (mInterfaceParams == null) ? -1 : mInterfaceParams.index,
+                msg.arg1, msg.arg2, Objects.toString(msg.obj), mMsgStateLogger);
+
+        final String richerLogLine = getWhatToString(msg.what) + " " + logLine;
+        mLog.log(richerLogLine);
+        if (DBG) {
+            Log.d(mTag, richerLogLine);
+        }
+
+        mMsgStateLogger.reset();
+        return logLine;
+    }
+
+    @Override
+    protected boolean recordLogRec(Message msg) {
+        // Don't log EVENT_NETLINK_LINKPROPERTIES_CHANGED. They can be noisy,
+        // and we already log any LinkProperties change that results in an
+        // invocation of IpClient.Callback#onLinkPropertiesChange().
+        final boolean shouldLog = (msg.what != EVENT_NETLINK_LINKPROPERTIES_CHANGED);
+        if (!shouldLog) {
+            mMsgStateLogger.reset();
+        }
+        return shouldLog;
+    }
+
+    private void logError(String fmt, Object... args) {
+        final String msg = "ERROR " + String.format(fmt, args);
+        Log.e(mTag, msg);
+        mLog.log(msg);
+    }
+
+    // This needs to be called with care to ensure that our LinkProperties
+    // are in sync with the actual LinkProperties of the interface. For example,
+    // we should only call this if we know for sure that there are no IP addresses
+    // assigned to the interface, etc.
+    private void resetLinkProperties() {
+        mNetlinkTracker.clearLinkProperties();
+        mConfiguration = null;
+        mDhcpResults = null;
+        mTcpBufferSizes = "";
+        mHttpProxy = null;
+
+        mLinkProperties = new LinkProperties();
+        mLinkProperties.setInterfaceName(mInterfaceName);
+    }
+
+    private void recordMetric(final int type) {
+        // We may record error metrics prior to starting.
+        // Map this to IMMEDIATE_FAILURE_DURATION.
+        final long duration = (mStartTimeMillis > 0)
+                ? (SystemClock.elapsedRealtime() - mStartTimeMillis)
+                : IMMEDIATE_FAILURE_DURATION;
+        mMetricsLog.log(mInterfaceName, new IpManagerEvent(type, duration));
+    }
+
+    // For now: use WifiStateMachine's historical notion of provisioned.
+    @VisibleForTesting
+    static boolean isProvisioned(LinkProperties lp, InitialConfiguration config) {
+        // For historical reasons, we should connect even if all we have is
+        // an IPv4 address and nothing else.
+        if (lp.hasIPv4Address() || lp.isProvisioned()) {
+            return true;
+        }
+        if (config == null) {
+            return false;
+        }
+
+        // When an InitialConfiguration is specified, ignore any difference with previous
+        // properties and instead check if properties observed match the desired properties.
+        return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes());
+    }
+
+    // TODO: Investigate folding all this into the existing static function
+    // LinkProperties.compareProvisioning() or some other single function that
+    // takes two LinkProperties objects and returns a ProvisioningChange
+    // object that is a correct and complete assessment of what changed, taking
+    // account of the asymmetries described in the comments in this function.
+    // Then switch to using it everywhere (IpReachabilityMonitor, etc.).
+    private int compareProvisioning(LinkProperties oldLp, LinkProperties newLp) {
+        int delta;
+        InitialConfiguration config = mConfiguration != null ? mConfiguration.mInitialConfig : null;
+        final boolean wasProvisioned = isProvisioned(oldLp, config);
+        final boolean isProvisioned = isProvisioned(newLp, config);
+
+        if (!wasProvisioned && isProvisioned) {
+            delta = PROV_CHANGE_GAINED_PROVISIONING;
+        } else if (wasProvisioned && isProvisioned) {
+            delta = PROV_CHANGE_STILL_PROVISIONED;
+        } else if (!wasProvisioned && !isProvisioned) {
+            delta = PROV_CHANGE_STILL_NOT_PROVISIONED;
+        } else {
+            // (wasProvisioned && !isProvisioned)
+            //
+            // Note that this is true even if we lose a configuration element
+            // (e.g., a default gateway) that would not be required to advance
+            // into provisioned state. This is intended: if we have a default
+            // router and we lose it, that's a sure sign of a problem, but if
+            // we connect to a network with no IPv4 DNS servers, we consider
+            // that to be a network without DNS servers and connect anyway.
+            //
+            // See the comment below.
+            delta = PROV_CHANGE_LOST_PROVISIONING;
+        }
+
+        final boolean lostIPv6 = oldLp.isIPv6Provisioned() && !newLp.isIPv6Provisioned();
+        final boolean lostIPv4Address = oldLp.hasIPv4Address() && !newLp.hasIPv4Address();
+        final boolean lostIPv6Router = oldLp.hasIPv6DefaultRoute() && !newLp.hasIPv6DefaultRoute();
+
+        // If bad wifi avoidance is disabled, then ignore IPv6 loss of
+        // provisioning. Otherwise, when a hotspot that loses Internet
+        // access sends out a 0-lifetime RA to its clients, the clients
+        // will disconnect and then reconnect, avoiding the bad hotspot,
+        // instead of getting stuck on the bad hotspot. http://b/31827713 .
+        //
+        // This is incorrect because if the hotspot then regains Internet
+        // access with a different prefix, TCP connections on the
+        // deprecated addresses will remain stuck.
+        //
+        // Note that we can still be disconnected by IpReachabilityMonitor
+        // if the IPv6 default gateway (but not the IPv6 DNS servers; see
+        // accompanying code in IpReachabilityMonitor) is unreachable.
+        final boolean ignoreIPv6ProvisioningLoss =
+                mConfiguration != null && mConfiguration.mUsingMultinetworkPolicyTracker
+                && mCm.getAvoidBadWifi();
+
+        // Additionally:
+        //
+        // Partial configurations (e.g., only an IPv4 address with no DNS
+        // servers and no default route) are accepted as long as DHCPv4
+        // succeeds. On such a network, isProvisioned() will always return
+        // false, because the configuration is not complete, but we want to
+        // connect anyway. It might be a disconnected network such as a
+        // Chromecast or a wireless printer, for example.
+        //
+        // Because on such a network isProvisioned() will always return false,
+        // delta will never be LOST_PROVISIONING. So check for loss of
+        // provisioning here too.
+        if (lostIPv4Address || (lostIPv6 && !ignoreIPv6ProvisioningLoss)) {
+            delta = PROV_CHANGE_LOST_PROVISIONING;
+        }
+
+        // Additionally:
+        //
+        // If the previous link properties had a global IPv6 address and an
+        // IPv6 default route then also consider the loss of that default route
+        // to be a loss of provisioning. See b/27962810.
+        if (oldLp.hasGlobalIPv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) {
+            delta = PROV_CHANGE_LOST_PROVISIONING;
+        }
+
+        return delta;
+    }
+
+    private void dispatchCallback(int delta, LinkProperties newLp) {
+        switch (delta) {
+            case PROV_CHANGE_GAINED_PROVISIONING:
+                if (DBG) {
+                    Log.d(mTag, "onProvisioningSuccess()");
+                }
+                recordMetric(IpManagerEvent.PROVISIONING_OK);
+                mCallback.onProvisioningSuccess(newLp);
+                break;
+
+            case PROV_CHANGE_LOST_PROVISIONING:
+                if (DBG) {
+                    Log.d(mTag, "onProvisioningFailure()");
+                }
+                recordMetric(IpManagerEvent.PROVISIONING_FAIL);
+                mCallback.onProvisioningFailure(newLp);
+                break;
+
+            default:
+                if (DBG) {
+                    Log.d(mTag, "onLinkPropertiesChange()");
+                }
+                mCallback.onLinkPropertiesChange(newLp);
+                break;
+        }
+    }
+
+    // Updates all IpClient-related state concerned with LinkProperties.
+    // Returns a ProvisioningChange for possibly notifying other interested
+    // parties that are not fronted by IpClient.
+    private int setLinkProperties(LinkProperties newLp) {
+        if (mApfFilter != null) {
+            mApfFilter.setLinkProperties(newLp);
+        }
+        if (mIpReachabilityMonitor != null) {
+            mIpReachabilityMonitor.updateLinkProperties(newLp);
+        }
+
+        int delta = compareProvisioning(mLinkProperties, newLp);
+        mLinkProperties = new LinkProperties(newLp);
+
+        if (delta == PROV_CHANGE_GAINED_PROVISIONING) {
+            // TODO: Add a proper ProvisionedState and cancel the alarm in
+            // its enter() method.
+            mProvisioningTimeoutAlarm.cancel();
+        }
+
+        return delta;
+    }
+
+    private LinkProperties assembleLinkProperties() {
+        // [1] Create a new LinkProperties object to populate.
+        LinkProperties newLp = new LinkProperties();
+        newLp.setInterfaceName(mInterfaceName);
+
+        // [2] Pull in data from netlink:
+        //         - IPv4 addresses
+        //         - IPv6 addresses
+        //         - IPv6 routes
+        //         - IPv6 DNS servers
+        //
+        // N.B.: this is fundamentally race-prone and should be fixed by
+        // changing NetlinkTracker from a hybrid edge/level model to an
+        // edge-only model, or by giving IpClient its own netlink socket(s)
+        // so as to track all required information directly.
+        LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties();
+        newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses());
+        for (RouteInfo route : netlinkLinkProperties.getRoutes()) {
+            newLp.addRoute(route);
+        }
+        addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers());
+
+        // [3] Add in data from DHCPv4, if available.
+        //
+        // mDhcpResults is never shared with any other owner so we don't have
+        // to worry about concurrent modification.
+        if (mDhcpResults != null) {
+            for (RouteInfo route : mDhcpResults.getRoutes(mInterfaceName)) {
+                newLp.addRoute(route);
+            }
+            addAllReachableDnsServers(newLp, mDhcpResults.dnsServers);
+            newLp.setDomains(mDhcpResults.domains);
+
+            if (mDhcpResults.mtu != 0) {
+                newLp.setMtu(mDhcpResults.mtu);
+            }
+        }
+
+        // [4] Add in TCP buffer sizes and HTTP Proxy config, if available.
+        if (!TextUtils.isEmpty(mTcpBufferSizes)) {
+            newLp.setTcpBufferSizes(mTcpBufferSizes);
+        }
+        if (mHttpProxy != null) {
+            newLp.setHttpProxy(mHttpProxy);
+        }
+
+        // [5] Add data from InitialConfiguration
+        if (mConfiguration != null && mConfiguration.mInitialConfig != null) {
+            InitialConfiguration config = mConfiguration.mInitialConfig;
+            // Add InitialConfiguration routes and dns server addresses once all addresses
+            // specified in the InitialConfiguration have been observed with Netlink.
+            if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) {
+                for (IpPrefix prefix : config.directlyConnectedRoutes) {
+                    newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName));
+                }
+            }
+            addAllReachableDnsServers(newLp, config.dnsServers);
+        }
+        final LinkProperties oldLp = mLinkProperties;
+        if (DBG) {
+            Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s",
+                    netlinkLinkProperties, newLp, oldLp));
+        }
+
+        // TODO: also learn via netlink routes specified by an InitialConfiguration and specified
+        // from a static IP v4 config instead of manually patching them in in steps [3] and [5].
+        return newLp;
+    }
+
+    private static void addAllReachableDnsServers(
+            LinkProperties lp, Iterable<InetAddress> dnses) {
+        // TODO: Investigate deleting this reachability check.  We should be
+        // able to pass everything down to netd and let netd do evaluation
+        // and RFC6724-style sorting.
+        for (InetAddress dns : dnses) {
+            if (!dns.isAnyLocalAddress() && lp.isReachable(dns)) {
+                lp.addDnsServer(dns);
+            }
+        }
+    }
+
+    // Returns false if we have lost provisioning, true otherwise.
+    private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
+        final LinkProperties newLp = assembleLinkProperties();
+        if (Objects.equals(newLp, mLinkProperties)) {
+            return true;
+        }
+        final int delta = setLinkProperties(newLp);
+        if (sendCallbacks) {
+            dispatchCallback(delta, newLp);
+        }
+        return (delta != PROV_CHANGE_LOST_PROVISIONING);
+    }
+
+    private void handleIPv4Success(DhcpResults dhcpResults) {
+        mDhcpResults = new DhcpResults(dhcpResults);
+        final LinkProperties newLp = assembleLinkProperties();
+        final int delta = setLinkProperties(newLp);
+
+        if (DBG) {
+            Log.d(mTag, "onNewDhcpResults(" + Objects.toString(dhcpResults) + ")");
+        }
+        mCallback.onNewDhcpResults(dhcpResults);
+        dispatchCallback(delta, newLp);
+    }
+
+    private void handleIPv4Failure() {
+        // TODO: Investigate deleting this clearIPv4Address() call.
+        //
+        // DhcpClient will send us CMD_CLEAR_LINKADDRESS in all circumstances
+        // that could trigger a call to this function. If we missed handling
+        // that message in StartedState for some reason we would still clear
+        // any addresses upon entry to StoppedState.
+        mInterfaceCtrl.clearIPv4Address();
+        mDhcpResults = null;
+        if (DBG) {
+            Log.d(mTag, "onNewDhcpResults(null)");
+        }
+        mCallback.onNewDhcpResults(null);
+
+        handleProvisioningFailure();
+    }
+
+    private void handleProvisioningFailure() {
+        final LinkProperties newLp = assembleLinkProperties();
+        int delta = setLinkProperties(newLp);
+        // If we've gotten here and we're still not provisioned treat that as
+        // a total loss of provisioning.
+        //
+        // Either (a) static IP configuration failed or (b) DHCPv4 failed AND
+        // there was no usable IPv6 obtained before a non-zero provisioning
+        // timeout expired.
+        //
+        // Regardless: GAME OVER.
+        if (delta == PROV_CHANGE_STILL_NOT_PROVISIONED) {
+            delta = PROV_CHANGE_LOST_PROVISIONING;
+        }
+
+        dispatchCallback(delta, newLp);
+        if (delta == PROV_CHANGE_LOST_PROVISIONING) {
+            transitionTo(mStoppingState);
+        }
+    }
+
+    private void doImmediateProvisioningFailure(int failureType) {
+        logError("onProvisioningFailure(): %s", failureType);
+        recordMetric(failureType);
+        mCallback.onProvisioningFailure(new LinkProperties(mLinkProperties));
+    }
+
+    private boolean startIPv4() {
+        // If we have a StaticIpConfiguration attempt to apply it and
+        // handle the result accordingly.
+        if (mConfiguration.mStaticIpConfig != null) {
+            if (mInterfaceCtrl.setIPv4Address(mConfiguration.mStaticIpConfig.ipAddress)) {
+                handleIPv4Success(new DhcpResults(mConfiguration.mStaticIpConfig));
+            } else {
+                return false;
+            }
+        } else {
+            // Start DHCPv4.
+            mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpClient.this, mInterfaceParams);
+            mDhcpClient.registerForPreDhcpNotification();
+            mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP);
+        }
+
+        return true;
+    }
+
+    private boolean startIPv6() {
+        return mInterfaceCtrl.setIPv6PrivacyExtensions(true)
+                && mInterfaceCtrl.setIPv6AddrGenModeIfSupported(mConfiguration.mIPv6AddrGenMode)
+                && mInterfaceCtrl.enableIPv6();
+    }
+
+    private boolean applyInitialConfig(InitialConfiguration config) {
+        // TODO: also support specifying a static IPv4 configuration in InitialConfiguration.
+        for (LinkAddress addr : findAll(config.ipAddresses, LinkAddress::isIPv6)) {
+            if (!mInterfaceCtrl.addAddress(addr)) return false;
+        }
+
+        return true;
+    }
+
+    private boolean startIpReachabilityMonitor() {
+        try {
+            // TODO: Fetch these parameters from settings, and install a
+            // settings observer to watch for update and re-program these
+            // parameters (Q: is this level of dynamic updatability really
+            // necessary or does reading from settings at startup suffice?).
+            final int numSolicits = 5;
+            final int interSolicitIntervalMs = 750;
+            setNeighborParameters(mDependencies.getNetd(), mInterfaceName,
+                    numSolicits, interSolicitIntervalMs);
+        } catch (Exception e) {
+            mLog.e("Failed to adjust neighbor parameters", e);
+            // Carry on using the system defaults (currently: 3, 1000);
+        }
+
+        try {
+            mIpReachabilityMonitor = new IpReachabilityMonitor(
+                    mContext,
+                    mInterfaceParams,
+                    getHandler(),
+                    mLog,
+                    new IpReachabilityMonitor.Callback() {
+                        @Override
+                        public void notifyLost(InetAddress ip, String logMsg) {
+                            mCallback.onReachabilityLost(logMsg);
+                        }
+                    },
+                    mConfiguration.mUsingMultinetworkPolicyTracker);
+        } catch (IllegalArgumentException iae) {
+            // Failed to start IpReachabilityMonitor. Log it and call
+            // onProvisioningFailure() immediately.
+            //
+            // See http://b/31038971.
+            logError("IpReachabilityMonitor failure: %s", iae);
+            mIpReachabilityMonitor = null;
+        }
+
+        return (mIpReachabilityMonitor != null);
+    }
+
+    private void stopAllIP() {
+        // We don't need to worry about routes, just addresses, because:
+        //     - disableIpv6() will clear autoconf IPv6 routes as well, and
+        //     - we don't get IPv4 routes from netlink
+        // so we neither react to nor need to wait for changes in either.
+
+        mInterfaceCtrl.disableIPv6();
+        mInterfaceCtrl.clearAllAddresses();
+    }
+
+    class StoppedState extends State {
+        @Override
+        public void enter() {
+            stopAllIP();
+
+            resetLinkProperties();
+            if (mStartTimeMillis > 0) {
+                // Completed a life-cycle; send a final empty LinkProperties
+                // (cleared in resetLinkProperties() above) and record an event.
+                mCallback.onLinkPropertiesChange(new LinkProperties(mLinkProperties));
+                recordMetric(IpManagerEvent.COMPLETE_LIFECYCLE);
+                mStartTimeMillis = 0;
+            }
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_TERMINATE_AFTER_STOP:
+                    stopStateMachineUpdaters();
+                    quit();
+                    break;
+
+                case CMD_STOP:
+                    break;
+
+                case CMD_START:
+                    mConfiguration = (android.net.shared.ProvisioningConfiguration) msg.obj;
+                    transitionTo(mStartedState);
+                    break;
+
+                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
+                    handleLinkPropertiesUpdate(NO_CALLBACKS);
+                    break;
+
+                case CMD_UPDATE_TCP_BUFFER_SIZES:
+                    mTcpBufferSizes = (String) msg.obj;
+                    handleLinkPropertiesUpdate(NO_CALLBACKS);
+                    break;
+
+                case CMD_UPDATE_HTTP_PROXY:
+                    mHttpProxy = (ProxyInfo) msg.obj;
+                    handleLinkPropertiesUpdate(NO_CALLBACKS);
+                    break;
+
+                case CMD_SET_MULTICAST_FILTER:
+                    mMulticastFiltering = (boolean) msg.obj;
+                    break;
+
+                case DhcpClient.CMD_ON_QUIT:
+                    // Everything is already stopped.
+                    logError("Unexpected CMD_ON_QUIT (already stopped).");
+                    break;
+
+                default:
+                    return NOT_HANDLED;
+            }
+
+            mMsgStateLogger.handled(this, getCurrentState());
+            return HANDLED;
+        }
+    }
+
+    class StoppingState extends State {
+        @Override
+        public void enter() {
+            if (mDhcpClient == null) {
+                // There's no DHCPv4 for which to wait; proceed to stopped.
+                deferMessage(obtainMessage(CMD_JUMP_STOPPING_TO_STOPPED));
+            }
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_JUMP_STOPPING_TO_STOPPED:
+                    transitionTo(mStoppedState);
+                    break;
+
+                case CMD_STOP:
+                    break;
+
+                case DhcpClient.CMD_CLEAR_LINKADDRESS:
+                    mInterfaceCtrl.clearIPv4Address();
+                    break;
+
+                case DhcpClient.CMD_ON_QUIT:
+                    mDhcpClient = null;
+                    transitionTo(mStoppedState);
+                    break;
+
+                default:
+                    deferMessage(msg);
+            }
+
+            mMsgStateLogger.handled(this, getCurrentState());
+            return HANDLED;
+        }
+    }
+
+    class StartedState extends State {
+        @Override
+        public void enter() {
+            mStartTimeMillis = SystemClock.elapsedRealtime();
+
+            if (mConfiguration.mProvisioningTimeoutMs > 0) {
+                final long alarmTime = SystemClock.elapsedRealtime()
+                        + mConfiguration.mProvisioningTimeoutMs;
+                mProvisioningTimeoutAlarm.schedule(alarmTime);
+            }
+
+            if (readyToProceed()) {
+                deferMessage(obtainMessage(CMD_JUMP_STARTED_TO_RUNNING));
+            } else {
+                // Clear all IPv4 and IPv6 before proceeding to RunningState.
+                // Clean up any leftover state from an abnormal exit from
+                // tethering or during an IpClient restart.
+                stopAllIP();
+            }
+        }
+
+        @Override
+        public void exit() {
+            mProvisioningTimeoutAlarm.cancel();
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_JUMP_STARTED_TO_RUNNING:
+                    transitionTo(mRunningState);
+                    break;
+
+                case CMD_STOP:
+                    transitionTo(mStoppingState);
+                    break;
+
+                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
+                    handleLinkPropertiesUpdate(NO_CALLBACKS);
+                    if (readyToProceed()) {
+                        transitionTo(mRunningState);
+                    }
+                    break;
+
+                case EVENT_PROVISIONING_TIMEOUT:
+                    handleProvisioningFailure();
+                    break;
+
+                default:
+                    // It's safe to process messages out of order because the
+                    // only message that can both
+                    //     a) be received at this time and
+                    //     b) affect provisioning state
+                    // is EVENT_NETLINK_LINKPROPERTIES_CHANGED (handled above).
+                    deferMessage(msg);
+            }
+
+            mMsgStateLogger.handled(this, getCurrentState());
+            return HANDLED;
+        }
+
+        private boolean readyToProceed() {
+            return (!mLinkProperties.hasIPv4Address() && !mLinkProperties.hasGlobalIPv6Address());
+        }
+    }
+
+    class RunningState extends State {
+        private ConnectivityPacketTracker mPacketTracker;
+        private boolean mDhcpActionInFlight;
+
+        @Override
+        public void enter() {
+            ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration();
+            apfConfig.apfCapabilities = mConfiguration.mApfCapabilities;
+            apfConfig.multicastFilter = mMulticastFiltering;
+            // Get the Configuration for ApfFilter from Context
+            apfConfig.ieee802_3Filter =
+                    mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames);
+            apfConfig.ethTypeBlackList =
+                    mContext.getResources().getIntArray(R.array.config_apfEthTypeBlackList);
+            mApfFilter = ApfFilter.maybeCreate(mContext, apfConfig, mInterfaceParams, mCallback);
+            // TODO: investigate the effects of any multicast filtering racing/interfering with the
+            // rest of this IP configuration startup.
+            if (mApfFilter == null) {
+                mCallback.setFallbackMulticastFilter(mMulticastFiltering);
+            }
+
+            mPacketTracker = createPacketTracker();
+            if (mPacketTracker != null) mPacketTracker.start(mConfiguration.mDisplayName);
+
+            if (mConfiguration.mEnableIPv6 && !startIPv6()) {
+                doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV6);
+                enqueueJumpToStoppingState();
+                return;
+            }
+
+            if (mConfiguration.mEnableIPv4 && !startIPv4()) {
+                doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV4);
+                enqueueJumpToStoppingState();
+                return;
+            }
+
+            final InitialConfiguration config = mConfiguration.mInitialConfig;
+            if ((config != null) && !applyInitialConfig(config)) {
+                // TODO introduce a new IpManagerEvent constant to distinguish this error case.
+                doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
+                enqueueJumpToStoppingState();
+                return;
+            }
+
+            if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) {
+                doImmediateProvisioningFailure(
+                        IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR);
+                enqueueJumpToStoppingState();
+                return;
+            }
+        }
+
+        @Override
+        public void exit() {
+            stopDhcpAction();
+
+            if (mIpReachabilityMonitor != null) {
+                mIpReachabilityMonitor.stop();
+                mIpReachabilityMonitor = null;
+            }
+
+            if (mDhcpClient != null) {
+                mDhcpClient.sendMessage(DhcpClient.CMD_STOP_DHCP);
+                mDhcpClient.doQuit();
+            }
+
+            if (mPacketTracker != null) {
+                mPacketTracker.stop();
+                mPacketTracker = null;
+            }
+
+            if (mApfFilter != null) {
+                mApfFilter.shutdown();
+                mApfFilter = null;
+            }
+
+            resetLinkProperties();
+        }
+
+        private void enqueueJumpToStoppingState() {
+            deferMessage(obtainMessage(CMD_JUMP_RUNNING_TO_STOPPING));
+        }
+
+        private ConnectivityPacketTracker createPacketTracker() {
+            try {
+                return new ConnectivityPacketTracker(
+                        getHandler(), mInterfaceParams, mConnectivityPacketLog);
+            } catch (IllegalArgumentException e) {
+                return null;
+            }
+        }
+
+        private void ensureDhcpAction() {
+            if (!mDhcpActionInFlight) {
+                mCallback.onPreDhcpAction();
+                mDhcpActionInFlight = true;
+                final long alarmTime = SystemClock.elapsedRealtime()
+                        + mConfiguration.mRequestedPreDhcpActionMs;
+                mDhcpActionTimeoutAlarm.schedule(alarmTime);
+            }
+        }
+
+        private void stopDhcpAction() {
+            mDhcpActionTimeoutAlarm.cancel();
+            if (mDhcpActionInFlight) {
+                mCallback.onPostDhcpAction();
+                mDhcpActionInFlight = false;
+            }
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_JUMP_RUNNING_TO_STOPPING:
+                case CMD_STOP:
+                    transitionTo(mStoppingState);
+                    break;
+
+                case CMD_START:
+                    logError("ALERT: START received in StartedState. Please fix caller.");
+                    break;
+
+                case CMD_CONFIRM:
+                    // TODO: Possibly introduce a second type of confirmation
+                    // that both probes (a) on-link neighbors and (b) does
+                    // a DHCPv4 RENEW.  We used to do this on Wi-Fi framework
+                    // roams.
+                    if (mIpReachabilityMonitor != null) {
+                        mIpReachabilityMonitor.probeAll();
+                    }
+                    break;
+
+                case EVENT_PRE_DHCP_ACTION_COMPLETE:
+                    // It's possible to reach here if, for example, someone
+                    // calls completedPreDhcpAction() after provisioning with
+                    // a static IP configuration.
+                    if (mDhcpClient != null) {
+                        mDhcpClient.sendMessage(DhcpClient.CMD_PRE_DHCP_ACTION_COMPLETE);
+                    }
+                    break;
+
+                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
+                    if (!handleLinkPropertiesUpdate(SEND_CALLBACKS)) {
+                        transitionTo(mStoppingState);
+                    }
+                    break;
+
+                case CMD_UPDATE_TCP_BUFFER_SIZES:
+                    mTcpBufferSizes = (String) msg.obj;
+                    // This cannot possibly change provisioning state.
+                    handleLinkPropertiesUpdate(SEND_CALLBACKS);
+                    break;
+
+                case CMD_UPDATE_HTTP_PROXY:
+                    mHttpProxy = (ProxyInfo) msg.obj;
+                    // This cannot possibly change provisioning state.
+                    handleLinkPropertiesUpdate(SEND_CALLBACKS);
+                    break;
+
+                case CMD_SET_MULTICAST_FILTER: {
+                    mMulticastFiltering = (boolean) msg.obj;
+                    if (mApfFilter != null) {
+                        mApfFilter.setMulticastFilter(mMulticastFiltering);
+                    } else {
+                        mCallback.setFallbackMulticastFilter(mMulticastFiltering);
+                    }
+                    break;
+                }
+
+                case EVENT_READ_PACKET_FILTER_COMPLETE: {
+                    if (mApfFilter != null) {
+                        mApfFilter.setDataSnapshot((byte[]) msg.obj);
+                    }
+                    mApfDataSnapshotComplete.open();
+                    break;
+                }
+
+                case EVENT_DHCPACTION_TIMEOUT:
+                    stopDhcpAction();
+                    break;
+
+                case DhcpClient.CMD_PRE_DHCP_ACTION:
+                    if (mConfiguration.mRequestedPreDhcpActionMs > 0) {
+                        ensureDhcpAction();
+                    } else {
+                        sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE);
+                    }
+                    break;
+
+                case DhcpClient.CMD_CLEAR_LINKADDRESS:
+                    mInterfaceCtrl.clearIPv4Address();
+                    break;
+
+                case DhcpClient.CMD_CONFIGURE_LINKADDRESS: {
+                    final LinkAddress ipAddress = (LinkAddress) msg.obj;
+                    if (mInterfaceCtrl.setIPv4Address(ipAddress)) {
+                        mDhcpClient.sendMessage(DhcpClient.EVENT_LINKADDRESS_CONFIGURED);
+                    } else {
+                        logError("Failed to set IPv4 address.");
+                        dispatchCallback(PROV_CHANGE_LOST_PROVISIONING,
+                                new LinkProperties(mLinkProperties));
+                        transitionTo(mStoppingState);
+                    }
+                    break;
+                }
+
+                // This message is only received when:
+                //
+                //     a) initial address acquisition succeeds,
+                //     b) renew succeeds or is NAK'd,
+                //     c) rebind succeeds or is NAK'd, or
+                //     c) the lease expires,
+                //
+                // but never when initial address acquisition fails. The latter
+                // condition is now governed by the provisioning timeout.
+                case DhcpClient.CMD_POST_DHCP_ACTION:
+                    stopDhcpAction();
+
+                    switch (msg.arg1) {
+                        case DhcpClient.DHCP_SUCCESS:
+                            handleIPv4Success((DhcpResults) msg.obj);
+                            break;
+                        case DhcpClient.DHCP_FAILURE:
+                            handleIPv4Failure();
+                            break;
+                        default:
+                            logError("Unknown CMD_POST_DHCP_ACTION status: %s", msg.arg1);
+                    }
+                    break;
+
+                case DhcpClient.CMD_ON_QUIT:
+                    // DHCPv4 quit early for some reason.
+                    logError("Unexpected CMD_ON_QUIT.");
+                    mDhcpClient = null;
+                    break;
+
+                default:
+                    return NOT_HANDLED;
+            }
+
+            mMsgStateLogger.handled(this, getCurrentState());
+            return HANDLED;
+        }
+    }
+
+    private static class MessageHandlingLogger {
+        public String processedInState;
+        public String receivedInState;
+
+        public void reset() {
+            processedInState = null;
+            receivedInState = null;
+        }
+
+        public void handled(State processedIn, IState receivedIn) {
+            processedInState = processedIn.getClass().getSimpleName();
+            receivedInState = receivedIn.getName();
+        }
+
+        public String toString() {
+            return String.format("rcvd_in=%s, proc_in=%s",
+                                 receivedInState, processedInState);
+        }
+    }
+
+    private static void setNeighborParameters(
+            INetd netd, String ifName, int numSolicits, int interSolicitIntervalMs)
+            throws RemoteException, IllegalArgumentException {
+        Preconditions.checkNotNull(netd);
+        Preconditions.checkArgument(!TextUtils.isEmpty(ifName));
+        Preconditions.checkArgument(numSolicits > 0);
+        Preconditions.checkArgument(interSolicitIntervalMs > 0);
+
+        for (int family : new Integer[]{INetd.IPV4, INetd.IPV6}) {
+            netd.setProcSysNet(family, INetd.NEIGH, ifName, "retrans_time_ms",
+                    Integer.toString(interSolicitIntervalMs));
+            netd.setProcSysNet(family, INetd.NEIGH, ifName, "ucast_solicit",
+                    Integer.toString(numSolicits));
+        }
+    }
+
+    // TODO: extract out into CollectionUtils.
+    static <T> boolean any(Iterable<T> coll, Predicate<T> fn) {
+        for (T t : coll) {
+            if (fn.test(t)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    static <T> boolean all(Iterable<T> coll, Predicate<T> fn) {
+        return !any(coll, not(fn));
+    }
+
+    static <T> Predicate<T> not(Predicate<T> fn) {
+        return (t) -> !fn.test(t);
+    }
+
+    static <T> String join(String delimiter, Collection<T> coll) {
+        return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter));
+    }
+
+    static <T> T find(Iterable<T> coll, Predicate<T> fn) {
+        for (T t: coll) {
+            if (fn.test(t)) {
+                return t;
+            }
+        }
+        return null;
+    }
+
+    static <T> List<T> findAll(Collection<T> coll, Predicate<T> fn) {
+        return coll.stream().filter(fn).collect(Collectors.toList());
+    }
+}
diff --git a/src/android/net/ip/IpNeighborMonitor.java b/src/android/net/ip/IpNeighborMonitor.java
new file mode 100644
index 0000000..eb993a4
--- /dev/null
+++ b/src/android/net/ip/IpNeighborMonitor.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2017 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 android.net.ip;
+
+import static android.net.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static android.net.netlink.NetlinkConstants.hexify;
+import static android.net.netlink.NetlinkConstants.stringForNlMsgType;
+import static android.net.util.SocketUtils.makeNetlinkSocketAddress;
+
+import android.net.MacAddress;
+import android.net.netlink.NetlinkErrorMessage;
+import android.net.netlink.NetlinkMessage;
+import android.net.netlink.NetlinkSocket;
+import android.net.netlink.RtNetlinkNeighborMessage;
+import android.net.netlink.StructNdMsg;
+import android.net.util.PacketReader;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.util.BitUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.StringJoiner;
+
+
+/**
+ * IpNeighborMonitor.
+ *
+ * Monitors the kernel rtnetlink neighbor notifications and presents to callers
+ * NeighborEvents describing each event. Callers can provide a consumer instance
+ * to both filter (e.g. by interface index and IP address) and handle the
+ * generated NeighborEvents.
+ *
+ * @hide
+ */
+public class IpNeighborMonitor extends PacketReader {
+    private static final String TAG = IpNeighborMonitor.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false;
+
+    /**
+     * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND)
+     * for the given IP address on the specified interface index.
+     *
+     * @return 0 if the request was successfully passed to the kernel; otherwise return
+     *         a non-zero error code.
+     */
+    public static int startKernelNeighborProbe(int ifIndex, InetAddress ip) {
+        final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex;
+        if (DBG) { Log.d(TAG, msgSnippet); }
+
+        final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage(
+                1, ip, StructNdMsg.NUD_PROBE, ifIndex, null);
+
+        try {
+            NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_ROUTE, msg);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Error " + msgSnippet + ": " + e);
+            return -e.errno;
+        }
+
+        return 0;
+    }
+
+    public static class NeighborEvent {
+        final long elapsedMs;
+        final short msgType;
+        final int ifindex;
+        final InetAddress ip;
+        final short nudState;
+        final MacAddress macAddr;
+
+        public NeighborEvent(long elapsedMs, short msgType, int ifindex, InetAddress ip,
+                short nudState, MacAddress macAddr) {
+            this.elapsedMs = elapsedMs;
+            this.msgType = msgType;
+            this.ifindex = ifindex;
+            this.ip = ip;
+            this.nudState = nudState;
+            this.macAddr = macAddr;
+        }
+
+        boolean isConnected() {
+            return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateConnected(nudState);
+        }
+
+        boolean isValid() {
+            return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateValid(nudState);
+        }
+
+        @Override
+        public String toString() {
+            final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
+            return j.add("@" + elapsedMs)
+                    .add(stringForNlMsgType(msgType))
+                    .add("if=" + ifindex)
+                    .add(ip.getHostAddress())
+                    .add(StructNdMsg.stringForNudState(nudState))
+                    .add("[" + macAddr + "]")
+                    .toString();
+        }
+    }
+
+    public interface NeighborEventConsumer {
+        // Every neighbor event received on the netlink socket is passed in
+        // here. Subclasses should filter for events of interest.
+        public void accept(NeighborEvent event);
+    }
+
+    private final SharedLog mLog;
+    private final NeighborEventConsumer mConsumer;
+
+    public IpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb) {
+        super(h, NetlinkSocket.DEFAULT_RECV_BUFSIZE);
+        mLog = log.forSubComponent(TAG);
+        mConsumer = (cb != null) ? cb : (event) -> { /* discard */ };
+    }
+
+    @Override
+    protected FileDescriptor createFd() {
+        FileDescriptor fd = null;
+
+        try {
+            fd = NetlinkSocket.forProto(OsConstants.NETLINK_ROUTE);
+            Os.bind(fd, makeNetlinkSocketAddress(0, OsConstants.RTMGRP_NEIGH));
+            NetlinkSocket.connectToKernel(fd);
+
+            if (VDBG) {
+                final SocketAddress nlAddr = Os.getsockname(fd);
+                Log.d(TAG, "bound to sockaddr_nl{" + nlAddr.toString() + "}");
+            }
+        } catch (ErrnoException|SocketException e) {
+            logError("Failed to create rtnetlink socket", e);
+            IoUtils.closeQuietly(fd);
+            return null;
+        }
+
+        return fd;
+    }
+
+    @Override
+    protected void handlePacket(byte[] recvbuf, int length) {
+        final long whenMs = SystemClock.elapsedRealtime();
+
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf, 0, length);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        parseNetlinkMessageBuffer(byteBuffer, whenMs);
+    }
+
+    private void parseNetlinkMessageBuffer(ByteBuffer byteBuffer, long whenMs) {
+        while (byteBuffer.remaining() > 0) {
+            final int position = byteBuffer.position();
+            final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer);
+            if (nlMsg == null || nlMsg.getHeader() == null) {
+                byteBuffer.position(position);
+                mLog.e("unparsable netlink msg: " + hexify(byteBuffer));
+                break;
+            }
+
+            final int srcPortId = nlMsg.getHeader().nlmsg_pid;
+            if (srcPortId !=  0) {
+                mLog.e("non-kernel source portId: " + BitUtils.uint32(srcPortId));
+                break;
+            }
+
+            if (nlMsg instanceof NetlinkErrorMessage) {
+                mLog.e("netlink error: " + nlMsg);
+                continue;
+            } else if (!(nlMsg instanceof RtNetlinkNeighborMessage)) {
+                mLog.i("non-rtnetlink neighbor msg: " + nlMsg);
+                continue;
+            }
+
+            evaluateRtNetlinkNeighborMessage((RtNetlinkNeighborMessage) nlMsg, whenMs);
+        }
+    }
+
+    private void evaluateRtNetlinkNeighborMessage(
+            RtNetlinkNeighborMessage neighMsg, long whenMs) {
+        final short msgType = neighMsg.getHeader().nlmsg_type;
+        final StructNdMsg ndMsg = neighMsg.getNdHeader();
+        if (ndMsg == null) {
+            mLog.e("RtNetlinkNeighborMessage without ND message header!");
+            return;
+        }
+
+        final int ifindex = ndMsg.ndm_ifindex;
+        final InetAddress destination = neighMsg.getDestination();
+        final short nudState =
+                (msgType == RTM_DELNEIGH)
+                ? StructNdMsg.NUD_NONE
+                : ndMsg.ndm_state;
+
+        final NeighborEvent event = new NeighborEvent(
+                whenMs, msgType, ifindex, destination, nudState,
+                getMacAddress(neighMsg.getLinkLayerAddress()));
+
+        if (VDBG) {
+            Log.d(TAG, neighMsg.toString());
+        }
+        if (DBG) {
+            Log.d(TAG, event.toString());
+        }
+
+        mConsumer.accept(event);
+    }
+
+    private static MacAddress getMacAddress(byte[] linkLayerAddress) {
+        if (linkLayerAddress != null) {
+            try {
+                return MacAddress.fromBytes(linkLayerAddress);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "Failed to parse link-layer address: " + hexify(linkLayerAddress));
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
new file mode 100644
index 0000000..761db68
--- /dev/null
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2015 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 android.net.ip;
+
+import static android.net.metrics.IpReachabilityEvent.NUD_FAILED;
+import static android.net.metrics.IpReachabilityEvent.NUD_FAILED_ORGANIC;
+import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST;
+import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST_ORGANIC;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+import android.net.ip.IpNeighborMonitor.NeighborEvent;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.IpReachabilityEvent;
+import android.net.netlink.StructNdMsg;
+import android.net.util.InterfaceParams;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.DumpUtils.Dump;
+
+import java.io.PrintWriter;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * IpReachabilityMonitor.
+ *
+ * Monitors on-link IP reachability and notifies callers whenever any on-link
+ * addresses of interest appear to have become unresponsive.
+ *
+ * This code does not concern itself with "why" a neighbour might have become
+ * unreachable. Instead, it primarily reacts to the kernel's notion of IP
+ * reachability for each of the neighbours we know to be critically important
+ * to normal network connectivity. As such, it is often "just the messenger":
+ * the neighbours about which it warns are already deemed by the kernel to have
+ * become unreachable.
+ *
+ *
+ * How it works:
+ *
+ *   1. The "on-link neighbours of interest" found in a given LinkProperties
+ *      instance are added to a "watch list" via #updateLinkProperties().
+ *      This usually means all default gateways and any on-link DNS servers.
+ *
+ *   2. We listen continuously for netlink neighbour messages (RTM_NEWNEIGH,
+ *      RTM_DELNEIGH), watching only for neighbours in the watch list.
+ *
+ *        - A neighbour going into NUD_REACHABLE, NUD_STALE, NUD_DELAY, and
+ *          even NUD_PROBE is perfectly normal; we merely record the new state.
+ *
+ *        - A neighbour's entry may be deleted (RTM_DELNEIGH), for example due
+ *          to garbage collection.  This is not necessarily of immediate
+ *          concern; we record the neighbour as moving to NUD_NONE.
+ *
+ *        - A neighbour transitioning to NUD_FAILED (for any reason) is
+ *          critically important and is handled as described below in #4.
+ *
+ *   3. All on-link neighbours in the watch list can be forcibly "probed" by
+ *      calling #probeAll(). This should be called whenever it is important to
+ *      verify that critical neighbours on the link are still reachable, e.g.
+ *      when roaming between BSSIDs.
+ *
+ *        - The kernel will send unicast ARP requests for IPv4 neighbours and
+ *          unicast NS packets for IPv6 neighbours.  The expected replies will
+ *          likely be unicast.
+ *
+ *        - The forced probing is done holding a wakelock. The kernel may,
+ *          however, initiate probing of a neighbor on its own, i.e. whenever
+ *          a neighbour has expired from NUD_DELAY.
+ *
+ *        - The kernel sends:
+ *
+ *              /proc/sys/net/ipv{4,6}/neigh/<ifname>/ucast_solicit
+ *
+ *          number of probes (usually 3) every:
+ *
+ *              /proc/sys/net/ipv{4,6}/neigh/<ifname>/retrans_time_ms
+ *
+ *          number of milliseconds (usually 1000ms). This normally results in
+ *          3 unicast packets, 1 per second.
+ *
+ *        - If no response is received to any of the probe packets, the kernel
+ *          marks the neighbour as being in state NUD_FAILED, and the listening
+ *          process in #2 will learn of it.
+ *
+ *   4. We call the supplied Callback#notifyLost() function if the loss of a
+ *      neighbour in NUD_FAILED would cause IPv4 or IPv6 configuration to
+ *      become incomplete (a loss of provisioning).
+ *
+ *        - For example, losing all our IPv4 on-link DNS servers (or losing
+ *          our only IPv6 default gateway) constitutes a loss of IPv4 (IPv6)
+ *          provisioning; Callback#notifyLost() would be called.
+ *
+ *        - Since it can be non-trivial to reacquire certain IP provisioning
+ *          state it may be best for the link to disconnect completely and
+ *          reconnect afresh.
+ *
+ * Accessing an instance of this class from multiple threads is NOT safe.
+ *
+ * @hide
+ */
+public class IpReachabilityMonitor {
+    private static final String TAG = "IpReachabilityMonitor";
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false;
+
+    public interface Callback {
+        // This callback function must execute as quickly as possible as it is
+        // run on the same thread that listens to kernel neighbor updates.
+        //
+        // TODO: refactor to something like notifyProvisioningLost(String msg).
+        public void notifyLost(InetAddress ip, String logMsg);
+    }
+
+    /**
+     * Encapsulates IpReachabilityMonitor depencencies on systems that hinder unit testing.
+     * TODO: consider also wrapping MultinetworkPolicyTracker in this interface.
+     */
+    interface Dependencies {
+        void acquireWakeLock(long durationMs);
+
+        static Dependencies makeDefault(Context context, String iface) {
+            final String lockName = TAG + "." + iface;
+            final PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+            final WakeLock lock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, lockName);
+
+            return new Dependencies() {
+                public void acquireWakeLock(long durationMs) {
+                    lock.acquire(durationMs);
+                }
+            };
+        }
+    }
+
+    private final InterfaceParams mInterfaceParams;
+    private final IpNeighborMonitor mIpNeighborMonitor;
+    private final SharedLog mLog;
+    private final Callback mCallback;
+    private final Dependencies mDependencies;
+    private final boolean mUsingMultinetworkPolicyTracker;
+    private final ConnectivityManager mCm;
+    private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
+    private LinkProperties mLinkProperties = new LinkProperties();
+    private Map<InetAddress, NeighborEvent> mNeighborWatchList = new HashMap<>();
+    // Time in milliseconds of the last forced probe request.
+    private volatile long mLastProbeTimeMs;
+
+    public IpReachabilityMonitor(
+            Context context, InterfaceParams ifParams, Handler h, SharedLog log, Callback callback,
+            boolean usingMultinetworkPolicyTracker) {
+        this(context, ifParams, h, log, callback, usingMultinetworkPolicyTracker,
+                Dependencies.makeDefault(context, ifParams.name));
+    }
+
+    @VisibleForTesting
+    IpReachabilityMonitor(Context context, InterfaceParams ifParams, Handler h, SharedLog log,
+            Callback callback, boolean usingMultinetworkPolicyTracker, Dependencies dependencies) {
+        if (ifParams == null) throw new IllegalArgumentException("null InterfaceParams");
+
+        mInterfaceParams = ifParams;
+        mLog = log.forSubComponent(TAG);
+        mCallback = callback;
+        mUsingMultinetworkPolicyTracker = usingMultinetworkPolicyTracker;
+        mCm = context.getSystemService(ConnectivityManager.class);
+        mDependencies = dependencies;
+
+        mIpNeighborMonitor = new IpNeighborMonitor(h, mLog,
+                (NeighborEvent event) -> {
+                    if (mInterfaceParams.index != event.ifindex) return;
+                    if (!mNeighborWatchList.containsKey(event.ip)) return;
+
+                    final NeighborEvent prev = mNeighborWatchList.put(event.ip, event);
+
+                    // TODO: Consider what to do with other states that are not within
+                    // NeighborEvent#isValid() (i.e. NUD_NONE, NUD_INCOMPLETE).
+                    if (event.nudState == StructNdMsg.NUD_FAILED) {
+                        mLog.w("ALERT neighbor went from: " + prev + " to: " + event);
+                        handleNeighborLost(event);
+                    }
+                });
+        mIpNeighborMonitor.start();
+    }
+
+    public void stop() {
+        mIpNeighborMonitor.stop();
+        clearLinkProperties();
+    }
+
+    public void dump(PrintWriter pw) {
+        DumpUtils.dumpAsync(
+                mIpNeighborMonitor.getHandler(),
+                new Dump() {
+                    @Override
+                    public void dump(PrintWriter pw, String prefix) {
+                        pw.println(describeWatchList("\n"));
+                    }
+                },
+                pw, "", 1000);
+    }
+
+    private String describeWatchList() { return describeWatchList(" "); }
+
+    private String describeWatchList(String sep) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("iface{" + mInterfaceParams + "}," + sep);
+        sb.append("ntable=[" + sep);
+        String delimiter = "";
+        for (Map.Entry<InetAddress, NeighborEvent> entry : mNeighborWatchList.entrySet()) {
+            sb.append(delimiter).append(entry.getKey().getHostAddress() + "/" + entry.getValue());
+            delimiter = "," + sep;
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+    private static boolean isOnLink(List<RouteInfo> routes, InetAddress ip) {
+        for (RouteInfo route : routes) {
+            if (!route.hasGateway() && route.matches(ip)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void updateLinkProperties(LinkProperties lp) {
+        if (!mInterfaceParams.name.equals(lp.getInterfaceName())) {
+            // TODO: figure out whether / how to cope with interface changes.
+            Log.wtf(TAG, "requested LinkProperties interface '" + lp.getInterfaceName() +
+                    "' does not match: " + mInterfaceParams.name);
+            return;
+        }
+
+        mLinkProperties = new LinkProperties(lp);
+        Map<InetAddress, NeighborEvent> newNeighborWatchList = new HashMap<>();
+
+        final List<RouteInfo> routes = mLinkProperties.getRoutes();
+        for (RouteInfo route : routes) {
+            if (route.hasGateway()) {
+                InetAddress gw = route.getGateway();
+                if (isOnLink(routes, gw)) {
+                    newNeighborWatchList.put(gw, mNeighborWatchList.getOrDefault(gw, null));
+                }
+            }
+        }
+
+        for (InetAddress dns : lp.getDnsServers()) {
+            if (isOnLink(routes, dns)) {
+                newNeighborWatchList.put(dns, mNeighborWatchList.getOrDefault(dns, null));
+            }
+        }
+
+        mNeighborWatchList = newNeighborWatchList;
+        if (DBG) { Log.d(TAG, "watch: " + describeWatchList()); }
+    }
+
+    public void clearLinkProperties() {
+        mLinkProperties.clear();
+        mNeighborWatchList.clear();
+        if (DBG) { Log.d(TAG, "clear: " + describeWatchList()); }
+    }
+
+    private void handleNeighborLost(NeighborEvent event) {
+        final LinkProperties whatIfLp = new LinkProperties(mLinkProperties);
+
+        InetAddress ip = null;
+        for (Map.Entry<InetAddress, NeighborEvent> entry : mNeighborWatchList.entrySet()) {
+            // TODO: Consider using NeighborEvent#isValid() here; it's more
+            // strict but may interact badly if other entries are somehow in
+            // NUD_INCOMPLETE (say, during network attach).
+            if (entry.getValue().nudState != StructNdMsg.NUD_FAILED) continue;
+
+            ip = entry.getKey();
+            for (RouteInfo route : mLinkProperties.getRoutes()) {
+                if (ip.equals(route.getGateway())) {
+                    whatIfLp.removeRoute(route);
+                }
+            }
+
+            if (avoidingBadLinks() || !(ip instanceof Inet6Address)) {
+                // We should do this unconditionally, but alas we cannot: b/31827713.
+                whatIfLp.removeDnsServer(ip);
+            }
+        }
+
+        final boolean lostProvisioning =
+                (mLinkProperties.isIPv4Provisioned() && !whatIfLp.isIPv4Provisioned())
+                || (mLinkProperties.isIPv6Provisioned() && !whatIfLp.isIPv6Provisioned());
+
+        if (lostProvisioning) {
+            final String logMsg = "FAILURE: LOST_PROVISIONING, " + event;
+            Log.w(TAG, logMsg);
+            if (mCallback != null) {
+                // TODO: remove |ip| when the callback signature no longer has
+                // an InetAddress argument.
+                mCallback.notifyLost(ip, logMsg);
+            }
+        }
+        logNudFailed(lostProvisioning);
+    }
+
+    private boolean avoidingBadLinks() {
+        return !mUsingMultinetworkPolicyTracker || mCm.getAvoidBadWifi();
+    }
+
+    public void probeAll() {
+        final List<InetAddress> ipProbeList = new ArrayList<>(mNeighborWatchList.keySet());
+
+        if (!ipProbeList.isEmpty()) {
+            // Keep the CPU awake long enough to allow all ARP/ND
+            // probes a reasonable chance at success. See b/23197666.
+            //
+            // The wakelock we use is (by default) refcounted, and this version
+            // of acquire(timeout) queues a release message to keep acquisitions
+            // and releases balanced.
+            mDependencies.acquireWakeLock(getProbeWakeLockDuration());
+        }
+
+        for (InetAddress ip : ipProbeList) {
+            final int rval = IpNeighborMonitor.startKernelNeighborProbe(mInterfaceParams.index, ip);
+            mLog.log(String.format("put neighbor %s into NUD_PROBE state (rval=%d)",
+                     ip.getHostAddress(), rval));
+            logEvent(IpReachabilityEvent.PROBE, rval);
+        }
+        mLastProbeTimeMs = SystemClock.elapsedRealtime();
+    }
+
+    private static long getProbeWakeLockDuration() {
+        // Ideally, this would be computed by examining the values of:
+        //
+        //     /proc/sys/net/ipv[46]/neigh/<ifname>/ucast_solicit
+        //
+        // and:
+        //
+        //     /proc/sys/net/ipv[46]/neigh/<ifname>/retrans_time_ms
+        //
+        // For now, just make some assumptions.
+        final long numUnicastProbes = 3;
+        final long retransTimeMs = 1000;
+        final long gracePeriodMs = 500;
+        return (numUnicastProbes * retransTimeMs) + gracePeriodMs;
+    }
+
+    private void logEvent(int probeType, int errorCode) {
+        int eventType = probeType | (errorCode & 0xff);
+        mMetricsLog.log(mInterfaceParams.name, new IpReachabilityEvent(eventType));
+    }
+
+    private void logNudFailed(boolean lostProvisioning) {
+        long duration = SystemClock.elapsedRealtime() - mLastProbeTimeMs;
+        boolean isFromProbe = (duration < getProbeWakeLockDuration());
+        int eventType = nudFailureEventType(isFromProbe, lostProvisioning);
+        mMetricsLog.log(mInterfaceParams.name, new IpReachabilityEvent(eventType));
+    }
+
+    /**
+     * Returns the NUD failure event type code corresponding to the given conditions.
+     */
+    private static int nudFailureEventType(boolean isFromProbe, boolean isProvisioningLost) {
+        if (isFromProbe) {
+            return isProvisioningLost ? PROVISIONING_LOST : NUD_FAILED;
+        } else {
+            return isProvisioningLost ? PROVISIONING_LOST_ORGANIC : NUD_FAILED_ORGANIC;
+        }
+    }
+}
diff --git a/src/android/net/util/ConnectivityPacketSummary.java b/src/android/net/util/ConnectivityPacketSummary.java
new file mode 100644
index 0000000..08c3f60
--- /dev/null
+++ b/src/android/net/util/ConnectivityPacketSummary.java
@@ -0,0 +1,435 @@
+/*
+ * 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 android.net.util;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.server.util.NetworkStackConstants.ARP_HWTYPE_ETHER;
+import static com.android.server.util.NetworkStackConstants.ARP_PAYLOAD_LEN;
+import static com.android.server.util.NetworkStackConstants.ARP_REPLY;
+import static com.android.server.util.NetworkStackConstants.ARP_REQUEST;
+import static com.android.server.util.NetworkStackConstants.DHCP4_CLIENT_PORT;
+import static com.android.server.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.server.util.NetworkStackConstants.ETHER_DST_ADDR_OFFSET;
+import static com.android.server.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.server.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET;
+import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_ARP;
+import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_IPV4;
+import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_HEADER_MIN_LEN;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_MIN_LENGTH;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.server.util.NetworkStackConstants.IPV4_DST_ADDR_OFFSET;
+import static com.android.server.util.NetworkStackConstants.IPV4_FLAGS_OFFSET;
+import static com.android.server.util.NetworkStackConstants.IPV4_FRAGMENT_MASK;
+import static com.android.server.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.server.util.NetworkStackConstants.IPV4_IHL_MASK;
+import static com.android.server.util.NetworkStackConstants.IPV4_PROTOCOL_OFFSET;
+import static com.android.server.util.NetworkStackConstants.IPV4_SRC_ADDR_OFFSET;
+import static com.android.server.util.NetworkStackConstants.IPV6_ADDR_LEN;
+import static com.android.server.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.server.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
+import static com.android.server.util.NetworkStackConstants.IPV6_SRC_ADDR_OFFSET;
+import static com.android.server.util.NetworkStackConstants.UDP_HEADER_LEN;
+
+import android.net.MacAddress;
+import android.net.dhcp.DhcpPacket;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.StringJoiner;
+
+
+/**
+ * Critical connectivity packet summarizing class.
+ *
+ * Outputs short descriptions of ARP, DHCPv4, and IPv6 RS/RA/NS/NA packets.
+ *
+ * @hide
+ */
+public class ConnectivityPacketSummary {
+    private static final String TAG = ConnectivityPacketSummary.class.getSimpleName();
+
+    private final byte[] mHwAddr;
+    private final byte[] mBytes;
+    private final int mLength;
+    private final ByteBuffer mPacket;
+    private final String mSummary;
+
+    public static String summarize(MacAddress hwaddr, byte[] buffer) {
+        return summarize(hwaddr, buffer, buffer.length);
+    }
+
+    // Methods called herein perform some but by no means all error checking.
+    // They may throw runtime exceptions on malformed packets.
+    public static String summarize(MacAddress macAddr, byte[] buffer, int length) {
+        if ((macAddr == null) || (buffer == null)) return null;
+        length = Math.min(length, buffer.length);
+        return (new ConnectivityPacketSummary(macAddr, buffer, length)).toString();
+    }
+
+    private ConnectivityPacketSummary(MacAddress macAddr, byte[] buffer, int length) {
+        mHwAddr = macAddr.toByteArray();
+        mBytes = buffer;
+        mLength = Math.min(length, mBytes.length);
+        mPacket = ByteBuffer.wrap(mBytes, 0, mLength);
+        mPacket.order(ByteOrder.BIG_ENDIAN);
+
+        final StringJoiner sj = new StringJoiner(" ");
+        // TODO: support other link-layers, or even no link-layer header.
+        parseEther(sj);
+        mSummary = sj.toString();
+    }
+
+    public String toString() {
+        return mSummary;
+    }
+
+    private void parseEther(StringJoiner sj) {
+        if (mPacket.remaining() < ETHER_HEADER_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        mPacket.position(ETHER_SRC_ADDR_OFFSET);
+        final ByteBuffer srcMac = (ByteBuffer) mPacket.slice().limit(ETHER_ADDR_LEN);
+        sj.add(ByteBuffer.wrap(mHwAddr).equals(srcMac) ? "TX" : "RX");
+        sj.add(getMacAddressString(srcMac));
+
+        mPacket.position(ETHER_DST_ADDR_OFFSET);
+        final ByteBuffer dstMac = (ByteBuffer) mPacket.slice().limit(ETHER_ADDR_LEN);
+        sj.add(">").add(getMacAddressString(dstMac));
+
+        mPacket.position(ETHER_TYPE_OFFSET);
+        final int etherType = asUint(mPacket.getShort());
+        switch (etherType) {
+            case ETHER_TYPE_ARP:
+                sj.add("arp");
+                parseARP(sj);
+                break;
+            case ETHER_TYPE_IPV4:
+                sj.add("ipv4");
+                parseIPv4(sj);
+                break;
+            case ETHER_TYPE_IPV6:
+                sj.add("ipv6");
+                parseIPv6(sj);
+                break;
+            default:
+                // Unknown ether type.
+                sj.add("ethtype").add(asString(etherType));
+                break;
+        }
+    }
+
+    private void parseARP(StringJoiner sj) {
+        if (mPacket.remaining() < ARP_PAYLOAD_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        if (asUint(mPacket.getShort()) != ARP_HWTYPE_ETHER ||
+            asUint(mPacket.getShort()) != ETHER_TYPE_IPV4 ||
+            asUint(mPacket.get()) != ETHER_ADDR_LEN ||
+            asUint(mPacket.get()) != IPV4_ADDR_LEN) {
+            sj.add("unexpected header");
+            return;
+        }
+
+        final int opCode = asUint(mPacket.getShort());
+
+        final String senderHwAddr = getMacAddressString(mPacket);
+        final String senderIPv4 = getIPv4AddressString(mPacket);
+        getMacAddressString(mPacket);  // target hardware address, unused
+        final String targetIPv4 = getIPv4AddressString(mPacket);
+
+        if (opCode == ARP_REQUEST) {
+            sj.add("who-has").add(targetIPv4);
+        } else if (opCode == ARP_REPLY) {
+            sj.add("reply").add(senderIPv4).add(senderHwAddr);
+        } else {
+            sj.add("unknown opcode").add(asString(opCode));
+        }
+    }
+
+    private void parseIPv4(StringJoiner sj) {
+        if (!mPacket.hasRemaining()) {
+            sj.add("runt");
+            return;
+        }
+
+        final int startOfIpLayer = mPacket.position();
+        final int ipv4HeaderLength = (mPacket.get(startOfIpLayer) & IPV4_IHL_MASK) * 4;
+        if (mPacket.remaining() < ipv4HeaderLength ||
+            mPacket.remaining() < IPV4_HEADER_MIN_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+        final int startOfTransportLayer = startOfIpLayer + ipv4HeaderLength;
+
+        mPacket.position(startOfIpLayer + IPV4_FLAGS_OFFSET);
+        final int flagsAndFragment = asUint(mPacket.getShort());
+        final boolean isFragment = (flagsAndFragment & IPV4_FRAGMENT_MASK) != 0;
+
+        mPacket.position(startOfIpLayer + IPV4_PROTOCOL_OFFSET);
+        final int protocol = asUint(mPacket.get());
+
+        mPacket.position(startOfIpLayer + IPV4_SRC_ADDR_OFFSET);
+        final String srcAddr = getIPv4AddressString(mPacket);
+
+        mPacket.position(startOfIpLayer + IPV4_DST_ADDR_OFFSET);
+        final String dstAddr = getIPv4AddressString(mPacket);
+
+        sj.add(srcAddr).add(">").add(dstAddr);
+
+        mPacket.position(startOfTransportLayer);
+        if (protocol == IPPROTO_UDP) {
+            sj.add("udp");
+            if (isFragment) sj.add("fragment");
+            else parseUDP(sj);
+        } else {
+            sj.add("proto").add(asString(protocol));
+            if (isFragment) sj.add("fragment");
+        }
+    }
+
+    private void parseIPv6(StringJoiner sj) {
+        if (mPacket.remaining() < IPV6_HEADER_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        final int startOfIpLayer = mPacket.position();
+
+        mPacket.position(startOfIpLayer + IPV6_PROTOCOL_OFFSET);
+        final int protocol = asUint(mPacket.get());
+
+        mPacket.position(startOfIpLayer + IPV6_SRC_ADDR_OFFSET);
+        final String srcAddr = getIPv6AddressString(mPacket);
+        final String dstAddr = getIPv6AddressString(mPacket);
+
+        sj.add(srcAddr).add(">").add(dstAddr);
+
+        mPacket.position(startOfIpLayer + IPV6_HEADER_LEN);
+        if (protocol == IPPROTO_ICMPV6) {
+            sj.add("icmp6");
+            parseICMPv6(sj);
+        } else {
+            sj.add("proto").add(asString(protocol));
+        }
+    }
+
+    private void parseICMPv6(StringJoiner sj) {
+        if (mPacket.remaining() < ICMPV6_HEADER_MIN_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        final int icmp6Type = asUint(mPacket.get());
+        final int icmp6Code = asUint(mPacket.get());
+        mPacket.getShort();  // checksum, unused
+
+        switch (icmp6Type) {
+            case ICMPV6_ROUTER_SOLICITATION:
+                sj.add("rs");
+                parseICMPv6RouterSolicitation(sj);
+                break;
+            case ICMPV6_ROUTER_ADVERTISEMENT:
+                sj.add("ra");
+                parseICMPv6RouterAdvertisement(sj);
+                break;
+            case ICMPV6_NEIGHBOR_SOLICITATION:
+                sj.add("ns");
+                parseICMPv6NeighborMessage(sj);
+                break;
+            case ICMPV6_NEIGHBOR_ADVERTISEMENT:
+                sj.add("na");
+                parseICMPv6NeighborMessage(sj);
+                break;
+            default:
+                sj.add("type").add(asString(icmp6Type));
+                sj.add("code").add(asString(icmp6Code));
+                break;
+        }
+    }
+
+    private void parseICMPv6RouterSolicitation(StringJoiner sj) {
+        final int RESERVED = 4;
+        if (mPacket.remaining() < RESERVED) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        mPacket.position(mPacket.position() + RESERVED);
+        parseICMPv6NeighborDiscoveryOptions(sj);
+    }
+
+    private void parseICMPv6RouterAdvertisement(StringJoiner sj) {
+        final int FLAGS_AND_TIMERS = 3 * 4;
+        if (mPacket.remaining() < FLAGS_AND_TIMERS) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        mPacket.position(mPacket.position() + FLAGS_AND_TIMERS);
+        parseICMPv6NeighborDiscoveryOptions(sj);
+    }
+
+    private void parseICMPv6NeighborMessage(StringJoiner sj) {
+        final int RESERVED = 4;
+        final int minReq = RESERVED + IPV6_ADDR_LEN;
+        if (mPacket.remaining() < minReq) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        mPacket.position(mPacket.position() + RESERVED);
+        sj.add(getIPv6AddressString(mPacket));
+        parseICMPv6NeighborDiscoveryOptions(sj);
+    }
+
+    private void parseICMPv6NeighborDiscoveryOptions(StringJoiner sj) {
+        // All ND options are TLV, where T is one byte and L is one byte equal
+        // to the length of T + L + V in units of 8 octets.
+        while (mPacket.remaining() >= ICMPV6_ND_OPTION_MIN_LENGTH) {
+            final int ndType = asUint(mPacket.get());
+            final int ndLength = asUint(mPacket.get());
+            final int ndBytes = ndLength * ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR - 2;
+            if (ndBytes < 0 || ndBytes > mPacket.remaining()) {
+                sj.add("<malformed>");
+                break;
+            }
+            final int position = mPacket.position();
+
+            switch (ndType) {
+                    case ICMPV6_ND_OPTION_SLLA:
+                        sj.add("slla");
+                        sj.add(getMacAddressString(mPacket));
+                        break;
+                    case ICMPV6_ND_OPTION_TLLA:
+                        sj.add("tlla");
+                        sj.add(getMacAddressString(mPacket));
+                        break;
+                    case ICMPV6_ND_OPTION_MTU:
+                        sj.add("mtu");
+                        final short reserved = mPacket.getShort();
+                        sj.add(asString(mPacket.getInt()));
+                        break;
+                    default:
+                        // Skip.
+                        break;
+            }
+
+            mPacket.position(position + ndBytes);
+        }
+    }
+
+    private void parseUDP(StringJoiner sj) {
+        if (mPacket.remaining() < UDP_HEADER_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        final int previous = mPacket.position();
+        final int srcPort = asUint(mPacket.getShort());
+        final int dstPort = asUint(mPacket.getShort());
+        sj.add(asString(srcPort)).add(">").add(asString(dstPort));
+
+        mPacket.position(previous + UDP_HEADER_LEN);
+        if (srcPort == DHCP4_CLIENT_PORT || dstPort == DHCP4_CLIENT_PORT) {
+            sj.add("dhcp4");
+            parseDHCPv4(sj);
+        }
+    }
+
+    private void parseDHCPv4(StringJoiner sj) {
+        final DhcpPacket dhcpPacket;
+        try {
+            dhcpPacket = DhcpPacket.decodeFullPacket(mBytes, mLength, DhcpPacket.ENCAP_L2);
+            sj.add(dhcpPacket.toString());
+        } catch (DhcpPacket.ParseException e) {
+            sj.add("parse error: " + e);
+        }
+    }
+
+    private static String getIPv4AddressString(ByteBuffer ipv4) {
+        return getIpAddressString(ipv4, IPV4_ADDR_LEN);
+    }
+
+    private static String getIPv6AddressString(ByteBuffer ipv6) {
+        return getIpAddressString(ipv6, IPV6_ADDR_LEN);
+    }
+
+    private static String getIpAddressString(ByteBuffer ip, int byteLength) {
+        if (ip == null || ip.remaining() < byteLength) return "invalid";
+
+        byte[] bytes = new byte[byteLength];
+        ip.get(bytes, 0, byteLength);
+        try {
+            InetAddress addr = InetAddress.getByAddress(bytes);
+            return addr.getHostAddress();
+        } catch (UnknownHostException uhe) {
+            return "unknown";
+        }
+    }
+
+    private static String getMacAddressString(ByteBuffer mac) {
+        if (mac == null || mac.remaining() < ETHER_ADDR_LEN) return "invalid";
+
+        byte[] bytes = new byte[ETHER_ADDR_LEN];
+        mac.get(bytes, 0, bytes.length);
+        Object[] printableBytes = new Object[bytes.length];
+        int i = 0;
+        for (byte b : bytes) printableBytes[i++] = new Byte(b);
+
+        final String MAC48_FORMAT = "%02x:%02x:%02x:%02x:%02x:%02x";
+        return String.format(MAC48_FORMAT, printableBytes);
+    }
+
+    /**
+     * Convenience method to convert an int to a String.
+     */
+    public static String asString(int i) {
+        return Integer.toString(i);
+    }
+
+    /**
+     * Convenience method to read a byte as an unsigned int.
+     */
+    public static int asUint(byte b) {
+        return (b & 0xff);
+    }
+
+    /**
+     * Convenience method to read a short as an unsigned int.
+     */
+    public static int asUint(short s) {
+        return (s & 0xffff);
+    }
+}
diff --git a/src/android/net/util/FdEventsReader.java b/src/android/net/util/FdEventsReader.java
new file mode 100644
index 0000000..8bbf449
--- /dev/null
+++ b/src/android/net/util/FdEventsReader.java
@@ -0,0 +1,254 @@
+/*
+ * 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 android.net.util;
+
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+
+
+/**
+ * This class encapsulates the mechanics of registering a file descriptor
+ * with a thread's Looper and handling read events (and errors).
+ *
+ * Subclasses MUST implement createFd() and SHOULD override handlePacket(). They MAY override
+ * onStop() and onStart().
+ *
+ * Subclasses can expect a call life-cycle like the following:
+ *
+ *     [1] when a client calls start(), createFd() is called, followed by the onStart() hook if all
+ *         goes well. Implementations may override onStart() for additional initialization.
+ *
+ *     [2] yield, waiting for read event or error notification:
+ *
+ *             [a] readPacket() && handlePacket()
+ *
+ *             [b] if (no error):
+ *                     goto 2
+ *                 else:
+ *                     goto 3
+ *
+ *     [3] when a client calls stop(), the onStop() hook is called (unless already stopped or never
+ *         started). Implementations may override onStop() for additional cleanup.
+ *
+ * The packet receive buffer is recycled on every read call, so subclasses
+ * should make any copies they would like inside their handlePacket()
+ * implementation.
+ *
+ * All public methods MUST only be called from the same thread with which
+ * the Handler constructor argument is associated.
+ *
+ * @hide
+ */
+public abstract class FdEventsReader<BufferType> {
+    private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+    private static final int UNREGISTER_THIS_FD = 0;
+
+    @NonNull
+    private final Handler mHandler;
+    @NonNull
+    private final MessageQueue mQueue;
+    @NonNull
+    private final BufferType mBuffer;
+    @Nullable
+    private FileDescriptor mFd;
+    private long mPacketsReceived;
+
+    protected static void closeFd(FileDescriptor fd) {
+        IoUtils.closeQuietly(fd);
+    }
+
+    protected FdEventsReader(@NonNull Handler h, @NonNull BufferType buffer) {
+        mHandler = h;
+        mQueue = mHandler.getLooper().getQueue();
+        mBuffer = buffer;
+    }
+
+    public void start() {
+        if (onCorrectThread()) {
+            createAndRegisterFd();
+        } else {
+            mHandler.post(() -> {
+                logError("start() called from off-thread", null);
+                createAndRegisterFd();
+            });
+        }
+    }
+
+    public void stop() {
+        if (onCorrectThread()) {
+            unregisterAndDestroyFd();
+        } else {
+            mHandler.post(() -> {
+                logError("stop() called from off-thread", null);
+                unregisterAndDestroyFd();
+            });
+        }
+    }
+
+    @NonNull
+    public Handler getHandler() { return mHandler; }
+
+    protected abstract int recvBufSize(@NonNull BufferType buffer);
+
+    public int recvBufSize() { return recvBufSize(mBuffer); }
+
+    /**
+     * Get the number of successful calls to {@link #readPacket(FileDescriptor, Object)}.
+     *
+     * <p>A call was successful if {@link #readPacket(FileDescriptor, Object)} returned a value > 0.
+     */
+    public final long numPacketsReceived() { return mPacketsReceived; }
+
+    /**
+     * Subclasses MUST create the listening socket here, including setting
+     * all desired socket options, interface or address/port binding, etc.
+     */
+    @Nullable
+    protected abstract FileDescriptor createFd();
+
+    /**
+     * Implementations MUST return the bytes read or throw an Exception.
+     *
+     * <p>The caller may throw a {@link ErrnoException} with {@link OsConstants#EAGAIN} or
+     * {@link OsConstants#EINTR}, in which case {@link FdEventsReader} will ignore the buffer
+     * contents and respectively wait for further input or retry the read immediately. For all other
+     * exceptions, the {@link FdEventsReader} will be stopped with no more interactions with this
+     * method.
+     */
+    protected abstract int readPacket(@NonNull FileDescriptor fd, @NonNull BufferType buffer)
+            throws Exception;
+
+    /**
+     * Called by the main loop for every packet.  Any desired copies of
+     * |recvbuf| should be made in here, as the underlying byte array is
+     * reused across all reads.
+     */
+    protected void handlePacket(@NonNull BufferType recvbuf, int length) {}
+
+    /**
+     * Called by the main loop to log errors.  In some cases |e| may be null.
+     */
+    protected void logError(@NonNull String msg, @Nullable Exception e) {}
+
+    /**
+     * Called by start(), if successful, just prior to returning.
+     */
+    protected void onStart() {}
+
+    /**
+     * Called by stop() just prior to returning.
+     */
+    protected void onStop() {}
+
+    private void createAndRegisterFd() {
+        if (mFd != null) return;
+
+        try {
+            mFd = createFd();
+            if (mFd != null) {
+                // Force the socket to be non-blocking.
+                IoUtils.setBlocking(mFd, false);
+            }
+        } catch (Exception e) {
+            logError("Failed to create socket: ", e);
+            closeFd(mFd);
+            mFd = null;
+        }
+
+        if (mFd == null) return;
+
+        mQueue.addOnFileDescriptorEventListener(
+                mFd,
+                FD_EVENTS,
+                (fd, events) -> {
+                    // Always call handleInput() so read/recvfrom are given
+                    // a proper chance to encounter a meaningful errno and
+                    // perhaps log a useful error message.
+                    if (!isRunning() || !handleInput()) {
+                        unregisterAndDestroyFd();
+                        return UNREGISTER_THIS_FD;
+                    }
+                    return FD_EVENTS;
+                });
+        onStart();
+    }
+
+    private boolean isRunning() { return (mFd != null) && mFd.valid(); }
+
+    // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error.
+    private boolean handleInput() {
+        while (isRunning()) {
+            final int bytesRead;
+
+            try {
+                bytesRead = readPacket(mFd, mBuffer);
+                if (bytesRead < 1) {
+                    if (isRunning()) logError("Socket closed, exiting", null);
+                    break;
+                }
+                mPacketsReceived++;
+            } catch (ErrnoException e) {
+                if (e.errno == OsConstants.EAGAIN) {
+                    // We've read everything there is to read this time around.
+                    return true;
+                } else if (e.errno == OsConstants.EINTR) {
+                    continue;
+                } else {
+                    if (isRunning()) logError("readPacket error: ", e);
+                    break;
+                }
+            } catch (Exception e) {
+                if (isRunning()) logError("readPacket error: ", e);
+                break;
+            }
+
+            try {
+                handlePacket(mBuffer, bytesRead);
+            } catch (Exception e) {
+                logError("handlePacket error: ", e);
+                break;
+            }
+        }
+
+        return false;
+    }
+
+    private void unregisterAndDestroyFd() {
+        if (mFd == null) return;
+
+        mQueue.removeOnFileDescriptorEventListener(mFd);
+        closeFd(mFd);
+        mFd = null;
+        onStop();
+    }
+
+    private boolean onCorrectThread() {
+        return (mHandler.getLooper() == Looper.myLooper());
+    }
+}
diff --git a/src/android/net/util/PacketReader.java b/src/android/net/util/PacketReader.java
new file mode 100644
index 0000000..4aec6b6
--- /dev/null
+++ b/src/android/net/util/PacketReader.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 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 android.net.util;
+
+import static java.lang.Math.max;
+
+import android.os.Handler;
+import android.system.Os;
+
+import java.io.FileDescriptor;
+
+/**
+ * Specialization of {@link FdEventsReader} that reads packets into a byte array.
+ *
+ * TODO: rename this class to something more correctly descriptive (something
+ * like [or less horrible than] FdReadEventsHandler?).
+ *
+ * @hide
+ */
+public abstract class PacketReader extends FdEventsReader<byte[]> {
+
+    public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024;
+
+    protected PacketReader(Handler h) {
+        this(h, DEFAULT_RECV_BUF_SIZE);
+    }
+
+    protected PacketReader(Handler h, int recvBufSize) {
+        super(h, new byte[max(recvBufSize, DEFAULT_RECV_BUF_SIZE)]);
+    }
+
+    @Override
+    protected final int recvBufSize(byte[] buffer) {
+        return buffer.length;
+    }
+
+    /**
+     * Subclasses MAY override this to change the default read() implementation
+     * in favour of, say, recvfrom().
+     *
+     * Implementations MUST return the bytes read or throw an Exception.
+     */
+    @Override
+    protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception {
+        return Os.read(fd, packetBuffer, 0, packetBuffer.length);
+    }
+}
diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java
index cca71e7..4080ddf 100644
--- a/src/com/android/server/NetworkStackService.java
+++ b/src/com/android/server/NetworkStackService.java
@@ -39,6 +39,8 @@
 import android.net.dhcp.DhcpServingParams;
 import android.net.dhcp.DhcpServingParamsParcel;
 import android.net.dhcp.IDhcpServerCallbacks;
+import android.net.ip.IIpClientCallbacks;
+import android.net.ip.IpClient;
 import android.net.shared.PrivateDnsConfig;
 import android.net.util.SharedLog;
 import android.os.IBinder;
@@ -50,7 +52,11 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
 
 /**
  * Android service used to start the network stack when bound to via an intent.
@@ -80,6 +86,8 @@
         private static final int NUM_VALIDATION_LOG_LINES = 20;
         private final Context mContext;
         private final ConnectivityManager mCm;
+        @GuardedBy("mIpClients")
+        private final ArrayList<WeakReference<IpClient>> mIpClients = new ArrayList<>();
 
         private static final int MAX_VALIDATION_LOGS = 10;
         @GuardedBy("mValidationLogs")
@@ -138,6 +146,24 @@
         }
 
         @Override
+        public void makeIpClient(String ifName, IIpClientCallbacks cb) throws RemoteException {
+            final IpClient ipClient = new IpClient(mContext, ifName, cb);
+
+            synchronized (mIpClients) {
+                final Iterator<WeakReference<IpClient>> it = mIpClients.iterator();
+                while (it.hasNext()) {
+                    final IpClient ipc = it.next().get();
+                    if (ipc == null) {
+                        it.remove();
+                    }
+                }
+                mIpClients.add(new WeakReference<>(ipClient));
+            }
+
+            cb.onIpClientCreated(ipClient.makeConnector());
+        }
+
+        @Override
         protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
                 @Nullable String[] args) {
             checkDumpPermission();
@@ -145,6 +171,33 @@
             pw.println("NetworkStack logs:");
             mLog.dump(fd, pw, args);
 
+            // Dump full IpClient logs for non-GCed clients
+            pw.println();
+            pw.println("Recently active IpClient logs:");
+            final ArrayList<IpClient> ipClients = new ArrayList<>();
+            final HashSet<String> dumpedIpClientIfaces = new HashSet<>();
+            synchronized (mIpClients) {
+                for (WeakReference<IpClient> ipcRef : mIpClients) {
+                    final IpClient ipc = ipcRef.get();
+                    if (ipc != null) {
+                        ipClients.add(ipc);
+                    }
+                }
+            }
+
+            for (IpClient ipc : ipClients) {
+                pw.println(ipc.getName());
+                pw.increaseIndent();
+                ipc.dump(fd, pw, args);
+                pw.decreaseIndent();
+                dumpedIpClientIfaces.add(ipc.getInterfaceName());
+            }
+
+            // State machine and connectivity metrics logs are kept for GCed IpClients
+            pw.println();
+            pw.println("Other IpClient logs:");
+            IpClient.dumpAllLogs(fout, dumpedIpClientIfaces);
+
             pw.println();
             pw.println("Validation logs (most recent first):");
             synchronized (mValidationLogs) {
diff --git a/src/com/android/server/util/NetworkStackConstants.java b/src/com/android/server/util/NetworkStackConstants.java
index bb5900c..eedaf30 100644
--- a/src/com/android/server/util/NetworkStackConstants.java
+++ b/src/com/android/server/util/NetworkStackConstants.java
@@ -32,12 +32,103 @@
     public static final int IPV4_MAX_MTU = 65_535;
 
     /**
+     * Ethernet constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc894
+     *     - https://tools.ietf.org/html/rfc2464
+     *     - https://tools.ietf.org/html/rfc7042
+     *     - http://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml
+     *     - http://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml
+     */
+    public static final int ETHER_DST_ADDR_OFFSET = 0;
+    public static final int ETHER_SRC_ADDR_OFFSET = 6;
+    public static final int ETHER_ADDR_LEN = 6;
+    public static final int ETHER_TYPE_OFFSET = 12;
+    public static final int ETHER_TYPE_LENGTH = 2;
+    public static final int ETHER_TYPE_ARP  = 0x0806;
+    public static final int ETHER_TYPE_IPV4 = 0x0800;
+    public static final int ETHER_TYPE_IPV6 = 0x86dd;
+    public static final int ETHER_HEADER_LEN = 14;
+
+    /**
+     * ARP constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc826
+     *     - http://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml
+     */
+    public static final int ARP_PAYLOAD_LEN = 28;  // For Ethernet+IPv4.
+    public static final int ARP_REQUEST = 1;
+    public static final int ARP_REPLY   = 2;
+    public static final int ARP_HWTYPE_RESERVED_LO = 0;
+    public static final int ARP_HWTYPE_ETHER       = 1;
+    public static final int ARP_HWTYPE_RESERVED_HI = 0xffff;
+
+    /**
+     * IPv4 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc791
+     */
+    public static final int IPV4_HEADER_MIN_LEN = 20;
+    public static final int IPV4_IHL_MASK = 0xf;
+    public static final int IPV4_FLAGS_OFFSET = 6;
+    public static final int IPV4_FRAGMENT_MASK = 0x1fff;
+    public static final int IPV4_PROTOCOL_OFFSET = 9;
+    public static final int IPV4_SRC_ADDR_OFFSET = 12;
+    public static final int IPV4_DST_ADDR_OFFSET = 16;
+    public static final int IPV4_ADDR_LEN = 4;
+
+    /**
+     * IPv6 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc2460
+     */
+    public static final int IPV6_ADDR_LEN = 16;
+    public static final int IPV6_HEADER_LEN = 40;
+    public static final int IPV6_PROTOCOL_OFFSET = 6;
+    public static final int IPV6_SRC_ADDR_OFFSET = 8;
+    public static final int IPV6_DST_ADDR_OFFSET = 24;
+
+    /**
+     * ICMPv6 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc4443
+     *     - https://tools.ietf.org/html/rfc4861
+     */
+    public static final int ICMPV6_HEADER_MIN_LEN = 4;
+    public static final int ICMPV6_ECHO_REPLY_TYPE = 129;
+    public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
+    public static final int ICMPV6_ROUTER_SOLICITATION    = 133;
+    public static final int ICMPV6_ROUTER_ADVERTISEMENT   = 134;
+    public static final int ICMPV6_NEIGHBOR_SOLICITATION  = 135;
+    public static final int ICMPV6_NEIGHBOR_ADVERTISEMENT = 136;
+    public static final int ICMPV6_ND_OPTION_MIN_LENGTH = 8;
+    public static final int ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR = 8;
+    public static final int ICMPV6_ND_OPTION_SLLA = 1;
+    public static final int ICMPV6_ND_OPTION_TLLA = 2;
+    public static final int ICMPV6_ND_OPTION_MTU  = 5;
+
+    /**
+     * UDP constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc768
+     */
+    public static final int UDP_HEADER_LEN = 8;
+
+
+    /**
      * DHCP constants.
      *
      * See also:
      *     - https://tools.ietf.org/html/rfc2131
      */
     public static final int INFINITE_LEASE = 0xffffffff;
+    public static final int DHCP4_CLIENT_PORT = 68;
 
     private NetworkStackConstants() {
         throw new UnsupportedOperationException("This class is not to be instantiated");
diff --git a/tests/Android.bp b/tests/Android.bp
index bd7ff2a..45fa2dc 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -16,9 +16,12 @@
 
 android_test {
     name: "NetworkStackTests",
+    certificate: "platform",
     srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
     static_libs: [
         "android-support-test",
+        "frameworks-base-testutils",
         "mockito-target-extended-minus-junit4",
         "NetworkStackLib",
         "testables",
@@ -26,10 +29,70 @@
     libs: [
         "android.test.runner",
         "android.test.base",
+        "android.test.mock",
     ],
     jni_libs: [
         // For mockito extended
         "libdexmakerjvmtiagent",
         "libstaticjvmtiagent",
-    ]
-}
\ No newline at end of file
+        // For ApfTest
+        "libartbase",
+        "libbacktrace",
+        "libbase",
+        "libbinder",
+        "libbinderthreadstate",
+        "libc++",
+        "libcrypto",
+        "libcutils",
+        "libdexfile",
+        "libhidl-gen-utils",
+        "libhidlbase",
+        "libhidltransport",
+        "libhwbinder",
+        "liblog",
+        "liblzma",
+        "libnativehelper",
+        "libnetworkstacktestsjni",
+        "libpackagelistparser",
+        "libpcre2",
+        "libprocessgroup",
+        "libselinux",
+        "libui",
+        "libutils",
+        "libvintf",
+        "libvndksupport",
+        "libtinyxml2",
+        "libunwindstack",
+        "libutilscallstack",
+        "libziparchive",
+        "libz",
+        "netd_aidl_interface-cpp",
+    ],
+}
+
+cc_library_shared {
+    name: "libnetworkstacktestsjni",
+    srcs: [
+        "jni/**/*.cpp"
+    ],
+    cflags: [
+        "-Wall",
+        "-Wextra",
+        "-Werror",
+    ],
+    include_dirs: [
+        "hardware/google/apf",
+    ],
+    shared_libs: [
+        "libbinder",
+        "liblog",
+        "libcutils",
+        "libnativehelper",
+        "netd_aidl_interface-cpp",
+    ],
+    static_libs: [
+        "libapf",
+        "libpcap",
+    ],
+
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 8b8474f..9cb2c21 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -15,6 +15,35 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.server.networkstack.tests">
+
+    <uses-permission android:name="android.permission.READ_LOGS" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.BROADCAST_STICKY" />
+    <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
+    <uses-permission android:name="android.permission.MANAGE_APP_TOKENS" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+    <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+    <uses-permission android:name="android.permission.GET_DETAILED_TASKS" />
+    <uses-permission android:name="android.permission.MANAGE_NETWORK_POLICY" />
+    <uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
+    <uses-permission android:name="android.permission.CONNECTIVITY_INTERNAL" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.MANAGE_USERS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+    <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
+    <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.PACKET_KEEPALIVE_OFFLOAD" />
+    <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" />
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" />
+    <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
+    <uses-permission android:name="android.permission.NETWORK_STACK" />
+
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/tests/jni/apf_jni.cpp b/tests/jni/apf_jni.cpp
new file mode 100644
index 0000000..4222adf
--- /dev/null
+++ b/tests/jni/apf_jni.cpp
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
+#include <jni.h>
+#include <pcap.h>
+#include <stdlib.h>
+#include <string>
+#include <utils/Log.h>
+#include <vector>
+
+#include "apf_interpreter.h"
+#include "nativehelper/scoped_primitive_array.h"
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+
+// JNI function acting as simply call-through to native APF interpreter.
+static jint com_android_server_ApfTest_apfSimulate(
+        JNIEnv* env, jclass, jbyteArray jprogram, jbyteArray jpacket,
+        jbyteArray jdata, jint filter_age) {
+
+    ScopedByteArrayRO packet(env, jpacket);
+    uint32_t packet_len = (uint32_t)packet.size();
+    uint32_t program_len = env->GetArrayLength(jprogram);
+    uint32_t data_len = jdata ? env->GetArrayLength(jdata) : 0;
+    std::vector<uint8_t> buf(program_len + data_len, 0);
+
+    env->GetByteArrayRegion(jprogram, 0, program_len, reinterpret_cast<jbyte*>(buf.data()));
+    if (jdata) {
+        // Merge program and data into a single buffer.
+        env->GetByteArrayRegion(jdata, 0, data_len,
+                                reinterpret_cast<jbyte*>(buf.data() + program_len));
+    }
+
+    jint result =
+        accept_packet(buf.data(), program_len, program_len + data_len,
+                        reinterpret_cast<const uint8_t*>(packet.get()), packet_len, filter_age);
+
+    if (jdata) {
+        env->SetByteArrayRegion(jdata, 0, data_len,
+                                reinterpret_cast<jbyte*>(buf.data() + program_len));
+    }
+
+    return result;
+}
+
+class ScopedPcap {
+  public:
+    explicit ScopedPcap(pcap_t* pcap) : pcap_ptr(pcap) {}
+    ~ScopedPcap() {
+        pcap_close(pcap_ptr);
+    }
+
+    pcap_t* get() const { return pcap_ptr; };
+  private:
+    pcap_t* const pcap_ptr;
+};
+
+class ScopedFILE {
+  public:
+    explicit ScopedFILE(FILE* fp) : file(fp) {}
+    ~ScopedFILE() {
+        fclose(file);
+    }
+
+    FILE* get() const { return file; };
+  private:
+    FILE* const file;
+};
+
+static void throwException(JNIEnv* env, const std::string& error) {
+    jclass newExcCls = env->FindClass("java/lang/IllegalStateException");
+    if (newExcCls == 0) {
+      abort();
+      return;
+    }
+    env->ThrowNew(newExcCls, error.c_str());
+}
+
+static jstring com_android_server_ApfTest_compileToBpf(JNIEnv* env, jclass, jstring jfilter) {
+    ScopedUtfChars filter(env, jfilter);
+    std::string bpf_string;
+    ScopedPcap pcap(pcap_open_dead(DLT_EN10MB, 65535));
+    if (pcap.get() == NULL) {
+        throwException(env, "pcap_open_dead failed");
+        return NULL;
+    }
+
+    // Compile "filter" to a BPF program
+    bpf_program bpf;
+    if (pcap_compile(pcap.get(), &bpf, filter.c_str(), 0, PCAP_NETMASK_UNKNOWN)) {
+        throwException(env, "pcap_compile failed");
+        return NULL;
+    }
+
+    // Translate BPF program to human-readable format
+    const struct bpf_insn* insn = bpf.bf_insns;
+    for (uint32_t i = 0; i < bpf.bf_len; i++) {
+        bpf_string += bpf_image(insn++, i);
+        bpf_string += "\n";
+    }
+
+    return env->NewStringUTF(bpf_string.c_str());
+}
+
+static jboolean com_android_server_ApfTest_compareBpfApf(JNIEnv* env, jclass, jstring jfilter,
+        jstring jpcap_filename, jbyteArray japf_program) {
+    ScopedUtfChars filter(env, jfilter);
+    ScopedUtfChars pcap_filename(env, jpcap_filename);
+    ScopedByteArrayRO apf_program(env, japf_program);
+
+    // Open pcap file for BPF filtering
+    ScopedFILE bpf_fp(fopen(pcap_filename.c_str(), "rb"));
+    char pcap_error[PCAP_ERRBUF_SIZE];
+    ScopedPcap bpf_pcap(pcap_fopen_offline(bpf_fp.get(), pcap_error));
+    if (bpf_pcap.get() == NULL) {
+        throwException(env, "pcap_fopen_offline failed: " + std::string(pcap_error));
+        return false;
+    }
+
+    // Open pcap file for APF filtering
+    ScopedFILE apf_fp(fopen(pcap_filename.c_str(), "rb"));
+    ScopedPcap apf_pcap(pcap_fopen_offline(apf_fp.get(), pcap_error));
+    if (apf_pcap.get() == NULL) {
+        throwException(env, "pcap_fopen_offline failed: " + std::string(pcap_error));
+        return false;
+    }
+
+    // Compile "filter" to a BPF program
+    bpf_program bpf;
+    if (pcap_compile(bpf_pcap.get(), &bpf, filter.c_str(), 0, PCAP_NETMASK_UNKNOWN)) {
+        throwException(env, "pcap_compile failed");
+        return false;
+    }
+
+    // Install BPF filter on bpf_pcap
+    if (pcap_setfilter(bpf_pcap.get(), &bpf)) {
+        throwException(env, "pcap_setfilter failed");
+        return false;
+    }
+
+    while (1) {
+        pcap_pkthdr bpf_header, apf_header;
+        // Run BPF filter to the next matching packet.
+        const uint8_t* bpf_packet = pcap_next(bpf_pcap.get(), &bpf_header);
+
+        // Run APF filter to the next matching packet.
+        const uint8_t* apf_packet;
+        do {
+            apf_packet = pcap_next(apf_pcap.get(), &apf_header);
+        } while (apf_packet != NULL && !accept_packet(
+                reinterpret_cast<uint8_t*>(const_cast<int8_t*>(apf_program.get())),
+                apf_program.size(), 0 /* data_len */,
+                apf_packet, apf_header.len, 0 /* filter_age */));
+
+        // Make sure both filters matched the same packet.
+        if (apf_packet == NULL && bpf_packet == NULL)
+            break;
+        if (apf_packet == NULL || bpf_packet == NULL)
+            return false;
+        if (apf_header.len != bpf_header.len ||
+                apf_header.ts.tv_sec != bpf_header.ts.tv_sec ||
+                apf_header.ts.tv_usec != bpf_header.ts.tv_usec ||
+                memcmp(apf_packet, bpf_packet, apf_header.len))
+            return false;
+    }
+    return true;
+}
+
+static jboolean com_android_server_ApfTest_dropsAllPackets(JNIEnv* env, jclass, jbyteArray jprogram,
+        jbyteArray jdata, jstring jpcap_filename) {
+    ScopedUtfChars pcap_filename(env, jpcap_filename);
+    ScopedByteArrayRO apf_program(env, jprogram);
+    uint32_t apf_program_len = (uint32_t)apf_program.size();
+    uint32_t data_len = env->GetArrayLength(jdata);
+    pcap_pkthdr apf_header;
+    const uint8_t* apf_packet;
+    char pcap_error[PCAP_ERRBUF_SIZE];
+    std::vector<uint8_t> buf(apf_program_len + data_len, 0);
+
+    // Merge program and data into a single buffer.
+    env->GetByteArrayRegion(jprogram, 0, apf_program_len, reinterpret_cast<jbyte*>(buf.data()));
+    env->GetByteArrayRegion(jdata, 0, data_len,
+                            reinterpret_cast<jbyte*>(buf.data() + apf_program_len));
+
+    // Open pcap file
+    ScopedFILE apf_fp(fopen(pcap_filename.c_str(), "rb"));
+    ScopedPcap apf_pcap(pcap_fopen_offline(apf_fp.get(), pcap_error));
+
+    if (apf_pcap.get() == NULL) {
+        throwException(env, "pcap_fopen_offline failed: " + std::string(pcap_error));
+        return false;
+    }
+
+    while ((apf_packet = pcap_next(apf_pcap.get(), &apf_header)) != NULL) {
+        int result = accept_packet(buf.data(), apf_program_len,
+                                    apf_program_len + data_len, apf_packet, apf_header.len, 0);
+
+        // Return false once packet passes the filter
+        if (result) {
+            env->SetByteArrayRegion(jdata, 0, data_len,
+                                    reinterpret_cast<jbyte*>(buf.data() + apf_program_len));
+            return false;
+         }
+    }
+
+    env->SetByteArrayRegion(jdata, 0, data_len,
+                            reinterpret_cast<jbyte*>(buf.data() + apf_program_len));
+    return true;
+}
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
+    JNIEnv *env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        ALOGE("ERROR: GetEnv failed");
+        return -1;
+    }
+
+    static JNINativeMethod gMethods[] = {
+            { "apfSimulate", "([B[B[BI)I",
+                    (void*)com_android_server_ApfTest_apfSimulate },
+            { "compileToBpf", "(Ljava/lang/String;)Ljava/lang/String;",
+                    (void*)com_android_server_ApfTest_compileToBpf },
+            { "compareBpfApf", "(Ljava/lang/String;Ljava/lang/String;[B)Z",
+                    (void*)com_android_server_ApfTest_compareBpfApf },
+            { "dropsAllPackets", "([B[BLjava/lang/String;)Z",
+                    (void*)com_android_server_ApfTest_dropsAllPackets },
+    };
+
+    jniRegisterNativeMethods(env, "android/net/apf/ApfTest",
+            gMethods, ARRAY_SIZE(gMethods));
+
+    return JNI_VERSION_1_6;
+}
diff --git a/tests/res/raw/apf.pcap b/tests/res/raw/apf.pcap
new file mode 100644
index 0000000..963165f
--- /dev/null
+++ b/tests/res/raw/apf.pcap
Binary files differ
diff --git a/tests/res/raw/apfPcap.pcap b/tests/res/raw/apfPcap.pcap
new file mode 100644
index 0000000..6f69c4a
--- /dev/null
+++ b/tests/res/raw/apfPcap.pcap
Binary files differ
diff --git a/tests/src/android/net/apf/ApfTest.java b/tests/src/android/net/apf/ApfTest.java
new file mode 100644
index 0000000..f76e412
--- /dev/null
+++ b/tests/src/android/net/apf/ApfTest.java
@@ -0,0 +1,1802 @@
+/*
+ * Copyright (C) 2012 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 android.net.apf;
+
+import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.ARPHRD_ETHER;
+import static android.system.OsConstants.ETH_P_ARP;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_STREAM;
+
+import static com.android.internal.util.BitUtils.bytesToBEInt;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.apf.ApfFilter.ApfConfiguration;
+import android.net.apf.ApfGenerator.IllegalInstructionException;
+import android.net.apf.ApfGenerator.Register;
+import android.net.ip.IIpClientCallbacks;
+import android.net.ip.IpClient;
+import android.net.ip.IpClient.IpClientCallbacksWrapper;
+import android.net.ip.IpClientCallbacks;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.RaEvent;
+import android.net.util.InterfaceParams;
+import android.net.util.SharedLog;
+import android.os.ConditionVariable;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+import com.android.internal.util.HexDump;
+import com.android.server.networkstack.tests.R;
+import com.android.server.util.NetworkStackConstants;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Tests for APF program generator and interpreter.
+ *
+ * Build, install and run with:
+ *  runtest frameworks-net -c android.net.apf.ApfTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ApfTest {
+    private static final int TIMEOUT_MS = 500;
+    private static final int MIN_APF_VERSION = 2;
+
+    @Mock IpConnectivityLog mLog;
+    @Mock Context mContext;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        // Load up native shared library containing APF interpreter exposed via JNI.
+        System.loadLibrary("networkstacktestsjni");
+    }
+
+    private static final String TAG = "ApfTest";
+    // Expected return codes from APF interpreter.
+    private static final int PASS = 1;
+    private static final int DROP = 0;
+    // Interpreter will just accept packets without link layer headers, so pad fake packet to at
+    // least the minimum packet size.
+    private static final int MIN_PKT_SIZE = 15;
+
+    private static final ApfCapabilities MOCK_APF_CAPABILITIES =
+      new ApfCapabilities(2, 1700, ARPHRD_ETHER);
+
+    private static final boolean DROP_MULTICAST = true;
+    private static final boolean ALLOW_MULTICAST = false;
+
+    private static final boolean DROP_802_3_FRAMES = true;
+    private static final boolean ALLOW_802_3_FRAMES = false;
+
+    // Constants for opcode encoding
+    private static final byte LI_OP   = (byte)(13 << 3);
+    private static final byte LDDW_OP = (byte)(22 << 3);
+    private static final byte STDW_OP = (byte)(23 << 3);
+    private static final byte SIZE0   = (byte)(0 << 1);
+    private static final byte SIZE8   = (byte)(1 << 1);
+    private static final byte SIZE16  = (byte)(2 << 1);
+    private static final byte SIZE32  = (byte)(3 << 1);
+    private static final byte R1 = 1;
+
+    private static ApfConfiguration getDefaultConfig() {
+        ApfFilter.ApfConfiguration config = new ApfConfiguration();
+        config.apfCapabilities = MOCK_APF_CAPABILITIES;
+        config.multicastFilter = ALLOW_MULTICAST;
+        config.ieee802_3Filter = ALLOW_802_3_FRAMES;
+        config.ethTypeBlackList = new int[0];
+        return config;
+    }
+
+    private static String label(int code) {
+        switch (code) {
+            case PASS: return "PASS";
+            case DROP: return "DROP";
+            default:   return "UNKNOWN";
+        }
+    }
+
+    private static void assertReturnCodesEqual(int expected, int got) {
+        assertEquals(label(expected), label(got));
+    }
+
+    private void assertVerdict(int expected, byte[] program, byte[] packet, int filterAge) {
+        assertReturnCodesEqual(expected, apfSimulate(program, packet, null, filterAge));
+    }
+
+    private void assertVerdict(int expected, byte[] program, byte[] packet) {
+        assertReturnCodesEqual(expected, apfSimulate(program, packet, null, 0));
+    }
+
+    private void assertPass(byte[] program, byte[] packet, int filterAge) {
+        assertVerdict(PASS, program, packet, filterAge);
+    }
+
+    private void assertPass(byte[] program, byte[] packet) {
+        assertVerdict(PASS, program, packet);
+    }
+
+    private void assertDrop(byte[] program, byte[] packet, int filterAge) {
+        assertVerdict(DROP, program, packet, filterAge);
+    }
+
+    private void assertDrop(byte[] program, byte[] packet) {
+        assertVerdict(DROP, program, packet);
+    }
+
+    private void assertProgramEquals(byte[] expected, byte[] program) throws AssertionError {
+        // assertArrayEquals() would only print one byte, making debugging difficult.
+        if (!java.util.Arrays.equals(expected, program)) {
+            throw new AssertionError(
+                    "\nexpected: " + HexDump.toHexString(expected) +
+                    "\nactual:   " + HexDump.toHexString(program));
+        }
+    }
+
+    private void assertDataMemoryContents(
+            int expected, byte[] program, byte[] packet, byte[] data, byte[] expected_data)
+            throws IllegalInstructionException, Exception {
+        assertReturnCodesEqual(expected, apfSimulate(program, packet, data, 0 /* filterAge */));
+
+        // assertArrayEquals() would only print one byte, making debugging difficult.
+        if (!java.util.Arrays.equals(expected_data, data)) {
+            throw new Exception(
+                    "\nprogram:     " + HexDump.toHexString(program) +
+                    "\ndata memory: " + HexDump.toHexString(data) +
+                    "\nexpected:    " + HexDump.toHexString(expected_data));
+        }
+    }
+
+    private void assertVerdict(int expected, ApfGenerator gen, byte[] packet, int filterAge)
+            throws IllegalInstructionException {
+        assertReturnCodesEqual(expected, apfSimulate(gen.generate(), packet, null,
+              filterAge));
+    }
+
+    private void assertPass(ApfGenerator gen, byte[] packet, int filterAge)
+            throws IllegalInstructionException {
+        assertVerdict(PASS, gen, packet, filterAge);
+    }
+
+    private void assertDrop(ApfGenerator gen, byte[] packet, int filterAge)
+            throws IllegalInstructionException {
+        assertVerdict(DROP, gen, packet, filterAge);
+    }
+
+    private void assertPass(ApfGenerator gen)
+            throws IllegalInstructionException {
+        assertVerdict(PASS, gen, new byte[MIN_PKT_SIZE], 0);
+    }
+
+    private void assertDrop(ApfGenerator gen)
+            throws IllegalInstructionException {
+        assertVerdict(DROP, gen, new byte[MIN_PKT_SIZE], 0);
+    }
+
+    /**
+     * Test each instruction by generating a program containing the instruction,
+     * generating bytecode for that program and running it through the
+     * interpreter to verify it functions correctly.
+     */
+    @Test
+    public void testApfInstructions() throws IllegalInstructionException {
+        // Empty program should pass because having the program counter reach the
+        // location immediately after the program indicates the packet should be
+        // passed to the AP.
+        ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION);
+        assertPass(gen);
+
+        // Test jumping to pass label.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJump(gen.PASS_LABEL);
+        byte[] program = gen.generate();
+        assertEquals(1, program.length);
+        assertEquals((14 << 3) | (0 << 1) | 0, program[0]);
+        assertPass(program, new byte[MIN_PKT_SIZE], 0);
+
+        // Test jumping to drop label.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJump(gen.DROP_LABEL);
+        program = gen.generate();
+        assertEquals(2, program.length);
+        assertEquals((14 << 3) | (1 << 1) | 0, program[0]);
+        assertEquals(1, program[1]);
+        assertDrop(program, new byte[15], 15);
+
+        // Test jumping if equal to 0.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfR0Equals(0, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test jumping if not equal to 0.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfR0NotEquals(0, gen.DROP_LABEL);
+        assertPass(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1);
+        gen.addJumpIfR0NotEquals(0, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test jumping if registers equal.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfR0EqualsR1(gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test jumping if registers not equal.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfR0NotEqualsR1(gen.DROP_LABEL);
+        assertPass(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1);
+        gen.addJumpIfR0NotEqualsR1(gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test load immediate.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test add.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addAdd(1234567890);
+        gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test subtract.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addAdd(-1234567890);
+        gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test or.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addOr(1234567890);
+        gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test and.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addAnd(123456789);
+        gen.addJumpIfR0Equals(1234567890 & 123456789, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test left shift.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLeftShift(1);
+        gen.addJumpIfR0Equals(1234567890 << 1, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test right shift.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addRightShift(1);
+        gen.addJumpIfR0Equals(1234567890 >> 1, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test multiply.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 123456789);
+        gen.addMul(2);
+        gen.addJumpIfR0Equals(123456789 * 2, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test divide.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addDiv(2);
+        gen.addJumpIfR0Equals(1234567890 / 2, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test divide by zero.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addDiv(0);
+        gen.addJump(gen.DROP_LABEL);
+        assertPass(gen);
+
+        // Test add.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 1234567890);
+        gen.addAddR1();
+        gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test subtract.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, -1234567890);
+        gen.addAddR1();
+        gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test or.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 1234567890);
+        gen.addOrR1();
+        gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test and.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLoadImmediate(Register.R1, 123456789);
+        gen.addAndR1();
+        gen.addJumpIfR0Equals(1234567890 & 123456789, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test left shift.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLoadImmediate(Register.R1, 1);
+        gen.addLeftShiftR1();
+        gen.addJumpIfR0Equals(1234567890 << 1, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test right shift.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLoadImmediate(Register.R1, -1);
+        gen.addLeftShiftR1();
+        gen.addJumpIfR0Equals(1234567890 >> 1, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test multiply.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 123456789);
+        gen.addLoadImmediate(Register.R1, 2);
+        gen.addMulR1();
+        gen.addJumpIfR0Equals(123456789 * 2, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test divide.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLoadImmediate(Register.R1, 2);
+        gen.addDivR1();
+        gen.addJumpIfR0Equals(1234567890 / 2, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test divide by zero.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addDivR1();
+        gen.addJump(gen.DROP_LABEL);
+        assertPass(gen);
+
+        // Test byte load.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoad8(Register.R0, 1);
+        gen.addJumpIfR0Equals(45, gen.DROP_LABEL);
+        assertDrop(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
+
+        // Test out of bounds load.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoad8(Register.R0, 16);
+        gen.addJumpIfR0Equals(0, gen.DROP_LABEL);
+        assertPass(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
+
+        // Test half-word load.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoad16(Register.R0, 1);
+        gen.addJumpIfR0Equals((45 << 8) | 67, gen.DROP_LABEL);
+        assertDrop(gen, new byte[]{123,45,67,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
+
+        // Test word load.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoad32(Register.R0, 1);
+        gen.addJumpIfR0Equals((45 << 24) | (67 << 16) | (89 << 8) | 12, gen.DROP_LABEL);
+        assertDrop(gen, new byte[]{123,45,67,89,12,0,0,0,0,0,0,0,0,0,0}, 0);
+
+        // Test byte indexed load.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 1);
+        gen.addLoad8Indexed(Register.R0, 0);
+        gen.addJumpIfR0Equals(45, gen.DROP_LABEL);
+        assertDrop(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
+
+        // Test out of bounds indexed load.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 8);
+        gen.addLoad8Indexed(Register.R0, 8);
+        gen.addJumpIfR0Equals(0, gen.DROP_LABEL);
+        assertPass(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
+
+        // Test half-word indexed load.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 1);
+        gen.addLoad16Indexed(Register.R0, 0);
+        gen.addJumpIfR0Equals((45 << 8) | 67, gen.DROP_LABEL);
+        assertDrop(gen, new byte[]{123,45,67,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
+
+        // Test word indexed load.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 1);
+        gen.addLoad32Indexed(Register.R0, 0);
+        gen.addJumpIfR0Equals((45 << 24) | (67 << 16) | (89 << 8) | 12, gen.DROP_LABEL);
+        assertDrop(gen, new byte[]{123,45,67,89,12,0,0,0,0,0,0,0,0,0,0}, 0);
+
+        // Test jumping if greater than.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfR0GreaterThan(0, gen.DROP_LABEL);
+        assertPass(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1);
+        gen.addJumpIfR0GreaterThan(0, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test jumping if less than.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfR0LessThan(0, gen.DROP_LABEL);
+        assertPass(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfR0LessThan(1, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test jumping if any bits set.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL);
+        assertPass(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1);
+        gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL);
+        assertDrop(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 3);
+        gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test jumping if register greater than.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfR0GreaterThanR1(gen.DROP_LABEL);
+        assertPass(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 2);
+        gen.addLoadImmediate(Register.R1, 1);
+        gen.addJumpIfR0GreaterThanR1(gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test jumping if register less than.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfR0LessThanR1(gen.DROP_LABEL);
+        assertPass(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 1);
+        gen.addJumpIfR0LessThanR1(gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test jumping if any bits set in register.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 3);
+        gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL);
+        assertPass(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 3);
+        gen.addLoadImmediate(Register.R0, 1);
+        gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL);
+        assertDrop(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 3);
+        gen.addLoadImmediate(Register.R0, 3);
+        gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test load from memory.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadFromMemory(Register.R0, 0);
+        gen.addJumpIfR0Equals(0, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test store to memory.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 1234567890);
+        gen.addStoreToMemory(Register.R1, 12);
+        gen.addLoadFromMemory(Register.R0, 12);
+        gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test filter age pre-filled memory.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT);
+        gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
+        assertDrop(gen, new byte[MIN_PKT_SIZE], 1234567890);
+
+        // Test packet size pre-filled memory.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT);
+        gen.addJumpIfR0Equals(MIN_PKT_SIZE, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test IPv4 header size pre-filled memory.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+        gen.addJumpIfR0Equals(20, gen.DROP_LABEL);
+        assertDrop(gen, new byte[]{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x45}, 0);
+
+        // Test not.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addNot(Register.R0);
+        gen.addJumpIfR0Equals(~1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test negate.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addNeg(Register.R0);
+        gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test move.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 1234567890);
+        gen.addMove(Register.R0);
+        gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addMove(Register.R1);
+        gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test swap.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R1, 1234567890);
+        gen.addSwap();
+        gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
+        assertDrop(gen);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addSwap();
+        gen.addJumpIfR0Equals(0, gen.DROP_LABEL);
+        assertDrop(gen);
+
+        // Test jump if bytes not equal.
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1);
+        gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL);
+        program = gen.generate();
+        assertEquals(6, program.length);
+        assertEquals((13 << 3) | (1 << 1) | 0, program[0]);
+        assertEquals(1, program[1]);
+        assertEquals(((20 << 3) | (1 << 1) | 0) - 256, program[2]);
+        assertEquals(1, program[3]);
+        assertEquals(1, program[4]);
+        assertEquals(123, program[5]);
+        assertDrop(program, new byte[MIN_PKT_SIZE], 0);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1);
+        gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL);
+        byte[] packet123 = {0,123,0,0,0,0,0,0,0,0,0,0,0,0,0};
+        assertPass(gen, packet123, 0);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL);
+        assertDrop(gen, packet123, 0);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1);
+        gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{1,2,30,4,5}, gen.DROP_LABEL);
+        byte[] packet12345 = {0,1,2,3,4,5,0,0,0,0,0,0,0,0,0};
+        assertDrop(gen, packet12345, 0);
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(Register.R0, 1);
+        gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{1,2,3,4,5}, gen.DROP_LABEL);
+        assertPass(gen, packet12345, 0);
+    }
+
+    @Test(expected = ApfGenerator.IllegalInstructionException.class)
+    public void testApfGeneratorWantsV2OrGreater() throws Exception {
+        // The minimum supported APF version is 2.
+        new ApfGenerator(1);
+    }
+
+    @Test
+    public void testApfDataOpcodesWantApfV3() throws IllegalInstructionException, Exception {
+        ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION);
+        try {
+            gen.addStoreData(Register.R0, 0);
+            fail();
+        } catch (IllegalInstructionException expected) {
+            /* pass */
+        }
+        try {
+            gen.addLoadData(Register.R0, 0);
+            fail();
+        } catch (IllegalInstructionException expected) {
+            /* pass */
+        }
+    }
+
+    /**
+     * Test that the generator emits immediates using the shortest possible encoding.
+     */
+    @Test
+    public void testImmediateEncoding() throws IllegalInstructionException {
+        ApfGenerator gen;
+
+        // 0-byte immediate: li R0, 0
+        gen = new ApfGenerator(4);
+        gen.addLoadImmediate(Register.R0, 0);
+        assertProgramEquals(new byte[]{LI_OP | SIZE0}, gen.generate());
+
+        // 1-byte immediate: li R0, 42
+        gen = new ApfGenerator(4);
+        gen.addLoadImmediate(Register.R0, 42);
+        assertProgramEquals(new byte[]{LI_OP | SIZE8, 42}, gen.generate());
+
+        // 2-byte immediate: li R1, 0x1234
+        gen = new ApfGenerator(4);
+        gen.addLoadImmediate(Register.R1, 0x1234);
+        assertProgramEquals(new byte[]{LI_OP | SIZE16 | R1, 0x12, 0x34}, gen.generate());
+
+        // 4-byte immediate: li R0, 0x12345678
+        gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R0, 0x12345678);
+        assertProgramEquals(
+                new byte[]{LI_OP | SIZE32, 0x12, 0x34, 0x56, 0x78},
+                gen.generate());
+    }
+
+    /**
+     * Test that the generator emits negative immediates using the shortest possible encoding.
+     */
+    @Test
+    public void testNegativeImmediateEncoding() throws IllegalInstructionException {
+        ApfGenerator gen;
+
+        // 1-byte negative immediate: li R0, -42
+        gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R0, -42);
+        assertProgramEquals(new byte[]{LI_OP | SIZE8, -42}, gen.generate());
+
+        // 2-byte negative immediate: li R1, -0x1122
+        gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R1, -0x1122);
+        assertProgramEquals(new byte[]{LI_OP | SIZE16 | R1, (byte)0xEE, (byte)0xDE},
+                gen.generate());
+
+        // 4-byte negative immediate: li R0, -0x11223344
+        gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R0, -0x11223344);
+        assertProgramEquals(
+                new byte[]{LI_OP | SIZE32, (byte)0xEE, (byte)0xDD, (byte)0xCC, (byte)0xBC},
+                gen.generate());
+    }
+
+    /**
+     * Test that the generator correctly emits positive and negative immediates for LDDW/STDW.
+     */
+    @Test
+    public void testLoadStoreDataEncoding() throws IllegalInstructionException {
+        ApfGenerator gen;
+
+        // Load data with no offset: lddw R0, [0 + r1]
+        gen = new ApfGenerator(3);
+        gen.addLoadData(Register.R0, 0);
+        assertProgramEquals(new byte[]{LDDW_OP | SIZE0}, gen.generate());
+
+        // Store data with 8bit negative offset: lddw r0, [-42 + r1]
+        gen = new ApfGenerator(3);
+        gen.addStoreData(Register.R0, -42);
+        assertProgramEquals(new byte[]{STDW_OP | SIZE8, -42}, gen.generate());
+
+        // Store data to R1 with 16bit negative offset: stdw r1, [-0x1122 + r0]
+        gen = new ApfGenerator(3);
+        gen.addStoreData(Register.R1, -0x1122);
+        assertProgramEquals(new byte[]{STDW_OP | SIZE16 | R1, (byte)0xEE, (byte)0xDE},
+                gen.generate());
+
+        // Load data to R1 with 32bit negative offset: lddw r1, [0xDEADBEEF + r0]
+        gen = new ApfGenerator(3);
+        gen.addLoadData(Register.R1, 0xDEADBEEF);
+        assertProgramEquals(
+                new byte[]{LDDW_OP | SIZE32 | R1, (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF},
+                gen.generate());
+    }
+
+    /**
+     * Test that the interpreter correctly executes STDW with a negative 8bit offset
+     */
+    @Test
+    public void testApfDataWrite() throws IllegalInstructionException, Exception {
+        byte[] packet = new byte[MIN_PKT_SIZE];
+        byte[] data = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
+        byte[] expected_data = data.clone();
+
+        // No memory access instructions: should leave the data segment untouched.
+        ApfGenerator gen = new ApfGenerator(3);
+        assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
+
+        // Expect value 0x87654321 to be stored starting from address -11 from the end of the
+        // data buffer, in big-endian order.
+        gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R0, 0x87654321);
+        gen.addLoadImmediate(Register.R1, -5);
+        gen.addStoreData(Register.R0, -6);  // -5 + -6 = -11 (offset +5 with data_len=16)
+        expected_data[5] = (byte)0x87;
+        expected_data[6] = (byte)0x65;
+        expected_data[7] = (byte)0x43;
+        expected_data[8] = (byte)0x21;
+        assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
+    }
+
+    /**
+     * Test that the interpreter correctly executes LDDW with a negative 16bit offset
+     */
+    @Test
+    public void testApfDataRead() throws IllegalInstructionException, Exception {
+        // Program that DROPs if address 10 (-6) contains 0x87654321.
+        ApfGenerator gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R1, 1000);
+        gen.addLoadData(Register.R0, -1006);  // 1000 + -1006 = -6 (offset +10 with data_len=16)
+        gen.addJumpIfR0Equals(0x87654321, gen.DROP_LABEL);
+        byte[] program = gen.generate();
+        byte[] packet = new byte[MIN_PKT_SIZE];
+
+        // Content is incorrect (last byte does not match) -> PASS
+        byte[] data = new byte[16];
+        data[10] = (byte)0x87;
+        data[11] = (byte)0x65;
+        data[12] = (byte)0x43;
+        data[13] = (byte)0x00;  // != 0x21
+        byte[] expected_data = data.clone();
+        assertDataMemoryContents(PASS, program, packet, data, expected_data);
+
+        // Fix the last byte -> conditional jump taken -> DROP
+        data[13] = (byte)0x21;
+        expected_data = data;
+        assertDataMemoryContents(DROP, program, packet, data, expected_data);
+    }
+
+    /**
+     * Test that the interpreter correctly executes LDDW followed by a STDW.
+     * To cover a few more edge cases, LDDW has a 0bit offset, while STDW has a positive 8bit
+     * offset.
+     */
+    @Test
+    public void testApfDataReadModifyWrite() throws IllegalInstructionException, Exception {
+        ApfGenerator gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R1, -22);
+        gen.addLoadData(Register.R0, 0);  // Load from address 32 -22 + 0 = 10
+        gen.addAdd(0x78453412);  // 87654321 + 78453412 = FFAA7733
+        gen.addStoreData(Register.R0, 4);  // Write back to address 32 -22 + 4 = 14
+
+        byte[] packet = new byte[MIN_PKT_SIZE];
+        byte[] data = new byte[32];
+        data[10] = (byte)0x87;
+        data[11] = (byte)0x65;
+        data[12] = (byte)0x43;
+        data[13] = (byte)0x21;
+        byte[] expected_data = data.clone();
+        expected_data[14] = (byte)0xFF;
+        expected_data[15] = (byte)0xAA;
+        expected_data[16] = (byte)0x77;
+        expected_data[17] = (byte)0x33;
+        assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
+    }
+
+    @Test
+    public void testApfDataBoundChecking() throws IllegalInstructionException, Exception {
+        byte[] packet = new byte[MIN_PKT_SIZE];
+        byte[] data = new byte[32];
+        byte[] expected_data = data;
+
+        // Program that DROPs unconditionally. This is our the baseline.
+        ApfGenerator gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R0, 3);
+        gen.addLoadData(Register.R1, 7);
+        gen.addJump(gen.DROP_LABEL);
+        assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
+
+        // Same program as before, but this time we're trying to load past the end of the data.
+        gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R0, 20);
+        gen.addLoadData(Register.R1, 15);  // 20 + 15 > 32
+        gen.addJump(gen.DROP_LABEL);  // Not reached.
+        assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
+
+        // Subtracting an immediate should work...
+        gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R0, 20);
+        gen.addLoadData(Register.R1, -4);
+        gen.addJump(gen.DROP_LABEL);
+        assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
+
+        // ...and underflowing simply wraps around to the end of the buffer...
+        gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R0, 20);
+        gen.addLoadData(Register.R1, -30);
+        gen.addJump(gen.DROP_LABEL);
+        assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
+
+        // ...but doesn't allow accesses before the start of the buffer
+        gen = new ApfGenerator(3);
+        gen.addLoadImmediate(Register.R0, 20);
+        gen.addLoadData(Register.R1, -1000);
+        gen.addJump(gen.DROP_LABEL);  // Not reached.
+        assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
+    }
+
+    /**
+     * Generate some BPF programs, translate them to APF, then run APF and BPF programs
+     * over packet traces and verify both programs filter out the same packets.
+     */
+    @Test
+    public void testApfAgainstBpf() throws Exception {
+        String[] tcpdump_filters = new String[]{ "udp", "tcp", "icmp", "icmp6", "udp port 53",
+                "arp", "dst 239.255.255.250", "arp or tcp or udp port 53", "net 192.168.1.0/24",
+                "arp or icmp6 or portrange 53-54", "portrange 53-54 or portrange 100-50000",
+                "tcp[tcpflags] & (tcp-ack|tcp-fin) != 0 and (ip[2:2] > 57 or icmp)" };
+        String pcap_filename = stageFile(R.raw.apf);
+        for (String tcpdump_filter : tcpdump_filters) {
+            byte[] apf_program = Bpf2Apf.convert(compileToBpf(tcpdump_filter));
+            assertTrue("Failed to match for filter: " + tcpdump_filter,
+                    compareBpfApf(tcpdump_filter, pcap_filename, apf_program));
+        }
+    }
+
+    /**
+     * Generate APF program, run pcap file though APF filter, then check all the packets in the file
+     * should be dropped.
+     */
+    @Test
+    public void testApfFilterPcapFile() throws Exception {
+        final byte[] MOCK_PCAP_IPV4_ADDR = {(byte) 172, 16, 7, (byte) 151};
+        String pcapFilename = stageFile(R.raw.apfPcap);
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_PCAP_IPV4_ADDR), 16);
+        LinkProperties lp = new LinkProperties();
+        lp.addLinkAddress(link);
+
+        ApfConfiguration config = getDefaultConfig();
+        ApfCapabilities MOCK_APF_PCAP_CAPABILITIES = new ApfCapabilities(4, 1700, ARPHRD_ETHER);
+        config.apfCapabilities = MOCK_APF_PCAP_CAPABILITIES;
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        apfFilter.setLinkProperties(lp);
+        byte[] program = ipClientCallback.getApfProgram();
+        byte[] data = new byte[ApfFilter.Counter.totalSize()];
+        final boolean result;
+
+        result = dropsAllPackets(program, data, pcapFilename);
+        Log.i(TAG, "testApfFilterPcapFile(): Data counters: " + HexDump.toHexString(data, false));
+
+        assertTrue("Failed to drop all packets by filter. \nAPF counters:" +
+            HexDump.toHexString(data, false), result);
+    }
+
+    private class MockIpClientCallback extends IpClientCallbacksWrapper {
+        private final ConditionVariable mGotApfProgram = new ConditionVariable();
+        private byte[] mLastApfProgram;
+
+        MockIpClientCallback() {
+            super(mock(IIpClientCallbacks.class), mock(SharedLog.class));
+        }
+
+        @Override
+        public void installPacketFilter(byte[] filter) {
+            mLastApfProgram = filter;
+            mGotApfProgram.open();
+        }
+
+        public void resetApfProgramWait() {
+            mGotApfProgram.close();
+        }
+
+        public byte[] getApfProgram() {
+            assertTrue(mGotApfProgram.block(TIMEOUT_MS));
+            return mLastApfProgram;
+        }
+
+        public void assertNoProgramUpdate() {
+            assertFalse(mGotApfProgram.block(TIMEOUT_MS));
+        }
+    }
+
+    private static class TestApfFilter extends ApfFilter {
+        public static final byte[] MOCK_MAC_ADDR = {1,2,3,4,5,6};
+
+        private FileDescriptor mWriteSocket;
+        private final long mFixedTimeMs = SystemClock.elapsedRealtime();
+
+        public TestApfFilter(Context context, ApfConfiguration config,
+                IpClientCallbacksWrapper ipClientCallback, IpConnectivityLog log) throws Exception {
+            super(context, config, InterfaceParams.getByName("lo"), ipClientCallback, log);
+        }
+
+        // Pretend an RA packet has been received and show it to ApfFilter.
+        public void pretendPacketReceived(byte[] packet) throws IOException, ErrnoException {
+            // ApfFilter's ReceiveThread will be waiting to read this.
+            Os.write(mWriteSocket, packet, 0, packet.length);
+        }
+
+        @Override
+        protected long currentTimeSeconds() {
+            return mFixedTimeMs / DateUtils.SECOND_IN_MILLIS;
+        }
+
+        @Override
+        void maybeStartFilter() {
+            mHardwareAddress = MOCK_MAC_ADDR;
+            installNewProgramLocked();
+
+            // Create two sockets, "readSocket" and "mWriteSocket" and connect them together.
+            FileDescriptor readSocket = new FileDescriptor();
+            mWriteSocket = new FileDescriptor();
+            try {
+                Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mWriteSocket, readSocket);
+            } catch (ErrnoException e) {
+                fail();
+                return;
+            }
+            // Now pass readSocket to ReceiveThread as if it was setup to read raw RAs.
+            // This allows us to pretend RA packets have been recieved via pretendPacketReceived().
+            mReceiveThread = new ReceiveThread(readSocket);
+            mReceiveThread.start();
+        }
+
+        @Override
+        public void shutdown() {
+            super.shutdown();
+            IoUtils.closeQuietly(mWriteSocket);
+        }
+    }
+
+    private static final int ETH_HEADER_LEN = 14;
+    private static final int ETH_DEST_ADDR_OFFSET = 0;
+    private static final int ETH_ETHERTYPE_OFFSET = 12;
+    private static final byte[] ETH_BROADCAST_MAC_ADDRESS =
+            {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
+
+    private static final int IPV4_VERSION_IHL_OFFSET = ETH_HEADER_LEN + 0;
+    private static final int IPV4_PROTOCOL_OFFSET = ETH_HEADER_LEN + 9;
+    private static final int IPV4_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 16;
+    private static final byte[] IPV4_BROADCAST_ADDRESS =
+            {(byte) 255, (byte) 255, (byte) 255, (byte) 255};
+
+    private static final int IPV6_NEXT_HEADER_OFFSET = ETH_HEADER_LEN + 6;
+    private static final int IPV6_HEADER_LEN = 40;
+    private static final int IPV6_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 24;
+    // The IPv6 all nodes address ff02::1
+    private static final byte[] IPV6_ALL_NODES_ADDRESS =
+            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
+    private static final byte[] IPV6_ALL_ROUTERS_ADDRESS =
+            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 };
+
+    private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
+    private static final int ICMP6_ROUTER_SOLICITATION = 133;
+    private static final int ICMP6_ROUTER_ADVERTISEMENT = 134;
+    private static final int ICMP6_NEIGHBOR_SOLICITATION = 135;
+    private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;
+
+    private static final int ICMP6_RA_HEADER_LEN = 16;
+    private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + 6;
+    private static final int ICMP6_RA_CHECKSUM_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
+    private static final int ICMP6_RA_OPTION_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
+
+    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_PREFERRED_LIFETIME_OFFSET = 8;
+
+    // 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_OPTION_LEN = 8;
+    private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
+    private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4;
+
+    private static final int UDP_HEADER_LEN = 8;
+    private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 22;
+
+    private static final int DHCP_CLIENT_PORT = 68;
+    private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 48;
+
+    private static final int ARP_HEADER_OFFSET = ETH_HEADER_LEN;
+    private static final byte[] ARP_IPV4_REQUEST_HEADER = {
+            0, 1, // Hardware type: Ethernet (1)
+            8, 0, // Protocol type: IP (0x0800)
+            6,    // Hardware size: 6
+            4,    // Protocol size: 4
+            0, 1  // Opcode: request (1)
+    };
+    private static final byte[] ARP_IPV4_REPLY_HEADER = {
+            0, 1, // Hardware type: Ethernet (1)
+            8, 0, // Protocol type: IP (0x0800)
+            6,    // Hardware size: 6
+            4,    // Protocol size: 4
+            0, 2  // Opcode: reply (2)
+    };
+    private static final int ARP_SOURCE_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 14;
+    private static final int ARP_TARGET_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 24;
+
+    private static final byte[] MOCK_IPV4_ADDR           = {10, 0, 0, 1};
+    private static final byte[] MOCK_BROADCAST_IPV4_ADDR = {10, 0, 31, (byte) 255}; // prefix = 19
+    private static final byte[] MOCK_MULTICAST_IPV4_ADDR = {(byte) 224, 0, 0, 1};
+    private static final byte[] ANOTHER_IPV4_ADDR        = {10, 0, 0, 2};
+    private static final byte[] IPV4_SOURCE_ADDR         = {10, 0, 0, 3};
+    private static final byte[] ANOTHER_IPV4_SOURCE_ADDR = {(byte) 192, 0, 2, 1};
+    private static final byte[] BUG_PROBE_SOURCE_ADDR1   = {0, 0, 1, 2};
+    private static final byte[] BUG_PROBE_SOURCE_ADDR2   = {3, 4, 0, 0};
+    private static final byte[] IPV4_ANY_HOST_ADDR       = {0, 0, 0, 0};
+
+    // Helper to initialize a default apfFilter.
+    private ApfFilter setupApfFilter(
+            IpClientCallbacksWrapper ipClientCallback, ApfConfiguration config) throws Exception {
+        LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 19);
+        LinkProperties lp = new LinkProperties();
+        lp.addLinkAddress(link);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        apfFilter.setLinkProperties(lp);
+        return apfFilter;
+    }
+
+    @Test
+    public void testApfFilterIPv4() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 19);
+        LinkProperties lp = new LinkProperties();
+        lp.addLinkAddress(link);
+
+        ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        apfFilter.setLinkProperties(lp);
+
+        byte[] program = ipClientCallback.getApfProgram();
+
+        // Verify empty packet of 100 zero bytes is passed
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        assertPass(program, packet.array());
+
+        // Verify unicast IPv4 packet is passed
+        put(packet, ETH_DEST_ADDR_OFFSET, TestApfFilter.MOCK_MAC_ADDR);
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_IPV4_ADDR);
+        assertPass(program, packet.array());
+
+        // Verify L2 unicast to IPv4 broadcast addresses is dropped (b/30231088)
+        put(packet, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS);
+        assertDrop(program, packet.array());
+        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_BROADCAST_IPV4_ADDR);
+        assertDrop(program, packet.array());
+
+        // Verify multicast/broadcast IPv4, not DHCP to us, is dropped
+        put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS);
+        assertDrop(program, packet.array());
+        packet.put(IPV4_VERSION_IHL_OFFSET, (byte)0x45);
+        assertDrop(program, packet.array());
+        packet.put(IPV4_PROTOCOL_OFFSET, (byte)IPPROTO_UDP);
+        assertDrop(program, packet.array());
+        packet.putShort(UDP_DESTINATION_PORT_OFFSET, (short)DHCP_CLIENT_PORT);
+        assertDrop(program, packet.array());
+        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_MULTICAST_IPV4_ADDR);
+        assertDrop(program, packet.array());
+        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_BROADCAST_IPV4_ADDR);
+        assertDrop(program, packet.array());
+        put(packet, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS);
+        assertDrop(program, packet.array());
+
+        // Verify broadcast IPv4 DHCP to us is passed
+        put(packet, DHCP_CLIENT_MAC_OFFSET, TestApfFilter.MOCK_MAC_ADDR);
+        assertPass(program, packet.array());
+
+        // Verify unicast IPv4 DHCP to us is passed
+        put(packet, ETH_DEST_ADDR_OFFSET, TestApfFilter.MOCK_MAC_ADDR);
+        assertPass(program, packet.array());
+
+        apfFilter.shutdown();
+    }
+
+    @Test
+    public void testApfFilterIPv6() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        byte[] program = ipClientCallback.getApfProgram();
+
+        // Verify empty IPv6 packet is passed
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6);
+        assertPass(program, packet.array());
+
+        // Verify empty ICMPv6 packet is passed
+        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6);
+        assertPass(program, packet.array());
+
+        // Verify empty ICMPv6 NA packet is passed
+        packet.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_NEIGHBOR_ANNOUNCEMENT);
+        assertPass(program, packet.array());
+
+        // Verify ICMPv6 NA to ff02::1 is dropped
+        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_NODES_ADDRESS);
+        assertDrop(program, packet.array());
+
+        // Verify ICMPv6 RS to any is dropped
+        packet.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_ROUTER_SOLICITATION);
+        assertDrop(program, packet.array());
+        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_ROUTERS_ADDRESS);
+        assertDrop(program, packet.array());
+
+        apfFilter.shutdown();
+    }
+
+    @Test
+    public void testApfFilterMulticast() throws Exception {
+        final byte[] unicastIpv4Addr   = {(byte)192,0,2,63};
+        final byte[] broadcastIpv4Addr = {(byte)192,0,2,(byte)255};
+        final byte[] multicastIpv4Addr = {(byte)224,0,0,1};
+        final byte[] multicastIpv6Addr = {(byte)0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,(byte)0xfb};
+
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        LinkAddress link = new LinkAddress(InetAddress.getByAddress(unicastIpv4Addr), 24);
+        LinkProperties lp = new LinkProperties();
+        lp.addLinkAddress(link);
+
+        ApfConfiguration config = getDefaultConfig();
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        apfFilter.setLinkProperties(lp);
+
+        byte[] program = ipClientCallback.getApfProgram();
+
+        // Construct IPv4 and IPv6 multicast packets.
+        ByteBuffer mcastv4packet = ByteBuffer.wrap(new byte[100]);
+        mcastv4packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        put(mcastv4packet, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr);
+
+        ByteBuffer mcastv6packet = ByteBuffer.wrap(new byte[100]);
+        mcastv6packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6);
+        mcastv6packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_UDP);
+        put(mcastv6packet, IPV6_DEST_ADDR_OFFSET, multicastIpv6Addr);
+
+        // Construct IPv4 broadcast packet.
+        ByteBuffer bcastv4packet1 = ByteBuffer.wrap(new byte[100]);
+        bcastv4packet1.put(ETH_BROADCAST_MAC_ADDRESS);
+        bcastv4packet1.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        put(bcastv4packet1, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr);
+
+        ByteBuffer bcastv4packet2 = ByteBuffer.wrap(new byte[100]);
+        bcastv4packet2.put(ETH_BROADCAST_MAC_ADDRESS);
+        bcastv4packet2.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        put(bcastv4packet2, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS);
+
+        // Construct IPv4 broadcast with L2 unicast address packet (b/30231088).
+        ByteBuffer bcastv4unicastl2packet = ByteBuffer.wrap(new byte[100]);
+        bcastv4unicastl2packet.put(TestApfFilter.MOCK_MAC_ADDR);
+        bcastv4unicastl2packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        put(bcastv4unicastl2packet, IPV4_DEST_ADDR_OFFSET, broadcastIpv4Addr);
+
+        // Verify initially disabled multicast filter is off
+        assertPass(program, mcastv4packet.array());
+        assertPass(program, mcastv6packet.array());
+        assertPass(program, bcastv4packet1.array());
+        assertPass(program, bcastv4packet2.array());
+        assertPass(program, bcastv4unicastl2packet.array());
+
+        // Turn on multicast filter and verify it works
+        ipClientCallback.resetApfProgramWait();
+        apfFilter.setMulticastFilter(true);
+        program = ipClientCallback.getApfProgram();
+        assertDrop(program, mcastv4packet.array());
+        assertDrop(program, mcastv6packet.array());
+        assertDrop(program, bcastv4packet1.array());
+        assertDrop(program, bcastv4packet2.array());
+        assertDrop(program, bcastv4unicastl2packet.array());
+
+        // Turn off multicast filter and verify it's off
+        ipClientCallback.resetApfProgramWait();
+        apfFilter.setMulticastFilter(false);
+        program = ipClientCallback.getApfProgram();
+        assertPass(program, mcastv4packet.array());
+        assertPass(program, mcastv6packet.array());
+        assertPass(program, bcastv4packet1.array());
+        assertPass(program, bcastv4packet2.array());
+        assertPass(program, bcastv4unicastl2packet.array());
+
+        // Verify it can be initialized to on
+        ipClientCallback.resetApfProgramWait();
+        apfFilter.shutdown();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        apfFilter.setLinkProperties(lp);
+        program = ipClientCallback.getApfProgram();
+        assertDrop(program, mcastv4packet.array());
+        assertDrop(program, mcastv6packet.array());
+        assertDrop(program, bcastv4packet1.array());
+        assertDrop(program, bcastv4unicastl2packet.array());
+
+        // Verify that ICMPv6 multicast is not dropped.
+        mcastv6packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6);
+        assertPass(program, mcastv6packet.array());
+
+        apfFilter.shutdown();
+    }
+
+    @Test
+    public void testApfFilterMulticastPingWhileDozing() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfFilter apfFilter = setupApfFilter(ipClientCallback, getDefaultConfig());
+
+        // Construct a multicast ICMPv6 ECHO request.
+        final byte[] multicastIpv6Addr = {(byte)0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,(byte)0xfb};
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6);
+        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6);
+        packet.put(ICMP6_TYPE_OFFSET, (byte)ICMPV6_ECHO_REQUEST_TYPE);
+        put(packet, IPV6_DEST_ADDR_OFFSET, multicastIpv6Addr);
+
+        // Normally, we let multicast pings alone...
+        assertPass(ipClientCallback.getApfProgram(), packet.array());
+
+        // ...and even while dozing...
+        apfFilter.setDozeMode(true);
+        assertPass(ipClientCallback.getApfProgram(), packet.array());
+
+        // ...but when the multicast filter is also enabled, drop the multicast pings to save power.
+        apfFilter.setMulticastFilter(true);
+        assertDrop(ipClientCallback.getApfProgram(), packet.array());
+
+        // However, we should still let through all other ICMPv6 types.
+        ByteBuffer raPacket = ByteBuffer.wrap(packet.array().clone());
+        raPacket.put(ICMP6_TYPE_OFFSET, (byte) NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT);
+        assertPass(ipClientCallback.getApfProgram(), raPacket.array());
+
+        // Now wake up from doze mode to ensure that we no longer drop the packets.
+        // (The multicast filter is still enabled at this point).
+        apfFilter.setDozeMode(false);
+        assertPass(ipClientCallback.getApfProgram(), packet.array());
+
+        apfFilter.shutdown();
+    }
+
+    @Test
+    public void testApfFilter802_3() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        ApfFilter apfFilter = setupApfFilter(ipClientCallback, config);
+        byte[] program = ipClientCallback.getApfProgram();
+
+        // Verify empty packet of 100 zero bytes is passed
+        // Note that eth-type = 0 makes it an IEEE802.3 frame
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        assertPass(program, packet.array());
+
+        // Verify empty packet with IPv4 is passed
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        assertPass(program, packet.array());
+
+        // Verify empty IPv6 packet is passed
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6);
+        assertPass(program, packet.array());
+
+        // Now turn on the filter
+        ipClientCallback.resetApfProgramWait();
+        apfFilter.shutdown();
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        apfFilter = setupApfFilter(ipClientCallback, config);
+        program = ipClientCallback.getApfProgram();
+
+        // Verify that IEEE802.3 frame is dropped
+        // In this case ethtype is used for payload length
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)(100 - 14));
+        assertDrop(program, packet.array());
+
+        // Verify that IPv4 (as example of Ethernet II) frame will pass
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        assertPass(program, packet.array());
+
+        // Verify that IPv6 (as example of Ethernet II) frame will pass
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6);
+        assertPass(program, packet.array());
+
+        apfFilter.shutdown();
+    }
+
+    @Test
+    public void testApfFilterEthTypeBL() throws Exception {
+        final int[] emptyBlackList = {};
+        final int[] ipv4BlackList = {ETH_P_IP};
+        final int[] ipv4Ipv6BlackList = {ETH_P_IP, ETH_P_IPV6};
+
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        ApfFilter apfFilter = setupApfFilter(ipClientCallback, config);
+        byte[] program = ipClientCallback.getApfProgram();
+
+        // Verify empty packet of 100 zero bytes is passed
+        // Note that eth-type = 0 makes it an IEEE802.3 frame
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        assertPass(program, packet.array());
+
+        // Verify empty packet with IPv4 is passed
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        assertPass(program, packet.array());
+
+        // Verify empty IPv6 packet is passed
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6);
+        assertPass(program, packet.array());
+
+        // Now add IPv4 to the black list
+        ipClientCallback.resetApfProgramWait();
+        apfFilter.shutdown();
+        config.ethTypeBlackList = ipv4BlackList;
+        apfFilter = setupApfFilter(ipClientCallback, config);
+        program = ipClientCallback.getApfProgram();
+
+        // Verify that IPv4 frame will be dropped
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        assertDrop(program, packet.array());
+
+        // Verify that IPv6 frame will pass
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6);
+        assertPass(program, packet.array());
+
+        // Now let us have both IPv4 and IPv6 in the black list
+        ipClientCallback.resetApfProgramWait();
+        apfFilter.shutdown();
+        config.ethTypeBlackList = ipv4Ipv6BlackList;
+        apfFilter = setupApfFilter(ipClientCallback, config);
+        program = ipClientCallback.getApfProgram();
+
+        // Verify that IPv4 frame will be dropped
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        assertDrop(program, packet.array());
+
+        // Verify that IPv6 frame will be dropped
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6);
+        assertDrop(program, packet.array());
+
+        apfFilter.shutdown();
+    }
+
+    private byte[] getProgram(MockIpClientCallback cb, ApfFilter filter, LinkProperties lp) {
+        cb.resetApfProgramWait();
+        filter.setLinkProperties(lp);
+        return cb.getApfProgram();
+    }
+
+    private void verifyArpFilter(byte[] program, int filterResult) {
+        // Verify ARP request packet
+        assertPass(program, arpRequestBroadcast(MOCK_IPV4_ADDR));
+        assertVerdict(filterResult, program, arpRequestBroadcast(ANOTHER_IPV4_ADDR));
+        assertDrop(program, arpRequestBroadcast(IPV4_ANY_HOST_ADDR));
+
+        // Verify ARP reply packets from different source ip
+        assertDrop(program, arpReply(IPV4_ANY_HOST_ADDR, IPV4_ANY_HOST_ADDR));
+        assertPass(program, arpReply(ANOTHER_IPV4_SOURCE_ADDR, IPV4_ANY_HOST_ADDR));
+        assertPass(program, arpReply(BUG_PROBE_SOURCE_ADDR1, IPV4_ANY_HOST_ADDR));
+        assertPass(program, arpReply(BUG_PROBE_SOURCE_ADDR2, IPV4_ANY_HOST_ADDR));
+
+        // Verify unicast ARP reply packet is always accepted.
+        assertPass(program, arpReply(IPV4_SOURCE_ADDR, MOCK_IPV4_ADDR));
+        assertPass(program, arpReply(IPV4_SOURCE_ADDR, ANOTHER_IPV4_ADDR));
+        assertPass(program, arpReply(IPV4_SOURCE_ADDR, IPV4_ANY_HOST_ADDR));
+
+        // Verify GARP reply packets are always filtered
+        assertDrop(program, garpReply());
+    }
+
+    @Test
+    public void testApfFilterArp() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+
+        // Verify initially ARP request filter is off, and GARP filter is on.
+        verifyArpFilter(ipClientCallback.getApfProgram(), PASS);
+
+        // Inform ApfFilter of our address and verify ARP filtering is on
+        LinkAddress linkAddress = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 24);
+        LinkProperties lp = new LinkProperties();
+        assertTrue(lp.addLinkAddress(linkAddress));
+        verifyArpFilter(getProgram(ipClientCallback, apfFilter, lp), DROP);
+
+        // Inform ApfFilter of loss of IP and verify ARP filtering is off
+        verifyArpFilter(getProgram(ipClientCallback, apfFilter, new LinkProperties()), PASS);
+
+        apfFilter.shutdown();
+    }
+
+    private static byte[] arpReply(byte[] sip, byte[] tip) {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP);
+        put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REPLY_HEADER);
+        put(packet, ARP_SOURCE_IP_ADDRESS_OFFSET, sip);
+        put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, tip);
+        return packet.array();
+    }
+
+    private static byte[] arpRequestBroadcast(byte[] tip) {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP);
+        put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS);
+        put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REQUEST_HEADER);
+        put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, tip);
+        return packet.array();
+    }
+
+    private static byte[] garpReply() {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP);
+        put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS);
+        put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REPLY_HEADER);
+        put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, IPV4_ANY_HOST_ADDR);
+        return packet.array();
+    }
+
+    // Verify that the last program pushed to the IpClient.Callback properly filters the
+    // given packet for the given lifetime.
+    private void verifyRaLifetime(byte[] program, ByteBuffer packet, int lifetime) {
+        final int FRACTION_OF_LIFETIME = 6;
+        final int ageLimit = lifetime / FRACTION_OF_LIFETIME;
+
+        // Verify new program should drop RA for 1/6th its lifetime and pass afterwards.
+        assertDrop(program, packet.array());
+        assertDrop(program, packet.array(), ageLimit);
+        assertPass(program, packet.array(), ageLimit + 1);
+        assertPass(program, packet.array(), lifetime);
+        // Verify RA checksum is ignored
+        final short originalChecksum = packet.getShort(ICMP6_RA_CHECKSUM_OFFSET);
+        packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, (short)12345);
+        assertDrop(program, packet.array());
+        packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, (short)-12345);
+        assertDrop(program, packet.array());
+        packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, originalChecksum);
+
+        // Verify other changes to RA make it not match filter
+        final byte originalFirstByte = packet.get(0);
+        packet.put(0, (byte)-1);
+        assertPass(program, packet.array());
+        packet.put(0, (byte)0);
+        assertDrop(program, packet.array());
+        packet.put(0, originalFirstByte);
+    }
+
+    // Test that when ApfFilter is shown the given packet, it generates a program to filter it
+    // for the given lifetime.
+    private void verifyRaLifetime(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback,
+            ByteBuffer packet, int lifetime) throws IOException, ErrnoException {
+        // Verify new program generated if ApfFilter witnesses RA
+        ipClientCallback.resetApfProgramWait();
+        apfFilter.pretendPacketReceived(packet.array());
+        byte[] program = ipClientCallback.getApfProgram();
+        verifyRaLifetime(program, packet, lifetime);
+    }
+
+    private void verifyRaEvent(RaEvent expected) {
+        ArgumentCaptor<IpConnectivityLog.Event> captor =
+                ArgumentCaptor.forClass(IpConnectivityLog.Event.class);
+        verify(mLog, atLeastOnce()).log(captor.capture());
+        RaEvent got = lastRaEvent(captor.getAllValues());
+        if (!raEventEquals(expected, got)) {
+            assertEquals(expected, got);  // fail for printing an assertion error message.
+        }
+    }
+
+    private RaEvent lastRaEvent(List<IpConnectivityLog.Event> events) {
+        RaEvent got = null;
+        for (Parcelable ev : events) {
+            if (ev instanceof RaEvent) {
+                got = (RaEvent) ev;
+            }
+        }
+        return got;
+    }
+
+    private boolean raEventEquals(RaEvent ev1, RaEvent ev2) {
+        return (ev1 != null) && (ev2 != null)
+                && (ev1.routerLifetime == ev2.routerLifetime)
+                && (ev1.prefixValidLifetime == ev2.prefixValidLifetime)
+                && (ev1.prefixPreferredLifetime == ev2.prefixPreferredLifetime)
+                && (ev1.routeInfoLifetime == ev2.routeInfoLifetime)
+                && (ev1.rdnssLifetime == ev2.rdnssLifetime)
+                && (ev1.dnsslLifetime == ev2.dnsslLifetime);
+    }
+
+    private void assertInvalidRa(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback,
+            ByteBuffer packet) throws IOException, ErrnoException {
+        ipClientCallback.resetApfProgramWait();
+        apfFilter.pretendPacketReceived(packet.array());
+        ipClientCallback.assertNoProgramUpdate();
+    }
+
+    @Test
+    public void testApfFilterRa() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        byte[] program = ipClientCallback.getApfProgram();
+
+        final int ROUTER_LIFETIME = 1000;
+        final int PREFIX_VALID_LIFETIME = 200;
+        final int PREFIX_PREFERRED_LIFETIME = 100;
+        final int RDNSS_LIFETIME  = 300;
+        final int ROUTE_LIFETIME  = 400;
+        // Note that lifetime of 2000 will be ignored in favor of shorter route lifetime of 1000.
+        final int DNSSL_LIFETIME  = 2000;
+        final int VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET = ETH_HEADER_LEN;
+        // IPv6, traffic class = 0, flow label = 0x12345
+        final int VERSION_TRAFFIC_CLASS_FLOW_LABEL = 0x60012345;
+
+        // Verify RA is passed the first time
+        ByteBuffer basePacket = ByteBuffer.wrap(new byte[ICMP6_RA_OPTION_OFFSET]);
+        basePacket.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6);
+        basePacket.putInt(VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET,
+                VERSION_TRAFFIC_CLASS_FLOW_LABEL);
+        basePacket.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6);
+        basePacket.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_ROUTER_ADVERTISEMENT);
+        basePacket.putShort(ICMP6_RA_ROUTER_LIFETIME_OFFSET, (short)ROUTER_LIFETIME);
+        basePacket.position(IPV6_DEST_ADDR_OFFSET);
+        basePacket.put(IPV6_ALL_NODES_ADDRESS);
+        assertPass(program, basePacket.array());
+
+        verifyRaLifetime(apfFilter, ipClientCallback, basePacket, ROUTER_LIFETIME);
+        verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, -1, -1));
+
+        ByteBuffer newFlowLabelPacket = ByteBuffer.wrap(new byte[ICMP6_RA_OPTION_OFFSET]);
+        basePacket.clear();
+        newFlowLabelPacket.put(basePacket);
+        // Check that changes are ignored in every byte of the flow label.
+        newFlowLabelPacket.putInt(VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET,
+                VERSION_TRAFFIC_CLASS_FLOW_LABEL + 0x11111);
+
+        // Ensure zero-length options cause the packet to be silently skipped.
+        // Do this before we test other packets. http://b/29586253
+        ByteBuffer zeroLengthOptionPacket = ByteBuffer.wrap(
+                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]);
+        basePacket.clear();
+        zeroLengthOptionPacket.put(basePacket);
+        zeroLengthOptionPacket.put((byte)ICMP6_PREFIX_OPTION_TYPE);
+        zeroLengthOptionPacket.put((byte)0);
+        assertInvalidRa(apfFilter, ipClientCallback, zeroLengthOptionPacket);
+
+        // Generate several RAs with different options and lifetimes, and verify when
+        // ApfFilter is shown these packets, it generates programs to filter them for the
+        // appropriate lifetime.
+        ByteBuffer prefixOptionPacket = ByteBuffer.wrap(
+                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_PREFIX_OPTION_LEN]);
+        basePacket.clear();
+        prefixOptionPacket.put(basePacket);
+        prefixOptionPacket.put((byte)ICMP6_PREFIX_OPTION_TYPE);
+        prefixOptionPacket.put((byte)(ICMP6_PREFIX_OPTION_LEN / 8));
+        prefixOptionPacket.putInt(
+                ICMP6_RA_OPTION_OFFSET + ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET,
+                PREFIX_PREFERRED_LIFETIME);
+        prefixOptionPacket.putInt(
+                ICMP6_RA_OPTION_OFFSET + ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET,
+                PREFIX_VALID_LIFETIME);
+        verifyRaLifetime(
+                apfFilter, ipClientCallback, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME);
+        verifyRaEvent(new RaEvent(
+                ROUTER_LIFETIME, PREFIX_VALID_LIFETIME, PREFIX_PREFERRED_LIFETIME, -1, -1, -1));
+
+        ByteBuffer rdnssOptionPacket = ByteBuffer.wrap(
+                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]);
+        basePacket.clear();
+        rdnssOptionPacket.put(basePacket);
+        rdnssOptionPacket.put((byte)ICMP6_RDNSS_OPTION_TYPE);
+        rdnssOptionPacket.put((byte)(ICMP6_4_BYTE_OPTION_LEN / 8));
+        rdnssOptionPacket.putInt(
+                ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_LIFETIME_OFFSET, RDNSS_LIFETIME);
+        verifyRaLifetime(apfFilter, ipClientCallback, rdnssOptionPacket, RDNSS_LIFETIME);
+        verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, RDNSS_LIFETIME, -1));
+
+        ByteBuffer routeInfoOptionPacket = ByteBuffer.wrap(
+                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]);
+        basePacket.clear();
+        routeInfoOptionPacket.put(basePacket);
+        routeInfoOptionPacket.put((byte)ICMP6_ROUTE_INFO_OPTION_TYPE);
+        routeInfoOptionPacket.put((byte)(ICMP6_4_BYTE_OPTION_LEN / 8));
+        routeInfoOptionPacket.putInt(
+                ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_LIFETIME_OFFSET, ROUTE_LIFETIME);
+        verifyRaLifetime(apfFilter, ipClientCallback, routeInfoOptionPacket, ROUTE_LIFETIME);
+        verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, ROUTE_LIFETIME, -1, -1));
+
+        ByteBuffer dnsslOptionPacket = ByteBuffer.wrap(
+                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]);
+        basePacket.clear();
+        dnsslOptionPacket.put(basePacket);
+        dnsslOptionPacket.put((byte)ICMP6_DNSSL_OPTION_TYPE);
+        dnsslOptionPacket.put((byte)(ICMP6_4_BYTE_OPTION_LEN / 8));
+        dnsslOptionPacket.putInt(
+                ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_LIFETIME_OFFSET, DNSSL_LIFETIME);
+        verifyRaLifetime(apfFilter, ipClientCallback, dnsslOptionPacket, ROUTER_LIFETIME);
+        verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, -1, DNSSL_LIFETIME));
+
+        // Verify that current program filters all five RAs:
+        program = ipClientCallback.getApfProgram();
+        verifyRaLifetime(program, basePacket, ROUTER_LIFETIME);
+        verifyRaLifetime(program, newFlowLabelPacket, ROUTER_LIFETIME);
+        verifyRaLifetime(program, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME);
+        verifyRaLifetime(program, rdnssOptionPacket, RDNSS_LIFETIME);
+        verifyRaLifetime(program, routeInfoOptionPacket, ROUTE_LIFETIME);
+        verifyRaLifetime(program, dnsslOptionPacket, ROUTER_LIFETIME);
+
+        apfFilter.shutdown();
+    }
+
+    /**
+     * Stage a file for testing, i.e. make it native accessible. Given a resource ID,
+     * copy that resource into the app's data directory and return the path to it.
+     */
+    private String stageFile(int rawId) throws Exception {
+        File file = new File(InstrumentationRegistry.getContext().getFilesDir(), "staged_file");
+        new File(file.getParent()).mkdirs();
+        InputStream in = null;
+        OutputStream out = null;
+        try {
+            in = InstrumentationRegistry.getContext().getResources().openRawResource(rawId);
+            out = new FileOutputStream(file);
+            Streams.copy(in, out);
+        } finally {
+            if (in != null) in.close();
+            if (out != null) out.close();
+        }
+        return file.getAbsolutePath();
+    }
+
+    private static void put(ByteBuffer buffer, int position, byte[] bytes) {
+        final int original = buffer.position();
+        buffer.position(position);
+        buffer.put(bytes);
+        buffer.position(original);
+    }
+
+    @Test
+    public void testRaParsing() throws Exception {
+        final int maxRandomPacketSize = 512;
+        final Random r = new Random();
+        MockIpClientCallback cb = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog);
+        for (int i = 0; i < 1000; i++) {
+            byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
+            r.nextBytes(packet);
+            try {
+                apfFilter.new Ra(packet, packet.length);
+            } catch (ApfFilter.InvalidRaException e) {
+            } catch (Exception e) {
+                throw new Exception("bad packet: " + HexDump.toHexString(packet), e);
+            }
+        }
+    }
+
+    @Test
+    public void testRaProcessing() throws Exception {
+        final int maxRandomPacketSize = 512;
+        final Random r = new Random();
+        MockIpClientCallback cb = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog);
+        for (int i = 0; i < 1000; i++) {
+            byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
+            r.nextBytes(packet);
+            try {
+                apfFilter.processRa(packet, packet.length);
+            } catch (Exception e) {
+                throw new Exception("bad packet: " + HexDump.toHexString(packet), e);
+            }
+        }
+    }
+
+    /**
+     * Call the APF interpreter to run {@code program} on {@code packet} with persistent memory
+     * segment {@data} pretending the filter was installed {@code filter_age} seconds ago.
+     */
+    private native static int apfSimulate(byte[] program, byte[] packet, byte[] data,
+        int filter_age);
+
+    /**
+     * Compile a tcpdump human-readable filter (e.g. "icmp" or "tcp port 54") into a BPF
+     * prorgam and return a human-readable dump of the BPF program identical to "tcpdump -d".
+     */
+    private native static String compileToBpf(String filter);
+
+    /**
+     * Open packet capture file {@code pcap_filename} and filter the packets using tcpdump
+     * human-readable filter (e.g. "icmp" or "tcp port 54") compiled to a BPF program and
+     * at the same time using APF program {@code apf_program}.  Return {@code true} if
+     * both APF and BPF programs filter out exactly the same packets.
+     */
+    private native static boolean compareBpfApf(String filter, String pcap_filename,
+            byte[] apf_program);
+
+
+    /**
+     * Open packet capture file {@code pcapFilename} and run it through APF filter. Then
+     * checks whether all the packets are dropped and populates data[] {@code data} with
+     * the APF counters.
+     */
+    private native static boolean dropsAllPackets(byte[] program, byte[] data, String pcapFilename);
+
+    @Test
+    public void testBroadcastAddress() throws Exception {
+        assertEqualsIp("255.255.255.255", ApfFilter.ipv4BroadcastAddress(IPV4_ANY_HOST_ADDR, 0));
+        assertEqualsIp("0.0.0.0", ApfFilter.ipv4BroadcastAddress(IPV4_ANY_HOST_ADDR, 32));
+        assertEqualsIp("0.0.3.255", ApfFilter.ipv4BroadcastAddress(IPV4_ANY_HOST_ADDR, 22));
+        assertEqualsIp("0.255.255.255", ApfFilter.ipv4BroadcastAddress(IPV4_ANY_HOST_ADDR, 8));
+
+        assertEqualsIp("255.255.255.255", ApfFilter.ipv4BroadcastAddress(MOCK_IPV4_ADDR, 0));
+        assertEqualsIp("10.0.0.1", ApfFilter.ipv4BroadcastAddress(MOCK_IPV4_ADDR, 32));
+        assertEqualsIp("10.0.0.255", ApfFilter.ipv4BroadcastAddress(MOCK_IPV4_ADDR, 24));
+        assertEqualsIp("10.0.255.255", ApfFilter.ipv4BroadcastAddress(MOCK_IPV4_ADDR, 16));
+    }
+
+    public void assertEqualsIp(String expected, int got) throws Exception {
+        int want = bytesToBEInt(InetAddress.getByName(expected).getAddress());
+        assertEquals(want, got);
+    }
+}
diff --git a/tests/src/android/net/apf/Bpf2Apf.java b/tests/src/android/net/apf/Bpf2Apf.java
new file mode 100644
index 0000000..5d57cde
--- /dev/null
+++ b/tests/src/android/net/apf/Bpf2Apf.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2015 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 android.net.apf;
+
+import android.net.apf.ApfGenerator;
+import android.net.apf.ApfGenerator.IllegalInstructionException;
+import android.net.apf.ApfGenerator.Register;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+
+/**
+ * BPF to APF translator.
+ *
+ * Note: This is for testing purposes only and is not guaranteed to support
+ *       translation of all BPF programs.
+ *
+ * Example usage:
+ *   javac net/java/android/net/apf/ApfGenerator.java \
+ *         tests/servicestests/src/android/net/apf/Bpf2Apf.java
+ *   sudo tcpdump -i em1 -d icmp | java -classpath tests/servicestests/src:net/java \
+ *                                      android.net.apf.Bpf2Apf
+ */
+public class Bpf2Apf {
+    private static int parseImm(String line, String arg) {
+        if (!arg.startsWith("#0x")) {
+            throw new IllegalArgumentException("Unhandled instruction: " + line);
+        }
+        final long val_long = Long.parseLong(arg.substring(3), 16);
+        if (val_long < 0 || val_long > Long.parseLong("ffffffff", 16)) {
+            throw new IllegalArgumentException("Unhandled instruction: " + line);
+        }
+        return new Long((val_long << 32) >> 32).intValue();
+    }
+
+    /**
+     * Convert a single line of "tcpdump -d" (human readable BPF program dump) {@code line} into
+     * APF instruction(s) and append them to {@code gen}. Here's an example line:
+     * (001) jeq      #0x86dd          jt 2    jf 7
+     */
+    private static void convertLine(String line, ApfGenerator gen)
+            throws IllegalInstructionException {
+        if (line.indexOf("(") != 0 || line.indexOf(")") != 4 || line.indexOf(" ") != 5) {
+            throw new IllegalArgumentException("Unhandled instruction: " + line);
+        }
+        int label = Integer.parseInt(line.substring(1, 4));
+        gen.defineLabel(Integer.toString(label));
+        String opcode = line.substring(6, 10).trim();
+        String arg = line.substring(15, Math.min(32, line.length())).trim();
+        switch (opcode) {
+            case "ld":
+            case "ldh":
+            case "ldb":
+            case "ldx":
+            case "ldxb":
+            case "ldxh":
+                Register dest = opcode.contains("x") ? Register.R1 : Register.R0;
+                if (arg.equals("4*([14]&0xf)")) {
+                    if (!opcode.equals("ldxb")) {
+                        throw new IllegalArgumentException("Unhandled instruction: " + line);
+                    }
+                    gen.addLoadFromMemory(dest, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+                    break;
+                }
+                if (arg.equals("#pktlen")) {
+                    if (!opcode.equals("ld")) {
+                        throw new IllegalArgumentException("Unhandled instruction: " + line);
+                    }
+                    gen.addLoadFromMemory(dest, gen.PACKET_SIZE_MEMORY_SLOT);
+                    break;
+                }
+                if (arg.startsWith("#0x")) {
+                    if (!opcode.equals("ld")) {
+                        throw new IllegalArgumentException("Unhandled instruction: " + line);
+                    }
+                    gen.addLoadImmediate(dest, parseImm(line, arg));
+                    break;
+                }
+                if (arg.startsWith("M[")) {
+                    if (!opcode.startsWith("ld")) {
+                        throw new IllegalArgumentException("Unhandled instruction: " + line);
+                    }
+                    int memory_slot = Integer.parseInt(arg.substring(2, arg.length() - 1));
+                    if (memory_slot < 0 || memory_slot >= gen.MEMORY_SLOTS ||
+                            // Disallow use of pre-filled slots as BPF programs might
+                            // wrongfully assume they're initialized to 0.
+                            (memory_slot >= gen.FIRST_PREFILLED_MEMORY_SLOT &&
+                                    memory_slot <= gen.LAST_PREFILLED_MEMORY_SLOT)) {
+                        throw new IllegalArgumentException("Unhandled instruction: " + line);
+                    }
+                    gen.addLoadFromMemory(dest, memory_slot);
+                    break;
+                }
+                if (arg.startsWith("[x + ")) {
+                    int offset = Integer.parseInt(arg.substring(5, arg.length() - 1));
+                    switch (opcode) {
+                        case "ld":
+                        case "ldx":
+                            gen.addLoad32Indexed(dest, offset);
+                            break;
+                        case "ldh":
+                        case "ldxh":
+                            gen.addLoad16Indexed(dest, offset);
+                            break;
+                        case "ldb":
+                        case "ldxb":
+                            gen.addLoad8Indexed(dest, offset);
+                            break;
+                    }
+                } else {
+                    int offset = Integer.parseInt(arg.substring(1, arg.length() - 1));
+                    switch (opcode) {
+                        case "ld":
+                        case "ldx":
+                            gen.addLoad32(dest, offset);
+                            break;
+                        case "ldh":
+                        case "ldxh":
+                            gen.addLoad16(dest, offset);
+                            break;
+                        case "ldb":
+                        case "ldxb":
+                            gen.addLoad8(dest, offset);
+                            break;
+                    }
+                }
+                break;
+            case "st":
+            case "stx":
+                Register src = opcode.contains("x") ? Register.R1 : Register.R0;
+                if (!arg.startsWith("M[")) {
+                    throw new IllegalArgumentException("Unhandled instruction: " + line);
+                }
+                int memory_slot = Integer.parseInt(arg.substring(2, arg.length() - 1));
+                if (memory_slot < 0 || memory_slot >= gen.MEMORY_SLOTS ||
+                        // Disallow overwriting pre-filled slots
+                        (memory_slot >= gen.FIRST_PREFILLED_MEMORY_SLOT &&
+                                memory_slot <= gen.LAST_PREFILLED_MEMORY_SLOT)) {
+                    throw new IllegalArgumentException("Unhandled instruction: " + line);
+                }
+                gen.addStoreToMemory(src, memory_slot);
+                break;
+            case "add":
+            case "and":
+            case "or":
+            case "sub":
+                if (arg.equals("x")) {
+                    switch(opcode) {
+                        case "add":
+                            gen.addAddR1();
+                            break;
+                        case "and":
+                            gen.addAndR1();
+                            break;
+                        case "or":
+                            gen.addOrR1();
+                            break;
+                        case "sub":
+                            gen.addNeg(Register.R1);
+                            gen.addAddR1();
+                            gen.addNeg(Register.R1);
+                            break;
+                    }
+                } else {
+                    int imm = parseImm(line, arg);
+                    switch(opcode) {
+                        case "add":
+                            gen.addAdd(imm);
+                            break;
+                        case "and":
+                            gen.addAnd(imm);
+                            break;
+                        case "or":
+                            gen.addOr(imm);
+                            break;
+                        case "sub":
+                            gen.addAdd(-imm);
+                            break;
+                    }
+                }
+                break;
+            case "jeq":
+            case "jset":
+            case "jgt":
+            case "jge":
+                int val = 0;
+                boolean reg_compare;
+                if (arg.startsWith("x")) {
+                    reg_compare = true;
+                } else {
+                    reg_compare = false;
+                    val = parseImm(line, arg);
+                }
+                int jt_offset = line.indexOf("jt");
+                int jf_offset = line.indexOf("jf");
+                String true_label = line.substring(jt_offset + 2, jf_offset).trim();
+                String false_label = line.substring(jf_offset + 2).trim();
+                boolean true_label_is_fallthrough = Integer.parseInt(true_label) == label + 1;
+                boolean false_label_is_fallthrough = Integer.parseInt(false_label) == label + 1;
+                if (true_label_is_fallthrough && false_label_is_fallthrough)
+                    break;
+                switch (opcode) {
+                    case "jeq":
+                        if (!true_label_is_fallthrough) {
+                            if (reg_compare) {
+                                gen.addJumpIfR0EqualsR1(true_label);
+                            } else {
+                                gen.addJumpIfR0Equals(val, true_label);
+                            }
+                        }
+                        if (!false_label_is_fallthrough) {
+                            if (!true_label_is_fallthrough) {
+                                gen.addJump(false_label);
+                            } else if (reg_compare) {
+                                gen.addJumpIfR0NotEqualsR1(false_label);
+                            } else {
+                                gen.addJumpIfR0NotEquals(val, false_label);
+                            }
+                        }
+                        break;
+                    case "jset":
+                        if (reg_compare) {
+                            gen.addJumpIfR0AnyBitsSetR1(true_label);
+                        } else {
+                            gen.addJumpIfR0AnyBitsSet(val, true_label);
+                        }
+                        if (!false_label_is_fallthrough) {
+                            gen.addJump(false_label);
+                        }
+                        break;
+                    case "jgt":
+                        if (!true_label_is_fallthrough ||
+                                // We have no less-than-or-equal-to register to register
+                                // comparison instruction, so in this case we'll jump
+                                // around an unconditional jump.
+                                (!false_label_is_fallthrough && reg_compare)) {
+                            if (reg_compare) {
+                                gen.addJumpIfR0GreaterThanR1(true_label);
+                            } else {
+                                gen.addJumpIfR0GreaterThan(val, true_label);
+                            }
+                        }
+                        if (!false_label_is_fallthrough) {
+                            if (!true_label_is_fallthrough || reg_compare) {
+                                gen.addJump(false_label);
+                            } else {
+                                gen.addJumpIfR0LessThan(val + 1, false_label);
+                            }
+                        }
+                        break;
+                    case "jge":
+                        if (!false_label_is_fallthrough ||
+                                // We have no greater-than-or-equal-to register to register
+                                // comparison instruction, so in this case we'll jump
+                                // around an unconditional jump.
+                                (!true_label_is_fallthrough && reg_compare)) {
+                            if (reg_compare) {
+                                gen.addJumpIfR0LessThanR1(false_label);
+                            } else {
+                                gen.addJumpIfR0LessThan(val, false_label);
+                            }
+                        }
+                        if (!true_label_is_fallthrough) {
+                            if (!false_label_is_fallthrough || reg_compare) {
+                                gen.addJump(true_label);
+                            } else {
+                                gen.addJumpIfR0GreaterThan(val - 1, true_label);
+                            }
+                        }
+                        break;
+                }
+                break;
+            case "ret":
+                if (arg.equals("#0")) {
+                    gen.addJump(gen.DROP_LABEL);
+                } else {
+                    gen.addJump(gen.PASS_LABEL);
+                }
+                break;
+            case "tax":
+                gen.addMove(Register.R1);
+                break;
+            case "txa":
+                gen.addMove(Register.R0);
+                break;
+            default:
+                throw new IllegalArgumentException("Unhandled instruction: " + line);
+        }
+    }
+
+    /**
+     * Convert the output of "tcpdump -d" (human readable BPF program dump) {@code bpf} into an APF
+     * program and return it.
+     */
+    public static byte[] convert(String bpf) throws IllegalInstructionException {
+        ApfGenerator gen = new ApfGenerator(3);
+        for (String line : bpf.split("\\n")) convertLine(line, gen);
+        return gen.generate();
+    }
+
+    /**
+     * Convert the output of "tcpdump -d" (human readable BPF program dump) piped in stdin into an
+     * APF program and output it via stdout.
+     */
+    public static void main(String[] args) throws Exception {
+        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
+        String line = null;
+        StringBuilder responseData = new StringBuilder();
+        ApfGenerator gen = new ApfGenerator(3);
+        while ((line = in.readLine()) != null) convertLine(line, gen);
+        System.out.write(gen.generate());
+    }
+}
diff --git a/tests/src/android/net/dhcp/DhcpPacketTest.java b/tests/src/android/net/dhcp/DhcpPacketTest.java
new file mode 100644
index 0000000..a592809
--- /dev/null
+++ b/tests/src/android/net/dhcp/DhcpPacketTest.java
@@ -0,0 +1,1075 @@
+/*
+ * Copyright (C) 2015 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 android.net.dhcp;
+
+import static android.net.NetworkUtils.getBroadcastAddress;
+import static android.net.NetworkUtils.getPrefixMaskAsInet4Address;
+import static android.net.dhcp.DhcpPacket.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.annotation.Nullable;
+import android.net.DhcpResults;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+import android.net.metrics.DhcpErrorEvent;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.filters.SmallTest;
+
+import com.android.internal.util.HexDump;
+
+import java.io.ByteArrayOutputStream;
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Random;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DhcpPacketTest {
+
+    private static final Inet4Address SERVER_ADDR = v4Address("192.0.2.1");
+    private static final Inet4Address CLIENT_ADDR = v4Address("192.0.2.234");
+    private static final int PREFIX_LENGTH = 22;
+    private static final Inet4Address NETMASK = getPrefixMaskAsInet4Address(PREFIX_LENGTH);
+    private static final Inet4Address BROADCAST_ADDR = getBroadcastAddress(
+            SERVER_ADDR, PREFIX_LENGTH);
+    private static final String HOSTNAME = "testhostname";
+    private static final short MTU = 1500;
+    // Use our own empty address instead of Inet4Address.ANY or INADDR_ANY to ensure that the code
+    // doesn't use == instead of equals when comparing addresses.
+    private static final Inet4Address ANY = (Inet4Address) v4Address("0.0.0.0");
+
+    private static final byte[] CLIENT_MAC = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
+
+    private static final Inet4Address v4Address(String addrString) throws IllegalArgumentException {
+        return (Inet4Address) NetworkUtils.numericToInetAddress(addrString);
+    }
+
+    @Before
+    public void setUp() {
+        DhcpPacket.testOverrideVendorId = "android-dhcp-???";
+        DhcpPacket.testOverrideHostname = "android-01234567890abcde";
+    }
+
+    class TestDhcpPacket extends DhcpPacket {
+        private byte mType;
+        // TODO: Make this a map of option numbers to bytes instead.
+        private byte[] mDomainBytes, mVendorInfoBytes, mLeaseTimeBytes, mNetmaskBytes;
+
+        public TestDhcpPacket(byte type, Inet4Address clientIp, Inet4Address yourIp) {
+            super(0xdeadbeef, (short) 0, clientIp, yourIp, INADDR_ANY, INADDR_ANY,
+                  CLIENT_MAC, true);
+            mType = type;
+        }
+
+        public TestDhcpPacket(byte type) {
+            this(type, INADDR_ANY, CLIENT_ADDR);
+        }
+
+        public TestDhcpPacket setDomainBytes(byte[] domainBytes) {
+            mDomainBytes = domainBytes;
+            return this;
+        }
+
+        public TestDhcpPacket setVendorInfoBytes(byte[] vendorInfoBytes) {
+            mVendorInfoBytes = vendorInfoBytes;
+            return this;
+        }
+
+        public TestDhcpPacket setLeaseTimeBytes(byte[] leaseTimeBytes) {
+            mLeaseTimeBytes = leaseTimeBytes;
+            return this;
+        }
+
+        public TestDhcpPacket setNetmaskBytes(byte[] netmaskBytes) {
+            mNetmaskBytes = netmaskBytes;
+            return this;
+        }
+
+        public ByteBuffer buildPacket(int encap, short unusedDestUdp, short unusedSrcUdp) {
+            ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+            fillInPacket(encap, CLIENT_ADDR, SERVER_ADDR,
+                         DHCP_CLIENT, DHCP_SERVER, result, DHCP_BOOTREPLY, false);
+            return result;
+        }
+
+        public void finishPacket(ByteBuffer buffer) {
+            addTlv(buffer, DHCP_MESSAGE_TYPE, mType);
+            if (mDomainBytes != null) {
+                addTlv(buffer, DHCP_DOMAIN_NAME, mDomainBytes);
+            }
+            if (mVendorInfoBytes != null) {
+                addTlv(buffer, DHCP_VENDOR_INFO, mVendorInfoBytes);
+            }
+            if (mLeaseTimeBytes != null) {
+                addTlv(buffer, DHCP_LEASE_TIME, mLeaseTimeBytes);
+            }
+            if (mNetmaskBytes != null) {
+                addTlv(buffer, DHCP_SUBNET_MASK, mNetmaskBytes);
+            }
+            addTlvEnd(buffer);
+        }
+
+        // Convenience method.
+        public ByteBuffer build() {
+            // ENCAP_BOOTP packets don't contain ports, so just pass in 0.
+            ByteBuffer pkt = buildPacket(ENCAP_BOOTP, (short) 0, (short) 0);
+            pkt.flip();
+            return pkt;
+        }
+    }
+
+    private void assertDomainAndVendorInfoParses(
+            String expectedDomain, byte[] domainBytes,
+            String expectedVendorInfo, byte[] vendorInfoBytes) throws Exception {
+        ByteBuffer packet = new TestDhcpPacket(DHCP_MESSAGE_TYPE_OFFER)
+                .setDomainBytes(domainBytes)
+                .setVendorInfoBytes(vendorInfoBytes)
+                .build();
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP);
+        assertEquals(expectedDomain, offerPacket.mDomainName);
+        assertEquals(expectedVendorInfo, offerPacket.mVendorInfo);
+    }
+
+    @Test
+    public void testDomainName() throws Exception {
+        byte[] nullByte = new byte[] { 0x00 };
+        byte[] twoNullBytes = new byte[] { 0x00, 0x00 };
+        byte[] nonNullDomain = new byte[] {
+            (byte) 'g', (byte) 'o', (byte) 'o', (byte) '.', (byte) 'g', (byte) 'l'
+        };
+        byte[] trailingNullDomain = new byte[] {
+            (byte) 'g', (byte) 'o', (byte) 'o', (byte) '.', (byte) 'g', (byte) 'l', 0x00
+        };
+        byte[] embeddedNullsDomain = new byte[] {
+            (byte) 'g', (byte) 'o', (byte) 'o', 0x00, 0x00, (byte) 'g', (byte) 'l'
+        };
+        byte[] metered = "ANDROID_METERED".getBytes("US-ASCII");
+
+        byte[] meteredEmbeddedNull = metered.clone();
+        meteredEmbeddedNull[7] = (char) 0;
+
+        byte[] meteredTrailingNull = metered.clone();
+        meteredTrailingNull[meteredTrailingNull.length - 1] = (char) 0;
+
+        assertDomainAndVendorInfoParses("", nullByte, "\u0000", nullByte);
+        assertDomainAndVendorInfoParses("", twoNullBytes, "\u0000\u0000", twoNullBytes);
+        assertDomainAndVendorInfoParses("goo.gl", nonNullDomain, "ANDROID_METERED", metered);
+        assertDomainAndVendorInfoParses("goo", embeddedNullsDomain,
+                                        "ANDROID\u0000METERED", meteredEmbeddedNull);
+        assertDomainAndVendorInfoParses("goo.gl", trailingNullDomain,
+                                        "ANDROID_METERE\u0000", meteredTrailingNull);
+    }
+
+    private void assertLeaseTimeParses(boolean expectValid, Integer rawLeaseTime,
+            long leaseTimeMillis, byte[] leaseTimeBytes) throws Exception {
+        TestDhcpPacket testPacket = new TestDhcpPacket(DHCP_MESSAGE_TYPE_OFFER);
+        if (leaseTimeBytes != null) {
+            testPacket.setLeaseTimeBytes(leaseTimeBytes);
+        }
+        ByteBuffer packet = testPacket.build();
+        DhcpPacket offerPacket = null;
+
+        if (!expectValid) {
+            try {
+                offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP);
+                fail("Invalid packet parsed successfully: " + offerPacket);
+            } catch (ParseException expected) {
+            }
+            return;
+        }
+
+        offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP);
+        assertNotNull(offerPacket);
+        assertEquals(rawLeaseTime, offerPacket.mLeaseTime);
+        DhcpResults dhcpResults = offerPacket.toDhcpResults();  // Just check this doesn't crash.
+        assertEquals(leaseTimeMillis, offerPacket.getLeaseTimeMillis());
+    }
+
+    @Test
+    public void testLeaseTime() throws Exception {
+        byte[] noLease = null;
+        byte[] tooShortLease = new byte[] { 0x00, 0x00 };
+        byte[] tooLongLease = new byte[] { 0x00, 0x00, 0x00, 60, 0x01 };
+        byte[] zeroLease = new byte[] { 0x00, 0x00, 0x00, 0x00 };
+        byte[] tenSecondLease = new byte[] { 0x00, 0x00, 0x00, 10 };
+        byte[] oneMinuteLease = new byte[] { 0x00, 0x00, 0x00, 60 };
+        byte[] fiveMinuteLease = new byte[] { 0x00, 0x00, 0x01, 0x2c };
+        byte[] oneDayLease = new byte[] { 0x00, 0x01, 0x51, (byte) 0x80 };
+        byte[] maxIntPlusOneLease = new byte[] { (byte) 0x80, 0x00, 0x00, 0x01 };
+        byte[] infiniteLease = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
+
+        assertLeaseTimeParses(true, null, 0, noLease);
+        assertLeaseTimeParses(false, null, 0, tooShortLease);
+        assertLeaseTimeParses(false, null, 0, tooLongLease);
+        assertLeaseTimeParses(true, 0, 60 * 1000, zeroLease);
+        assertLeaseTimeParses(true, 10, 60 * 1000, tenSecondLease);
+        assertLeaseTimeParses(true, 60, 60 * 1000, oneMinuteLease);
+        assertLeaseTimeParses(true, 300, 300 * 1000, fiveMinuteLease);
+        assertLeaseTimeParses(true, 86400, 86400 * 1000, oneDayLease);
+        assertLeaseTimeParses(true, -2147483647, 2147483649L * 1000, maxIntPlusOneLease);
+        assertLeaseTimeParses(true, DhcpPacket.INFINITE_LEASE, 0, infiniteLease);
+    }
+
+    private void checkIpAddress(String expected, Inet4Address clientIp, Inet4Address yourIp,
+                                byte[] netmaskBytes) throws Exception {
+        checkIpAddress(expected, DHCP_MESSAGE_TYPE_OFFER, clientIp, yourIp, netmaskBytes);
+        checkIpAddress(expected, DHCP_MESSAGE_TYPE_ACK, clientIp, yourIp, netmaskBytes);
+    }
+
+    private void checkIpAddress(String expected, byte type,
+                                Inet4Address clientIp, Inet4Address yourIp,
+                                byte[] netmaskBytes) throws Exception {
+        ByteBuffer packet = new TestDhcpPacket(type, clientIp, yourIp)
+                .setNetmaskBytes(netmaskBytes)
+                .build();
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP);
+        DhcpResults results = offerPacket.toDhcpResults();
+
+        if (expected != null) {
+            LinkAddress expectedAddress = new LinkAddress(expected);
+            assertEquals(expectedAddress, results.ipAddress);
+        } else {
+            assertNull(results);
+        }
+    }
+
+    @Test
+    public void testIpAddress() throws Exception {
+        byte[] slash11Netmask = new byte[] { (byte) 0xff, (byte) 0xe0, 0x00, 0x00 };
+        byte[] slash24Netmask = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00 };
+        byte[] invalidNetmask = new byte[] { (byte) 0xff, (byte) 0xfb, (byte) 0xff, 0x00 };
+        Inet4Address example1 = v4Address("192.0.2.1");
+        Inet4Address example2 = v4Address("192.0.2.43");
+
+        // A packet without any addresses is not valid.
+        checkIpAddress(null, ANY, ANY, slash24Netmask);
+
+        // ClientIP is used iff YourIP is not present.
+        checkIpAddress("192.0.2.1/24", example2, example1, slash24Netmask);
+        checkIpAddress("192.0.2.43/11", example2, ANY, slash11Netmask);
+        checkIpAddress("192.0.2.43/11", ANY, example2, slash11Netmask);
+
+        // Invalid netmasks are ignored.
+        checkIpAddress(null, example2, ANY, invalidNetmask);
+
+        // If there is no netmask, implicit netmasks are used.
+        checkIpAddress("192.0.2.43/24", ANY, example2, null);
+    }
+
+    private void assertDhcpResults(String ipAddress, String gateway, String dnsServersString,
+            String domains, String serverAddress, String vendorInfo, int leaseDuration,
+            boolean hasMeteredHint, int mtu, DhcpResults dhcpResults) throws Exception {
+        assertEquals(new LinkAddress(ipAddress), dhcpResults.ipAddress);
+        assertEquals(v4Address(gateway), dhcpResults.gateway);
+
+        String[] dnsServerStrings = dnsServersString.split(",");
+        ArrayList dnsServers = new ArrayList();
+        for (String dnsServerString : dnsServerStrings) {
+            dnsServers.add(v4Address(dnsServerString));
+        }
+        assertEquals(dnsServers, dhcpResults.dnsServers);
+
+        assertEquals(domains, dhcpResults.domains);
+        assertEquals(v4Address(serverAddress), dhcpResults.serverAddress);
+        assertEquals(vendorInfo, dhcpResults.vendorInfo);
+        assertEquals(leaseDuration, dhcpResults.leaseDuration);
+        assertEquals(hasMeteredHint, dhcpResults.hasMeteredHint());
+        assertEquals(mtu, dhcpResults.mtu);
+    }
+
+    @Test
+    public void testOffer1() throws Exception {
+        // TODO: Turn all of these into golden files. This will probably require using
+        // android.support.test.InstrumentationRegistry for obtaining a Context object
+        // to read such golden files, along with an appropriate Android.mk.
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+            // IP header.
+            "451001480000000080118849c0a89003c0a89ff7" +
+            // UDP header.
+            "004300440134dcfa" +
+            // BOOTP header.
+            "02010600c997a63b0000000000000000c0a89ff70000000000000000" +
+            // MAC address.
+            "30766ff2a90c00000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options
+            "638253633501023604c0a89003330400001c200104fffff0000304c0a89ffe06080808080808080404" +
+            "3a0400000e103b040000189cff00000000000000000000"));
+
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
+        assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
+        DhcpResults dhcpResults = offerPacket.toDhcpResults();
+        assertDhcpResults("192.168.159.247/20", "192.168.159.254", "8.8.8.8,8.8.4.4",
+                null, "192.168.144.3", null, 7200, false, 0, dhcpResults);
+    }
+
+    @Test
+    public void testOffer2() throws Exception {
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+            // IP header.
+            "450001518d0600004011144dc0a82b01c0a82bf7" +
+            // UDP header.
+            "00430044013d9ac7" +
+            // BOOTP header.
+            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
+            // MAC address.
+            "30766ff2a90c00000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options
+            "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
+            "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff"));
+
+        assertEquals(337, packet.limit());
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
+        assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
+        DhcpResults dhcpResults = offerPacket.toDhcpResults();
+        assertDhcpResults("192.168.43.247/24", "192.168.43.1", "192.168.43.1",
+                null, "192.168.43.1", "ANDROID_METERED", 3600, true, 0, dhcpResults);
+        assertTrue(dhcpResults.hasMeteredHint());
+    }
+
+    @Test
+    public void testBadIpPacket() throws Exception {
+        final byte[] packet = HexDump.hexStringToByteArray(
+            // IP header.
+            "450001518d0600004011144dc0a82b01c0a82bf7");
+
+        try {
+            DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
+        } catch (DhcpPacket.ParseException expected) {
+            assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode);
+            return;
+        }
+        fail("Dhcp packet parsing should have failed");
+    }
+
+    @Test
+    public void testBadDhcpPacket() throws Exception {
+        final byte[] packet = HexDump.hexStringToByteArray(
+            // IP header.
+            "450001518d0600004011144dc0a82b01c0a82bf7" +
+            // UDP header.
+            "00430044013d9ac7" +
+            // BOOTP header.
+            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000");
+
+        try {
+            DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
+        } catch (DhcpPacket.ParseException expected) {
+            assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode);
+            return;
+        }
+        fail("Dhcp packet parsing should have failed");
+    }
+
+    @Test
+    public void testBadTruncatedOffer() throws Exception {
+        final byte[] packet = HexDump.hexStringToByteArray(
+            // IP header.
+            "450001518d0600004011144dc0a82b01c0a82bf7" +
+            // UDP header.
+            "00430044013d9ac7" +
+            // BOOTP header.
+            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
+            // MAC address.
+            "30766ff2a90c00000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File, missing one byte
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "00000000000000000000000000000000000000000000000000000000000000");
+
+        try {
+            DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
+        } catch (DhcpPacket.ParseException expected) {
+            assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode);
+            return;
+        }
+        fail("Dhcp packet parsing should have failed");
+    }
+
+    @Test
+    public void testBadOfferWithoutACookie() throws Exception {
+        final byte[] packet = HexDump.hexStringToByteArray(
+            // IP header.
+            "450001518d0600004011144dc0a82b01c0a82bf7" +
+            // UDP header.
+            "00430044013d9ac7" +
+            // BOOTP header.
+            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
+            // MAC address.
+            "30766ff2a90c00000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000"
+            // No options
+            );
+
+        try {
+            DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
+        } catch (DhcpPacket.ParseException expected) {
+            assertDhcpErrorCodes(DhcpErrorEvent.DHCP_NO_COOKIE, expected.errorCode);
+            return;
+        }
+        fail("Dhcp packet parsing should have failed");
+    }
+
+    @Test
+    public void testOfferWithBadCookie() throws Exception {
+        final byte[] packet = HexDump.hexStringToByteArray(
+            // IP header.
+            "450001518d0600004011144dc0a82b01c0a82bf7" +
+            // UDP header.
+            "00430044013d9ac7" +
+            // BOOTP header.
+            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
+            // MAC address.
+            "30766ff2a90c00000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Bad cookie
+            "DEADBEEF3501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
+            "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff");
+
+        try {
+            DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
+        } catch (DhcpPacket.ParseException expected) {
+            assertDhcpErrorCodes(DhcpErrorEvent.DHCP_BAD_MAGIC_COOKIE, expected.errorCode);
+            return;
+        }
+        fail("Dhcp packet parsing should have failed");
+    }
+
+    private void assertDhcpErrorCodes(int expected, int got) {
+        assertEquals(Integer.toHexString(expected), Integer.toHexString(got));
+    }
+
+    @Test
+    public void testTruncatedOfferPackets() throws Exception {
+        final byte[] packet = HexDump.hexStringToByteArray(
+            // IP header.
+            "450001518d0600004011144dc0a82b01c0a82bf7" +
+            // UDP header.
+            "00430044013d9ac7" +
+            // BOOTP header.
+            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
+            // MAC address.
+            "30766ff2a90c00000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options
+            "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
+            "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff");
+
+        for (int len = 0; len < packet.length; len++) {
+            try {
+                DhcpPacket.decodeFullPacket(packet, len, ENCAP_L3);
+            } catch (ParseException e) {
+                if (e.errorCode == DhcpErrorEvent.PARSING_ERROR) {
+                    fail(String.format("bad truncated packet of length %d", len));
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testRandomPackets() throws Exception {
+        final int maxRandomPacketSize = 512;
+        final Random r = new Random();
+        for (int i = 0; i < 10000; i++) {
+            byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
+            r.nextBytes(packet);
+            try {
+                DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
+            } catch (ParseException e) {
+                if (e.errorCode == DhcpErrorEvent.PARSING_ERROR) {
+                    fail("bad packet: " + HexDump.toHexString(packet));
+                }
+            }
+        }
+    }
+
+    private byte[] mtuBytes(int mtu) {
+        // 0x1a02: option 26, length 2. 0xff: no more options.
+        if (mtu > Short.MAX_VALUE - Short.MIN_VALUE) {
+            throw new IllegalArgumentException(
+                String.format("Invalid MTU %d, must be 16-bit unsigned", mtu));
+        }
+        String hexString = String.format("1a02%04xff", mtu);
+        return HexDump.hexStringToByteArray(hexString);
+    }
+
+    private void checkMtu(ByteBuffer packet, int expectedMtu, byte[] mtuBytes) throws Exception {
+        if (mtuBytes != null) {
+            packet.position(packet.capacity() - mtuBytes.length);
+            packet.put(mtuBytes);
+            packet.clear();
+        }
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
+        assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
+        DhcpResults dhcpResults = offerPacket.toDhcpResults();
+        assertDhcpResults("192.168.159.247/20", "192.168.159.254", "8.8.8.8,8.8.4.4",
+                null, "192.168.144.3", null, 7200, false, expectedMtu, dhcpResults);
+    }
+
+    @Test
+    public void testMtu() throws Exception {
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+            // IP header.
+            "451001480000000080118849c0a89003c0a89ff7" +
+            // UDP header.
+            "004300440134dcfa" +
+            // BOOTP header.
+            "02010600c997a63b0000000000000000c0a89ff70000000000000000" +
+            // MAC address.
+            "30766ff2a90c00000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options
+            "638253633501023604c0a89003330400001c200104fffff0000304c0a89ffe06080808080808080404" +
+            "3a0400000e103b040000189cff00000000"));
+
+        checkMtu(packet, 0, null);
+        checkMtu(packet, 0, mtuBytes(1501));
+        checkMtu(packet, 1500, mtuBytes(1500));
+        checkMtu(packet, 1499, mtuBytes(1499));
+        checkMtu(packet, 1280, mtuBytes(1280));
+        checkMtu(packet, 0, mtuBytes(1279));
+        checkMtu(packet, 0, mtuBytes(576));
+        checkMtu(packet, 0, mtuBytes(68));
+        checkMtu(packet, 0, mtuBytes(Short.MIN_VALUE));
+        checkMtu(packet, 0, mtuBytes(Short.MAX_VALUE + 3));
+        checkMtu(packet, 0, mtuBytes(-1));
+    }
+
+    @Test
+    public void testBadHwaddrLength() throws Exception {
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+            // IP header.
+            "450001518d0600004011144dc0a82b01c0a82bf7" +
+            // UDP header.
+            "00430044013d9ac7" +
+            // BOOTP header.
+            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
+            // MAC address.
+            "30766ff2a90c00000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options
+            "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
+            "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff"));
+        String expectedClientMac = "30766FF2A90C";
+
+        final int hwAddrLenOffset = 20 + 8 + 2;
+        assertEquals(6, packet.get(hwAddrLenOffset));
+
+        // Expect the expected.
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
+        assertNotNull(offerPacket);
+        assertEquals(6, offerPacket.getClientMac().length);
+        assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac()));
+
+        // Reduce the hardware address length and verify that it shortens the client MAC.
+        packet.flip();
+        packet.put(hwAddrLenOffset, (byte) 5);
+        offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
+        assertNotNull(offerPacket);
+        assertEquals(5, offerPacket.getClientMac().length);
+        assertEquals(expectedClientMac.substring(0, 10),
+                HexDump.toHexString(offerPacket.getClientMac()));
+
+        packet.flip();
+        packet.put(hwAddrLenOffset, (byte) 3);
+        offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
+        assertNotNull(offerPacket);
+        assertEquals(3, offerPacket.getClientMac().length);
+        assertEquals(expectedClientMac.substring(0, 6),
+                HexDump.toHexString(offerPacket.getClientMac()));
+
+        // Set the the hardware address length to 0xff and verify that we a) don't treat it as -1
+        // and crash, and b) hardcode it to 6.
+        packet.flip();
+        packet.put(hwAddrLenOffset, (byte) -1);
+        offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
+        assertNotNull(offerPacket);
+        assertEquals(6, offerPacket.getClientMac().length);
+        assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac()));
+
+        // Set the the hardware address length to a positive invalid value (> 16) and verify that we
+        // hardcode it to 6.
+        packet.flip();
+        packet.put(hwAddrLenOffset, (byte) 17);
+        offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
+        assertNotNull(offerPacket);
+        assertEquals(6, offerPacket.getClientMac().length);
+        assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac()));
+    }
+
+    @Test
+    public void testPadAndOverloadedOptionsOffer() throws Exception {
+        // A packet observed in the real world that is interesting for two reasons:
+        //
+        // 1. It uses pad bytes, which we previously didn't support correctly.
+        // 2. It uses DHCP option overloading, which we don't currently support (but it doesn't
+        //    store any information in the overloaded fields).
+        //
+        // For now, we just check that it parses correctly.
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+            // Ethernet header.
+            "b4cef6000000e80462236e300800" +
+            // IP header.
+            "4500014c00000000ff11741701010101ac119876" +
+            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
+            "004300440138ae5a" +
+            // BOOTP header.
+            "020106000fa0059f0000000000000000ac1198760000000000000000" +
+            // MAC address.
+            "b4cef600000000000000000000000000" +
+            // Server name.
+            "ff00000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "ff00000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options
+            "638253633501023604010101010104ffff000033040000a8c03401030304ac1101010604ac110101" +
+            "0000000000000000000000000000000000000000000000ff000000"));
+
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
+        assertTrue(offerPacket instanceof DhcpOfferPacket);
+        DhcpResults dhcpResults = offerPacket.toDhcpResults();
+        assertDhcpResults("172.17.152.118/16", "172.17.1.1", "172.17.1.1",
+                null, "1.1.1.1", null, 43200, false, 0, dhcpResults);
+    }
+
+    @Test
+    public void testBug2111() throws Exception {
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+            // IP header.
+            "4500014c00000000ff119beac3eaf3880a3f5d04" +
+            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
+            "0043004401387464" +
+            // BOOTP header.
+            "0201060002554812000a0000000000000a3f5d040000000000000000" +
+            // MAC address.
+            "00904c00000000000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options.
+            "638253633501023604c00002fe33040000bfc60104fffff00003040a3f50010608c0000201c0000202" +
+            "0f0f646f6d61696e3132332e636f2e756b0000000000ff00000000"));
+
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
+        assertTrue(offerPacket instanceof DhcpOfferPacket);
+        DhcpResults dhcpResults = offerPacket.toDhcpResults();
+        assertDhcpResults("10.63.93.4/20", "10.63.80.1", "192.0.2.1,192.0.2.2",
+                "domain123.co.uk", "192.0.2.254", null, 49094, false, 0, dhcpResults);
+    }
+
+    @Test
+    public void testBug2136() throws Exception {
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+            // Ethernet header.
+            "bcf5ac000000d0c7890000000800" +
+            // IP header.
+            "4500014c00000000ff119beac3eaf3880a3f5d04" +
+            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
+            "0043004401387574" +
+            // BOOTP header.
+            "0201060163339a3000050000000000000a209ecd0000000000000000" +
+            // MAC address.
+            "bcf5ac00000000000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options.
+            "6382536335010236040a20ff80330400001c200104fffff00003040a20900106089458413494584135" +
+            "0f0b6c616e63732e61632e756b000000000000000000ff00000000"));
+
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
+        assertTrue(offerPacket instanceof DhcpOfferPacket);
+        assertEquals("BCF5AC000000", HexDump.toHexString(offerPacket.getClientMac()));
+        DhcpResults dhcpResults = offerPacket.toDhcpResults();
+        assertDhcpResults("10.32.158.205/20", "10.32.144.1", "148.88.65.52,148.88.65.53",
+                "lancs.ac.uk", "10.32.255.128", null, 7200, false, 0, dhcpResults);
+    }
+
+    @Test
+    public void testUdpServerAnySourcePort() throws Exception {
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+            // Ethernet header.
+            "9cd917000000001c2e0000000800" +
+            // IP header.
+            "45a00148000040003d115087d18194fb0a0f7af2" +
+            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
+            // NOTE: The server source port is not the canonical port 67.
+            "C29F004401341268" +
+            // BOOTP header.
+            "02010600d628ba8200000000000000000a0f7af2000000000a0fc818" +
+            // MAC address.
+            "9cd91700000000000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options.
+            "6382536335010236040a0169fc3304000151800104ffff000003040a0fc817060cd1818003d1819403" +
+            "d18180060f0777766d2e6564751c040a0fffffff000000"));
+
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
+        assertTrue(offerPacket instanceof DhcpOfferPacket);
+        assertEquals("9CD917000000", HexDump.toHexString(offerPacket.getClientMac()));
+        DhcpResults dhcpResults = offerPacket.toDhcpResults();
+        assertDhcpResults("10.15.122.242/16", "10.15.200.23",
+                "209.129.128.3,209.129.148.3,209.129.128.6",
+                "wvm.edu", "10.1.105.252", null, 86400, false, 0, dhcpResults);
+    }
+
+    @Test
+    public void testUdpInvalidDstPort() throws Exception {
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+            // Ethernet header.
+            "9cd917000000001c2e0000000800" +
+            // IP header.
+            "45a00148000040003d115087d18194fb0a0f7af2" +
+            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
+            // NOTE: The destination port is a non-DHCP port.
+            "0043aaaa01341268" +
+            // BOOTP header.
+            "02010600d628ba8200000000000000000a0f7af2000000000a0fc818" +
+            // MAC address.
+            "9cd91700000000000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options.
+            "6382536335010236040a0169fc3304000151800104ffff000003040a0fc817060cd1818003d1819403" +
+            "d18180060f0777766d2e6564751c040a0fffffff000000"));
+
+        try {
+            DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
+            fail("Packet with invalid dst port did not throw ParseException");
+        } catch (ParseException expected) {}
+    }
+
+    @Test
+    public void testMultipleRouters() throws Exception {
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+            // Ethernet header.
+            "fc3d93000000" + "081735000000" + "0800" +
+            // IP header.
+            "45000148c2370000ff117ac2c0a8bd02ffffffff" +
+            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
+            "0043004401343beb" +
+            // BOOTP header.
+            "0201060027f518e20000800000000000c0a8bd310000000000000000" +
+            // MAC address.
+            "fc3d9300000000000000000000000000" +
+            // Server name.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // File.
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            "0000000000000000000000000000000000000000000000000000000000000000" +
+            // Options.
+            "638253633501023604c0abbd023304000070803a04000038403b04000062700104ffffff00" +
+            "0308c0a8bd01ffffff0006080808080808080404ff000000000000"));
+
+        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
+        assertTrue(offerPacket instanceof DhcpOfferPacket);
+        assertEquals("FC3D93000000", HexDump.toHexString(offerPacket.getClientMac()));
+        DhcpResults dhcpResults = offerPacket.toDhcpResults();
+        assertDhcpResults("192.168.189.49/24", "192.168.189.1", "8.8.8.8,8.8.4.4",
+                null, "192.171.189.2", null, 28800, false, 0, dhcpResults);
+    }
+
+    @Test
+    public void testDiscoverPacket() throws Exception {
+        short secs = 7;
+        int transactionId = 0xdeadbeef;
+        byte[] hwaddr = {
+                (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a
+        };
+
+        ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
+                DhcpPacket.ENCAP_L2, transactionId, secs, hwaddr,
+                false /* do unicast */, DhcpClient.REQUESTED_PARAMS);
+
+        byte[] headers = new byte[] {
+            // Ethernet header.
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+            (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a,
+            (byte) 0x08, (byte) 0x00,
+            // IP header.
+            (byte) 0x45, (byte) 0x10, (byte) 0x01, (byte) 0x56,
+            (byte) 0x00, (byte) 0x00, (byte) 0x40, (byte) 0x00,
+            (byte) 0x40, (byte) 0x11, (byte) 0x39, (byte) 0x88,
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+            // UDP header.
+            (byte) 0x00, (byte) 0x44, (byte) 0x00, (byte) 0x43,
+            (byte) 0x01, (byte) 0x42, (byte) 0x6a, (byte) 0x4a,
+            // BOOTP.
+            (byte) 0x01, (byte) 0x01, (byte) 0x06, (byte) 0x00,
+            (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef,
+            (byte) 0x00, (byte) 0x07, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b,
+            (byte) 0xb1, (byte) 0x7a
+        };
+        byte[] options = new byte[] {
+            // Magic cookie 0x63825363.
+            (byte) 0x63, (byte) 0x82, (byte) 0x53, (byte) 0x63,
+            // Message type DISCOVER.
+            (byte) 0x35, (byte) 0x01, (byte) 0x01,
+            // Client identifier Ethernet, da:01:19:5b:b1:7a.
+            (byte) 0x3d, (byte) 0x07,
+                    (byte) 0x01,
+                    (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a,
+            // Max message size 1500.
+            (byte) 0x39, (byte) 0x02, (byte) 0x05, (byte) 0xdc,
+            // Version "android-dhcp-???".
+            (byte) 0x3c, (byte) 0x10,
+                    'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 'h', 'c', 'p', '-', '?', '?', '?',
+            // Hostname "android-01234567890abcde"
+            (byte) 0x0c, (byte) 0x18,
+                    'a', 'n', 'd', 'r', 'o', 'i', 'd', '-',
+                    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e',
+            // Requested parameter list.
+            (byte) 0x37, (byte) 0x0a,
+                DHCP_SUBNET_MASK,
+                DHCP_ROUTER,
+                DHCP_DNS_SERVER,
+                DHCP_DOMAIN_NAME,
+                DHCP_MTU,
+                DHCP_BROADCAST_ADDRESS,
+                DHCP_LEASE_TIME,
+                DHCP_RENEWAL_TIME,
+                DHCP_REBINDING_TIME,
+                DHCP_VENDOR_INFO,
+            // End options.
+            (byte) 0xff,
+            // Our packets are always of even length. TODO: find out why and possibly fix it.
+            (byte) 0x00
+        };
+        byte[] expected = new byte[DhcpPacket.MIN_PACKET_LENGTH_L2 + options.length];
+        assertTrue((expected.length & 1) == 0);
+        System.arraycopy(headers, 0, expected, 0, headers.length);
+        System.arraycopy(options, 0, expected, DhcpPacket.MIN_PACKET_LENGTH_L2, options.length);
+
+        byte[] actual = new byte[packet.limit()];
+        packet.get(actual);
+        String msg =
+                "Expected:\n  " + Arrays.toString(expected) +
+                "\nActual:\n  " + Arrays.toString(actual);
+        assertTrue(msg, Arrays.equals(expected, actual));
+    }
+
+    public void checkBuildOfferPacket(int leaseTimeSecs, @Nullable String hostname)
+            throws Exception {
+        final int renewalTime = (int) (Integer.toUnsignedLong(leaseTimeSecs) / 2);
+        final int rebindingTime = (int) (Integer.toUnsignedLong(leaseTimeSecs) * 875 / 1000);
+        final int transactionId = 0xdeadbeef;
+
+        final ByteBuffer packet = DhcpPacket.buildOfferPacket(
+                DhcpPacket.ENCAP_BOOTP, transactionId, false /* broadcast */,
+                SERVER_ADDR, INADDR_ANY /* relayIp */, CLIENT_ADDR /* yourIp */,
+                CLIENT_MAC, leaseTimeSecs, NETMASK /* netMask */,
+                BROADCAST_ADDR /* bcAddr */, Collections.singletonList(SERVER_ADDR) /* gateways */,
+                Collections.singletonList(SERVER_ADDR) /* dnsServers */,
+                SERVER_ADDR /* dhcpServerIdentifier */, null /* domainName */, hostname,
+                false /* metered */, MTU);
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        // BOOTP headers
+        bos.write(new byte[] {
+                (byte) 0x02, (byte) 0x01, (byte) 0x06, (byte) 0x00,
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                // ciaddr
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        });
+        // yiaddr
+        bos.write(CLIENT_ADDR.getAddress());
+        // siaddr
+        bos.write(SERVER_ADDR.getAddress());
+        // giaddr
+        bos.write(INADDR_ANY.getAddress());
+        // chaddr
+        bos.write(CLIENT_MAC);
+
+        // Padding
+        bos.write(new byte[202]);
+
+        // Options
+        bos.write(new byte[]{
+                // Magic cookie 0x63825363.
+                (byte) 0x63, (byte) 0x82, (byte) 0x53, (byte) 0x63,
+                // Message type OFFER.
+                (byte) 0x35, (byte) 0x01, (byte) 0x02,
+        });
+        // Server ID
+        bos.write(new byte[] { (byte) 0x36, (byte) 0x04 });
+        bos.write(SERVER_ADDR.getAddress());
+        // Lease time
+        bos.write(new byte[] { (byte) 0x33, (byte) 0x04 });
+        bos.write(intToByteArray(leaseTimeSecs));
+        if (leaseTimeSecs != INFINITE_LEASE) {
+            // Renewal time
+            bos.write(new byte[]{(byte) 0x3a, (byte) 0x04});
+            bos.write(intToByteArray(renewalTime));
+            // Rebinding time
+            bos.write(new byte[]{(byte) 0x3b, (byte) 0x04});
+            bos.write(intToByteArray(rebindingTime));
+        }
+        // Subnet mask
+        bos.write(new byte[] { (byte) 0x01, (byte) 0x04 });
+        bos.write(NETMASK.getAddress());
+        // Broadcast address
+        bos.write(new byte[] { (byte) 0x1c, (byte) 0x04 });
+        bos.write(BROADCAST_ADDR.getAddress());
+        // Router
+        bos.write(new byte[] { (byte) 0x03, (byte) 0x04 });
+        bos.write(SERVER_ADDR.getAddress());
+        // Nameserver
+        bos.write(new byte[] { (byte) 0x06, (byte) 0x04 });
+        bos.write(SERVER_ADDR.getAddress());
+        // Hostname
+        if (hostname != null) {
+            bos.write(new byte[]{(byte) 0x0c, (byte) hostname.length()});
+            bos.write(hostname.getBytes(Charset.forName("US-ASCII")));
+        }
+        // MTU
+        bos.write(new byte[] { (byte) 0x1a, (byte) 0x02 });
+        bos.write(shortToByteArray(MTU));
+        // End options.
+        bos.write(0xff);
+
+        if ((bos.size() & 1) != 0) {
+            bos.write(0x00);
+        }
+
+        final byte[] expected = bos.toByteArray();
+        final byte[] actual = new byte[packet.limit()];
+        packet.get(actual);
+        final String msg = "Expected:\n  " + HexDump.dumpHexString(expected) +
+                "\nActual:\n  " + HexDump.dumpHexString(actual);
+        assertTrue(msg, Arrays.equals(expected, actual));
+    }
+
+    @Test
+    public void testOfferPacket() throws Exception {
+        checkBuildOfferPacket(3600, HOSTNAME);
+        checkBuildOfferPacket(Integer.MAX_VALUE, HOSTNAME);
+        checkBuildOfferPacket(0x80000000, HOSTNAME);
+        checkBuildOfferPacket(INFINITE_LEASE, HOSTNAME);
+        checkBuildOfferPacket(3600, null);
+    }
+
+    private static byte[] intToByteArray(int val) {
+        return ByteBuffer.allocate(4).putInt(val).array();
+    }
+
+    private static byte[] shortToByteArray(short val) {
+        return ByteBuffer.allocate(2).putShort(val).array();
+    }
+}
diff --git a/tests/src/android/net/ip/IpClientTest.java b/tests/src/android/net/ip/IpClientTest.java
new file mode 100644
index 0000000..f21809f
--- /dev/null
+++ b/tests/src/android/net/ip/IpClientTest.java
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2017 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 android.net.ip;
+
+import static android.net.shared.LinkPropertiesParcelableUtil.fromStableParcelable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.RouteInfo;
+import android.net.shared.InitialConfiguration;
+import android.net.shared.ProvisioningConfiguration;
+import android.net.util.InterfaceParams;
+import android.os.INetworkManagementService;
+import android.provider.Settings;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.mock.MockContentResolver;
+
+import com.android.internal.R;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.server.net.BaseNetworkObserver;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tests for IpClient.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpClientTest {
+    private static final int DEFAULT_AVOIDBADWIFI_CONFIG_VALUE = 1;
+
+    private static final String VALID = "VALID";
+    private static final String INVALID = "INVALID";
+    private static final String TEST_IFNAME = "test_wlan0";
+    private static final int TEST_IFINDEX = 1001;
+    // See RFC 7042#section-2.1.2 for EUI-48 documentation values.
+    private static final MacAddress TEST_MAC = MacAddress.fromString("00:00:5E:00:53:01");
+    private static final int TEST_TIMEOUT_MS = 400;
+
+    @Mock private Context mContext;
+    @Mock private ConnectivityManager mCm;
+    @Mock private INetworkManagementService mNMService;
+    @Mock private INetd mNetd;
+    @Mock private Resources mResources;
+    @Mock private IIpClientCallbacks mCb;
+    @Mock private AlarmManager mAlarm;
+    @Mock private IpClient.Dependencies mDependecies;
+    private MockContentResolver mContentResolver;
+
+    private BaseNetworkObserver mObserver;
+    private InterfaceParams mIfParams;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mContext.getSystemService(eq(Context.ALARM_SERVICE))).thenReturn(mAlarm);
+        when(mContext.getSystemServiceName(ConnectivityManager.class))
+                .thenReturn(Context.CONNECTIVITY_SERVICE);
+        when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mCm);
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getInteger(R.integer.config_networkAvoidBadWifi))
+                .thenReturn(DEFAULT_AVOIDBADWIFI_CONFIG_VALUE);
+
+        mContentResolver = new MockContentResolver();
+        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+
+        mIfParams = null;
+
+        when(mDependecies.getNMS()).thenReturn(mNMService);
+        when(mDependecies.getNetd()).thenReturn(mNetd);
+    }
+
+    private void setTestInterfaceParams(String ifname) {
+        mIfParams = (ifname != null)
+                ? new InterfaceParams(ifname, TEST_IFINDEX, TEST_MAC)
+                : null;
+        when(mDependecies.getInterfaceParams(anyString())).thenReturn(mIfParams);
+    }
+
+    private IpClient makeIpClient(String ifname) throws Exception {
+        setTestInterfaceParams(ifname);
+        final IpClient ipc = new IpClient(mContext, ifname, mCb, mDependecies);
+        verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetEnableIPv6(ifname, false);
+        verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceClearAddrs(ifname);
+        ArgumentCaptor<BaseNetworkObserver> arg =
+                ArgumentCaptor.forClass(BaseNetworkObserver.class);
+        verify(mNMService, times(1)).registerObserver(arg.capture());
+        mObserver = arg.getValue();
+        reset(mNMService);
+        reset(mNetd);
+        // Verify IpClient doesn't call onLinkPropertiesChange() when it starts.
+        verify(mCb, never()).onLinkPropertiesChange(any());
+        reset(mCb);
+        return ipc;
+    }
+
+    private static LinkProperties makeEmptyLinkProperties(String iface) {
+        final LinkProperties empty = new LinkProperties();
+        empty.setInterfaceName(iface);
+        return empty;
+    }
+
+    @Test
+    public void testNullInterfaceNameMostDefinitelyThrows() throws Exception {
+        setTestInterfaceParams(null);
+        try {
+            final IpClient ipc = new IpClient(mContext, null, mCb, mDependecies);
+            ipc.shutdown();
+            fail();
+        } catch (NullPointerException npe) {
+            // Phew; null interface names not allowed.
+        }
+    }
+
+    @Test
+    public void testNullCallbackMostDefinitelyThrows() throws Exception {
+        final String ifname = "lo";
+        setTestInterfaceParams(ifname);
+        try {
+            final IpClient ipc = new IpClient(mContext, ifname, null, mDependecies);
+            ipc.shutdown();
+            fail();
+        } catch (NullPointerException npe) {
+            // Phew; null callbacks not allowed.
+        }
+    }
+
+    @Test
+    public void testInvalidInterfaceDoesNotThrow() throws Exception {
+        setTestInterfaceParams(TEST_IFNAME);
+        final IpClient ipc = new IpClient(mContext, TEST_IFNAME, mCb, mDependecies);
+        ipc.shutdown();
+    }
+
+    @Test
+    public void testInterfaceNotFoundFailsImmediately() throws Exception {
+        setTestInterfaceParams(null);
+        final IpClient ipc = new IpClient(mContext, TEST_IFNAME, mCb, mDependecies);
+        ipc.startProvisioning(new ProvisioningConfiguration());
+        verify(mCb, times(1)).onProvisioningFailure(any());
+        ipc.shutdown();
+    }
+
+    @Test
+    public void testDefaultProvisioningConfiguration() throws Exception {
+        final String iface = TEST_IFNAME;
+        final IpClient ipc = makeIpClient(iface);
+
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                // TODO: mock IpReachabilityMonitor's dependencies (NetworkInterface, PowerManager)
+                // and enable it in this test
+                .withoutIpReachabilityMonitor()
+                .build();
+
+        ipc.startProvisioning(config);
+        verify(mCb, times(1)).setNeighborDiscoveryOffload(true);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setFallbackMulticastFilter(false);
+        verify(mCb, never()).onProvisioningFailure(any());
+
+        ipc.shutdown();
+        verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetEnableIPv6(iface, false);
+        verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceClearAddrs(iface);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).times(1))
+                .onLinkPropertiesChange(argThat(
+                        lp -> fromStableParcelable(lp).equals(makeEmptyLinkProperties(iface))));
+    }
+
+    @Test
+    public void testProvisioningWithInitialConfiguration() throws Exception {
+        final String iface = TEST_IFNAME;
+        final IpClient ipc = makeIpClient(iface);
+
+        String[] addresses = {
+            "fe80::a4be:f92:e1f7:22d1/64",
+            "fe80::f04a:8f6:6a32:d756/64",
+            "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"
+        };
+        String[] prefixes = { "fe80::/64", "fd2c:4e57:8e3c::/64" };
+
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .withInitialConfiguration(conf(links(addresses), prefixes(prefixes), ips()))
+                .build();
+
+        ipc.startProvisioning(config);
+        verify(mCb, times(1)).setNeighborDiscoveryOffload(true);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setFallbackMulticastFilter(false);
+        verify(mCb, never()).onProvisioningFailure(any());
+
+        for (String addr : addresses) {
+            String[] parts = addr.split("/");
+            verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1))
+                    .interfaceAddAddress(iface, parts[0], Integer.parseInt(parts[1]));
+        }
+
+        final int lastAddr = addresses.length - 1;
+
+        // Add N - 1 addresses
+        for (int i = 0; i < lastAddr; i++) {
+            mObserver.addressUpdated(iface, new LinkAddress(addresses[i]));
+            verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(any());
+            reset(mCb);
+        }
+
+        // Add Nth address
+        mObserver.addressUpdated(iface, new LinkAddress(addresses[lastAddr]));
+        LinkProperties want = linkproperties(links(addresses), routes(prefixes));
+        want.setInterfaceName(iface);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).onProvisioningSuccess(argThat(
+                lp -> fromStableParcelable(lp).equals(want)));
+
+        ipc.shutdown();
+        verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetEnableIPv6(iface, false);
+        verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceClearAddrs(iface);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).times(1))
+                .onLinkPropertiesChange(argThat(
+                        lp -> fromStableParcelable(lp).equals(makeEmptyLinkProperties(iface))));
+    }
+
+    @Test
+    public void testIsProvisioned() throws Exception {
+        InitialConfiguration empty = conf(links(), prefixes());
+        IsProvisionedTestCase[] testcases = {
+            // nothing
+            notProvisionedCase(links(), routes(), dns(), null),
+            notProvisionedCase(links(), routes(), dns(), empty),
+
+            // IPv4
+            provisionedCase(links("192.0.2.12/24"), routes(), dns(), empty),
+
+            // IPv6
+            notProvisionedCase(
+                    links("fe80::a4be:f92:e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"),
+                    routes(), dns(), empty),
+            notProvisionedCase(
+                    links("fe80::a4be:f92:e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"),
+                    routes("fe80::/64", "fd2c:4e57:8e3c::/64"), dns("fd00:1234:5678::1000"), empty),
+            provisionedCase(
+                    links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"),
+                    routes("::/0"),
+                    dns("2001:db8:dead:beef:f00::02"), empty),
+
+            // Initial configuration
+            provisionedCase(
+                    links("fe80::e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"),
+                    routes("fe80::/64", "fd2c:4e57:8e3c::/64"),
+                    dns(),
+                    conf(links("fe80::e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"),
+                        prefixes( "fe80::/64", "fd2c:4e57:8e3c::/64"), ips()))
+        };
+
+        for (IsProvisionedTestCase testcase : testcases) {
+            if (IpClient.isProvisioned(testcase.lp, testcase.config) != testcase.isProvisioned) {
+                fail(testcase.errorMessage());
+            }
+        }
+    }
+
+    static class IsProvisionedTestCase {
+        boolean isProvisioned;
+        LinkProperties lp;
+        InitialConfiguration config;
+
+        String errorMessage() {
+            return String.format("expected %s with config %s to be %s, but was %s",
+                     lp, config, provisioned(isProvisioned), provisioned(!isProvisioned));
+        }
+
+        static String provisioned(boolean isProvisioned) {
+            return isProvisioned ? "provisioned" : "not provisioned";
+        }
+    }
+
+    static IsProvisionedTestCase provisionedCase(Set<LinkAddress> lpAddrs, Set<RouteInfo> lpRoutes,
+            Set<InetAddress> lpDns, InitialConfiguration config) {
+        return provisioningTest(true, lpAddrs, lpRoutes, lpDns, config);
+    }
+
+    static IsProvisionedTestCase notProvisionedCase(Set<LinkAddress> lpAddrs,
+            Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) {
+        return provisioningTest(false, lpAddrs, lpRoutes, lpDns, config);
+    }
+
+    static IsProvisionedTestCase provisioningTest(boolean isProvisioned, Set<LinkAddress> lpAddrs,
+            Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) {
+        IsProvisionedTestCase testcase = new IsProvisionedTestCase();
+        testcase.isProvisioned = isProvisioned;
+        testcase.lp = new LinkProperties();
+        testcase.lp.setLinkAddresses(lpAddrs);
+        for (RouteInfo route : lpRoutes) {
+            testcase.lp.addRoute(route);
+        }
+        for (InetAddress dns : lpDns) {
+            testcase.lp.addDnsServer(dns);
+        }
+        testcase.config = config;
+        return testcase;
+    }
+
+    @Test
+    public void testInitialConfigurations() throws Exception {
+        InitialConfigurationTestCase[] testcases = {
+            validConf("valid IPv4 configuration",
+                    links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns("192.0.2.2")),
+            validConf("another valid IPv4 configuration",
+                    links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns()),
+            validConf("valid IPv6 configurations",
+                    links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"),
+                    prefixes("2001:db8:dead:beef::/64", "fe80::/64"),
+                    dns("2001:db8:dead:beef:f00::02")),
+            validConf("valid IPv6 configurations",
+                    links("fe80::1/64"), prefixes("fe80::/64"), dns()),
+            validConf("valid IPv6/v4 configuration",
+                    links("2001:db8:dead:beef:f00::a0/48", "192.0.2.12/24"),
+                    prefixes("2001:db8:dead:beef::/64", "192.0.2.0/24"),
+                    dns("192.0.2.2", "2001:db8:dead:beef:f00::02")),
+            validConf("valid IPv6 configuration without any GUA.",
+                    links("fd00:1234:5678::1/48"),
+                    prefixes("fd00:1234:5678::/48"),
+                    dns("fd00:1234:5678::1000")),
+
+            invalidConf("empty configuration", links(), prefixes(), dns()),
+            invalidConf("v4 addr and dns not in any prefix",
+                    links("192.0.2.12/24"), prefixes("198.51.100.0/24"), dns("192.0.2.2")),
+            invalidConf("v4 addr not in any prefix",
+                    links("198.51.2.12/24"), prefixes("198.51.100.0/24"), dns("192.0.2.2")),
+            invalidConf("v4 dns addr not in any prefix",
+                    links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns("198.51.100.2")),
+            invalidConf("v6 addr not in any prefix",
+                    links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"),
+                    prefixes("2001:db8:dead:beef::/64"),
+                    dns("2001:db8:dead:beef:f00::02")),
+            invalidConf("v6 dns addr not in any prefix",
+                    links("fe80::1/64"), prefixes("fe80::/64"), dns("2001:db8:dead:beef:f00::02")),
+            invalidConf("default ipv6 route and no GUA",
+                    links("fd01:1111:2222:3333::a0/128"), prefixes("::/0"), dns()),
+            invalidConf("invalid v6 prefix length",
+                    links("2001:db8:dead:beef:f00::a0/128"), prefixes("2001:db8:dead:beef::/32"),
+                    dns()),
+            invalidConf("another invalid v6 prefix length",
+                    links("2001:db8:dead:beef:f00::a0/128"), prefixes("2001:db8:dead:beef::/72"),
+                    dns())
+        };
+
+        for (InitialConfigurationTestCase testcase : testcases) {
+            if (testcase.config.isValid() != testcase.isValid) {
+                fail(testcase.errorMessage());
+            }
+        }
+    }
+
+    static class InitialConfigurationTestCase {
+        String descr;
+        boolean isValid;
+        InitialConfiguration config;
+        public String errorMessage() {
+            return String.format("%s: expected configuration %s to be %s, but was %s",
+                    descr, config, validString(isValid), validString(!isValid));
+        }
+        static String validString(boolean isValid) {
+            return isValid ? VALID : INVALID;
+        }
+    }
+
+    static InitialConfigurationTestCase validConf(String descr, Set<LinkAddress> links,
+            Set<IpPrefix> prefixes, Set<InetAddress> dns) {
+        return confTestCase(descr, true, conf(links, prefixes, dns));
+    }
+
+    static InitialConfigurationTestCase invalidConf(String descr, Set<LinkAddress> links,
+            Set<IpPrefix> prefixes, Set<InetAddress> dns) {
+        return confTestCase(descr, false, conf(links, prefixes, dns));
+    }
+
+    static InitialConfigurationTestCase confTestCase(
+            String descr, boolean isValid, InitialConfiguration config) {
+        InitialConfigurationTestCase testcase = new InitialConfigurationTestCase();
+        testcase.descr = descr;
+        testcase.isValid = isValid;
+        testcase.config = config;
+        return testcase;
+    }
+
+    static LinkProperties linkproperties(Set<LinkAddress> addresses, Set<RouteInfo> routes) {
+        LinkProperties lp = new LinkProperties();
+        lp.setLinkAddresses(addresses);
+        for (RouteInfo route : routes) {
+            lp.addRoute(route);
+        }
+        return lp;
+    }
+
+    static InitialConfiguration conf(Set<LinkAddress> links, Set<IpPrefix> prefixes) {
+        return conf(links, prefixes, new HashSet<>());
+    }
+
+    static InitialConfiguration conf(
+            Set<LinkAddress> links, Set<IpPrefix> prefixes, Set<InetAddress> dns) {
+        InitialConfiguration conf = new InitialConfiguration();
+        conf.ipAddresses.addAll(links);
+        conf.directlyConnectedRoutes.addAll(prefixes);
+        conf.dnsServers.addAll(dns);
+        return conf;
+    }
+
+    static Set<RouteInfo> routes(String... routes) {
+        return mapIntoSet(routes, (r) -> new RouteInfo(new IpPrefix(r)));
+    }
+
+    static Set<IpPrefix> prefixes(String... prefixes) {
+        return mapIntoSet(prefixes, IpPrefix::new);
+    }
+
+    static Set<LinkAddress> links(String... addresses) {
+        return mapIntoSet(addresses, LinkAddress::new);
+    }
+
+    static Set<InetAddress> ips(String... addresses) {
+        return mapIntoSet(addresses, InetAddress::getByName);
+    }
+
+    static Set<InetAddress> dns(String... addresses) {
+        return ips(addresses);
+    }
+
+    static <A, B> Set<B> mapIntoSet(A[] in, Fn<A, B> fn) {
+        Set<B> out = new HashSet<>(in.length);
+        for (A item : in) {
+            try {
+                out.add(fn.call(item));
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+        return out;
+    }
+
+    interface Fn<A,B> {
+        B call(A a) throws Exception;
+    }
+
+    @Test
+    public void testAll() {
+        List<String> list1 = Arrays.asList();
+        List<String> list2 = Arrays.asList("foo");
+        List<String> list3 = Arrays.asList("bar", "baz");
+        List<String> list4 = Arrays.asList("foo", "bar", "baz");
+
+        assertTrue(InitialConfiguration.all(list1, (x) -> false));
+        assertFalse(InitialConfiguration.all(list2, (x) -> false));
+        assertTrue(InitialConfiguration.all(list3, (x) -> true));
+        assertTrue(InitialConfiguration.all(list2, (x) -> x.charAt(0) == 'f'));
+        assertFalse(InitialConfiguration.all(list4, (x) -> x.charAt(0) == 'f'));
+    }
+
+    @Test
+    public void testAny() {
+        List<String> list1 = Arrays.asList();
+        List<String> list2 = Arrays.asList("foo");
+        List<String> list3 = Arrays.asList("bar", "baz");
+        List<String> list4 = Arrays.asList("foo", "bar", "baz");
+
+        assertFalse(InitialConfiguration.any(list1, (x) -> true));
+        assertTrue(InitialConfiguration.any(list2, (x) -> true));
+        assertTrue(InitialConfiguration.any(list2, (x) -> x.charAt(0) == 'f'));
+        assertFalse(InitialConfiguration.any(list3, (x) -> x.charAt(0) == 'f'));
+        assertTrue(InitialConfiguration.any(list4, (x) -> x.charAt(0) == 'f'));
+    }
+
+    @Test
+    public void testFindAll() {
+        List<String> list1 = Arrays.asList();
+        List<String> list2 = Arrays.asList("foo");
+        List<String> list3 = Arrays.asList("foo", "bar", "baz");
+
+        assertEquals(list1, IpClient.findAll(list1, (x) -> true));
+        assertEquals(list1, IpClient.findAll(list3, (x) -> false));
+        assertEquals(list3, IpClient.findAll(list3, (x) -> true));
+        assertEquals(list2, IpClient.findAll(list3, (x) -> x.charAt(0) == 'f'));
+    }
+}
diff --git a/tests/src/android/net/ip/IpReachabilityMonitorTest.java b/tests/src/android/net/ip/IpReachabilityMonitorTest.java
new file mode 100644
index 0000000..e3b5ddf
--- /dev/null
+++ b/tests/src/android/net/ip/IpReachabilityMonitorTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 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 android.net.ip;
+
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.util.InterfaceParams;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+
+/**
+ * Tests for IpReachabilityMonitor.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpReachabilityMonitorTest {
+
+    @Mock IpReachabilityMonitor.Callback mCallback;
+    @Mock IpReachabilityMonitor.Dependencies mDependencies;
+    @Mock SharedLog mLog;
+    @Mock Context mContext;
+    Handler mHandler;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mLog.forSubComponent(anyString())).thenReturn(mLog);
+        mHandler = new Handler(Looper.getMainLooper());
+    }
+
+    IpReachabilityMonitor makeMonitor() {
+        final InterfaceParams ifParams = new InterfaceParams("fake0", 1, null);
+        return new IpReachabilityMonitor(
+                mContext, ifParams, mHandler, mLog, mCallback, false, mDependencies);
+    }
+
+    @Test
+    public void testNothing() {
+        IpReachabilityMonitor monitor = makeMonitor();
+    }
+}
diff --git a/tests/src/android/net/util/ConnectivityPacketSummaryTest.java b/tests/src/android/net/util/ConnectivityPacketSummaryTest.java
new file mode 100644
index 0000000..dfaf52a
--- /dev/null
+++ b/tests/src/android/net/util/ConnectivityPacketSummaryTest.java
@@ -0,0 +1,418 @@
+/*
+ * 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 android.net.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.MacAddress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for ConnectivityPacketSummary.
+ *
+ * @hide
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConnectivityPacketSummaryTest {
+    private static final MacAddress MYHWADDR = MacAddress.fromString("80:7a:bf:6f:48:f3");
+
+    private String getSummary(String hexBytes) {
+        hexBytes = hexBytes.replaceAll("\\s+", "");
+        final byte[] bytes = HexEncoding.decode(hexBytes.toCharArray(), false);
+        return ConnectivityPacketSummary.summarize(MYHWADDR, bytes);
+    }
+
+    @Test
+    public void testParseICMPv6DADProbe() {
+        final String packet =
+                // Ethernet
+                "3333FF6F48F3 807ABF6F48F3 86DD" +
+                // IPv6
+                "600000000018 3A FF" +
+                "00000000000000000000000000000000" +
+                "FF0200000000000000000001FF6F48F3" +
+                // ICMPv6
+                "87 00 A8E7" +
+                "00000000" +
+                "FE80000000000000827ABFFFFE6F48F3";
+
+        final String expected =
+                "TX 80:7a:bf:6f:48:f3 > 33:33:ff:6f:48:f3 ipv6" +
+                " :: > ff02::1:ff6f:48f3 icmp6" +
+                " ns fe80::827a:bfff:fe6f:48f3";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    @Test
+    public void testParseICMPv6RS() {
+        final String packet =
+                // Ethernet
+                "333300000002 807ABF6F48F3 86DD" +
+                // IPv6
+                "600000000010 3A FF" +
+                "FE80000000000000827ABFFFFE6F48F3" +
+                "FF020000000000000000000000000002" +
+                // ICMPv6 RS
+                "85 00 6973" +
+                "00000000" +
+                "01 01 807ABF6F48F3";
+
+        final String expected =
+                "TX 80:7a:bf:6f:48:f3 > 33:33:00:00:00:02 ipv6" +
+                " fe80::827a:bfff:fe6f:48f3 > ff02::2 icmp6" +
+                " rs slla 80:7a:bf:6f:48:f3";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    @Test
+    public void testParseICMPv6RA() {
+        final String packet =
+                // Ethernet
+                "807ABF6F48F3 100E7E263FC1 86DD" +
+                // IPv6
+                "600000000068 3A FF" +
+                "FE80000000000000FA000004FD000001" +
+                "FE80000000000000827ABFFFFE6F48F3" +
+                // ICMPv6 RA
+                "86 00 8141" +
+                "40 00 0E10" +
+                "00000000" +
+                "00000000" +
+                "01 01 00005E000265" +
+                "05 01 0000000005DC" +
+                "19 05 000000000E10" +
+                "      20014860486000000000000000008844" +
+                "      20014860486000000000000000008888" +
+                "03 04 40 C0" +
+                "      00278D00" +
+                "      00093A80" +
+                "      00000000" +
+                "      2401FA000004FD000000000000000000";
+
+        final String expected =
+                "RX 10:0e:7e:26:3f:c1 > 80:7a:bf:6f:48:f3 ipv6" +
+                " fe80::fa00:4:fd00:1 > fe80::827a:bfff:fe6f:48f3 icmp6" +
+                " ra slla 00:00:5e:00:02:65 mtu 1500";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    @Test
+    public void testParseICMPv6NS() {
+        final String packet =
+                // Ethernet
+                  "807ABF6F48F3 100E7E263FC1 86DD" +
+                  // IPv6
+                  "6C0000000020 3A FF" +
+                  "FE80000000000000FA000004FD000001" +
+                  "FF0200000000000000000001FF01C146" +
+                  // ICMPv6 NS
+                  "87 00 8AD4" +
+                  "00000000" +
+                  "2401FA000004FD0015EA6A5C7B01C146" +
+                  "01 01 00005E000265";
+
+        final String expected =
+                "RX 10:0e:7e:26:3f:c1 > 80:7a:bf:6f:48:f3 ipv6" +
+                " fe80::fa00:4:fd00:1 > ff02::1:ff01:c146 icmp6" +
+                " ns 2401:fa00:4:fd00:15ea:6a5c:7b01:c146 slla 00:00:5e:00:02:65";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    @Test
+    public void testInvalidICMPv6NDLength() {
+        final String packet =
+                // Ethernet
+                "807ABF6F48F3 100E7E263FC1 86DD" +
+                // IPv6
+                "600000000068 3A FF" +
+                "FE80000000000000FA000004FD000001" +
+                "FE80000000000000827ABFFFFE6F48F3" +
+                // ICMPv6 RA
+                "86 00 8141" +
+                "40 00 0E10" +
+                "00000000" +
+                "00000000" +
+                "01 01 00005E000265" +
+                "00 00 0102030405D6";
+
+        final String expected =
+                "RX 10:0e:7e:26:3f:c1 > 80:7a:bf:6f:48:f3 ipv6" +
+                " fe80::fa00:4:fd00:1 > fe80::827a:bfff:fe6f:48f3 icmp6" +
+                " ra slla 00:00:5e:00:02:65 <malformed>";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    @Test
+    public void testParseICMPv6NA() {
+        final String packet =
+                // Ethernet
+                "00005E000265 807ABF6F48F3 86DD" +
+                "600000000020 3A FF" +
+                "2401FA000004FD0015EA6A5C7B01C146" +
+                "FE80000000000000FA000004FD000001" +
+                "88 00 E8126" +
+                "0000000" +
+                "2401FA000004FD0015EA6A5C7B01C146" +
+                "02 01 807ABF6F48F3";
+
+        final String expected =
+                "TX 80:7a:bf:6f:48:f3 > 00:00:5e:00:02:65 ipv6" +
+                " 2401:fa00:4:fd00:15ea:6a5c:7b01:c146 > fe80::fa00:4:fd00:1 icmp6" +
+                " na 2401:fa00:4:fd00:15ea:6a5c:7b01:c146 tlla 80:7a:bf:6f:48:f3";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    @Test
+    public void testParseARPRequest() {
+        final String packet =
+                // Ethernet
+                  "FFFFFFFFFFFF 807ABF6F48F3 0806" +
+                  // ARP
+                  "0001 0800 06 04" +
+                  // Request
+                  "0001" +
+                  "807ABF6F48F3 64706ADB" +
+                  "000000000000 64706FFD";
+
+        final String expected =
+                "TX 80:7a:bf:6f:48:f3 > ff:ff:ff:ff:ff:ff arp" +
+                " who-has 100.112.111.253";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    @Test
+    public void testParseARPReply() {
+        final String packet =
+                // Ethernet
+                  "807ABF6F48F3 288A1CA8DFC1 0806" +
+                  // ARP
+                  "0001 0800 06 04" +
+                  // Reply
+                  "0002" +
+                  "288A1CA8DFC1 64706FFD"+
+                  "807ABF6F48F3 64706ADB" +
+                  // Ethernet padding to packet min size.
+                  "0000000000000000000000000000";
+
+        final String expected =
+                "RX 28:8a:1c:a8:df:c1 > 80:7a:bf:6f:48:f3 arp" +
+                " reply 100.112.111.253 28:8a:1c:a8:df:c1";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    @Test
+    public void testParseDHCPv4Discover() {
+        final String packet =
+                // Ethernet
+                "FFFFFFFFFFFF 807ABF6F48F3 0800" +
+                // IPv4
+                "451001580000400040113986" +
+                "00000000" +
+                "FFFFFFFF" +
+                // UDP
+                "0044 0043" +
+                "0144 5559" +
+                // DHCPv4
+                "01 01 06 00" +
+                "79F7ACA4" +
+                "0000 0000" +
+                "00000000" +
+                "00000000" +
+                "00000000" +
+                "00000000" +
+                "807ABF6F48F300000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "63 82 53 63" +
+                "35 01 01" +
+                "3D 07 01807ABF6F48F3" +
+                "39 02 05DC" +
+                "3C 12 616E64726F69642D646863702D372E312E32" +
+                "0C 18 616E64726F69642D36623030366333313333393835343139" +
+                "37 0A 01 03 06 0F 1A 1C 33 3A 3B 2B" +
+                "FF" +
+                "00";
+
+        final String expectedPrefix =
+                "TX 80:7a:bf:6f:48:f3 > ff:ff:ff:ff:ff:ff ipv4" +
+                " 0.0.0.0 > 255.255.255.255 udp" +
+                " 68 > 67 dhcp4" +
+                " 80:7a:bf:6f:48:f3 DISCOVER";
+
+        assertTrue(getSummary(packet).startsWith(expectedPrefix));
+    }
+
+    @Test
+    public void testParseDHCPv4Offer() {
+        final String packet =
+                // Ethernet
+                "807ABF6F48F3 288A1CA8DFC1 0800" +
+                // IPv4
+                "4500013D4D2C0000401188CB" +
+                "64706FFD" +
+                "64706ADB" +
+                // UDP
+                "0043 0044" +
+                "0129 371D" +
+                // DHCPv4
+                "02 01 06 01" +
+                "79F7ACA4" +
+                "0000 0000" +
+                "00000000" +
+                "64706ADB" +
+                "00000000" +
+                "00000000" +
+                "807ABF6F48F300000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "63 82 53 63" +
+                "35 01 02" +
+                "36 04 AC188A0B" +
+                "33 04 00000708" +
+                "01 04 FFFFF000" +
+                "03 04 64706FFE" +
+                "06 08 08080808" +
+                "      08080404" +
+                "FF0001076165313A363636FF";
+
+        final String expectedPrefix =
+                "RX 28:8a:1c:a8:df:c1 > 80:7a:bf:6f:48:f3 ipv4" +
+                " 100.112.111.253 > 100.112.106.219 udp" +
+                " 67 > 68 dhcp4" +
+                " 80:7a:bf:6f:48:f3 OFFER";
+
+        assertTrue(getSummary(packet).startsWith(expectedPrefix));
+    }
+
+    @Test
+    public void testParseDHCPv4Request() {
+        final String packet =
+                // Ethernet
+                "FFFFFFFFFFFF 807ABF6F48F3 0800" +
+                // IPv4
+                "45100164000040004011397A" +
+                "00000000" +
+                "FFFFFFFF" +
+                // UDP
+                "0044 0043" +
+                "0150 E5C7" +
+                // DHCPv4
+                "01 01 06 00" +
+                "79F7ACA4" +
+                "0001 0000" +
+                "00000000" +
+                "00000000" +
+                "00000000" +
+                "00000000" +
+                "807ABF6F48F300000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "63 82 53 63" +
+                "35 01 03" +
+                "3D 07 01807ABF6F48F3" +
+                "32 04 64706ADB" +
+                "36 04 AC188A0B" +
+                "39 02 05DC" +
+                "3C 12 616E64726F69642D646863702D372E312E32" +
+                "0C 18 616E64726F69642D36623030366333313333393835343139" +
+                "37 0A 01 03 06 0F 1A 1C 33 3A 3B 2B" +
+                "FF" +
+                "00";
+
+        final String expectedPrefix =
+                "TX 80:7a:bf:6f:48:f3 > ff:ff:ff:ff:ff:ff ipv4" +
+                " 0.0.0.0 > 255.255.255.255 udp" +
+                " 68 > 67 dhcp4" +
+                " 80:7a:bf:6f:48:f3 REQUEST";
+
+        assertTrue(getSummary(packet).startsWith(expectedPrefix));
+    }
+
+    @Test
+    public void testParseDHCPv4Ack() {
+        final String packet =
+                // Ethernet
+                "807ABF6F48F3 288A1CA8DFC1 0800" +
+                // IPv4
+                "4500013D4D3B0000401188BC" +
+                "64706FFD" +
+                "64706ADB" +
+                // UDP
+                "0043 0044" +
+                "0129 341C" +
+                // DHCPv4
+                "02 01 06 01" +
+                "79F7ACA4" +
+                "0001 0000" +
+                "00000000" +
+                "64706ADB" +
+                "00000000" +
+                "00000000" +
+                "807ABF6F48F300000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "63 82 53 63" +
+                "35 01 05" +
+                "36 04 AC188A0B" +
+                "33 04 00000708" +
+                "01 04 FFFFF000" +
+                "03 04 64706FFE" +
+                "06 08 08080808" +
+                "      08080404" +
+                "FF0001076165313A363636FF";
+
+        final String expectedPrefix =
+                "RX 28:8a:1c:a8:df:c1 > 80:7a:bf:6f:48:f3 ipv4" +
+                " 100.112.111.253 > 100.112.106.219 udp" +
+                " 67 > 68 dhcp4" +
+                " 80:7a:bf:6f:48:f3 ACK";
+
+        assertTrue(getSummary(packet).startsWith(expectedPrefix));
+    }
+}
diff --git a/tests/src/android/net/util/PacketReaderTest.java b/tests/src/android/net/util/PacketReaderTest.java
new file mode 100644
index 0000000..dced743
--- /dev/null
+++ b/tests/src/android/net/util/PacketReaderTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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 android.net.util;
+
+import static android.net.util.PacketReader.DEFAULT_RECV_BUF_SIZE;
+import static android.system.OsConstants.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.runner.RunWith;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import libcore.io.IoBridge;
+
+/**
+ * Tests for PacketReader.
+ *
+ * @hide
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PacketReaderTest {
+    static final InetAddress LOOPBACK6 = Inet6Address.getLoopbackAddress();
+    static final StructTimeval TIMEO = StructTimeval.fromMillis(500);
+
+    protected CountDownLatch mLatch;
+    protected FileDescriptor mLocalSocket;
+    protected InetSocketAddress mLocalSockName;
+    protected byte[] mLastRecvBuf;
+    protected boolean mStopped;
+    protected HandlerThread mHandlerThread;
+    protected PacketReader mReceiver;
+
+    class UdpLoopbackReader extends PacketReader {
+        public UdpLoopbackReader(Handler h) {
+            super(h);
+        }
+
+        @Override
+        protected FileDescriptor createFd() {
+            FileDescriptor s = null;
+            try {
+                s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+                Os.bind(s, LOOPBACK6, 0);
+                mLocalSockName = (InetSocketAddress) Os.getsockname(s);
+                Os.setsockoptTimeval(s, SOL_SOCKET, SO_SNDTIMEO, TIMEO);
+            } catch (ErrnoException|SocketException e) {
+                closeFd(s);
+                fail();
+                return null;
+            }
+
+            mLocalSocket = s;
+            return s;
+        }
+
+        @Override
+        protected void handlePacket(byte[] recvbuf, int length) {
+            mLastRecvBuf = Arrays.copyOf(recvbuf, length);
+            mLatch.countDown();
+        }
+
+        @Override
+        protected void onStart() {
+            mStopped = false;
+            mLatch.countDown();
+        }
+
+        @Override
+        protected void onStop() {
+            mStopped = true;
+            mLatch.countDown();
+        }
+    };
+
+    @Before
+    public void setUp() {
+        resetLatch();
+        mLocalSocket = null;
+        mLocalSockName = null;
+        mLastRecvBuf = null;
+        mStopped = false;
+
+        mHandlerThread = new HandlerThread(PacketReaderTest.class.getSimpleName());
+        mHandlerThread.start();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mReceiver != null) {
+            mHandlerThread.getThreadHandler().post(() -> { mReceiver.stop(); });
+            waitForActivity();
+        }
+        mReceiver = null;
+        mHandlerThread.quit();
+        mHandlerThread = null;
+    }
+
+    void resetLatch() { mLatch = new CountDownLatch(1); }
+
+    void waitForActivity() throws Exception {
+        try {
+            mLatch.await(1000, TimeUnit.MILLISECONDS);
+        } finally {
+            resetLatch();
+        }
+    }
+
+    void sendPacket(byte[] contents) throws Exception {
+        final DatagramSocket sender = new DatagramSocket();
+        sender.connect(mLocalSockName);
+        sender.send(new DatagramPacket(contents, contents.length));
+        sender.close();
+    }
+
+    @Test
+    public void testBasicWorking() throws Exception {
+        final Handler h = mHandlerThread.getThreadHandler();
+        mReceiver = new UdpLoopbackReader(h);
+
+        h.post(() -> { mReceiver.start(); });
+        waitForActivity();
+        assertTrue(mLocalSockName != null);
+        assertEquals(LOOPBACK6, mLocalSockName.getAddress());
+        assertTrue(0 < mLocalSockName.getPort());
+        assertTrue(mLocalSocket != null);
+        assertFalse(mStopped);
+
+        final byte[] one = "one 1".getBytes("UTF-8");
+        sendPacket(one);
+        waitForActivity();
+        assertEquals(1, mReceiver.numPacketsReceived());
+        assertTrue(Arrays.equals(one, mLastRecvBuf));
+        assertFalse(mStopped);
+
+        final byte[] two = "two 2".getBytes("UTF-8");
+        sendPacket(two);
+        waitForActivity();
+        assertEquals(2, mReceiver.numPacketsReceived());
+        assertTrue(Arrays.equals(two, mLastRecvBuf));
+        assertFalse(mStopped);
+
+        mReceiver.stop();
+        waitForActivity();
+        assertEquals(2, mReceiver.numPacketsReceived());
+        assertTrue(Arrays.equals(two, mLastRecvBuf));
+        assertTrue(mStopped);
+        mReceiver = null;
+    }
+
+    class NullPacketReader extends PacketReader {
+        public NullPacketReader(Handler h, int recvbufsize) {
+            super(h, recvbufsize);
+        }
+
+        @Override
+        public FileDescriptor createFd() { return null; }
+    }
+
+    @Test
+    public void testMinimalRecvBufSize() throws Exception {
+        final Handler h = mHandlerThread.getThreadHandler();
+
+        for (int i : new int[]{-1, 0, 1, DEFAULT_RECV_BUF_SIZE-1}) {
+            final PacketReader b = new NullPacketReader(h, i);
+            assertEquals(DEFAULT_RECV_BUF_SIZE, b.recvBufSize());
+        }
+    }
+}