Merge "Ipv6UtilsTest: testBuildEchoReplyPacket"
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index e0b75c5..2fc4142 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -127,7 +127,6 @@
         "//frameworks/libs/net/common/testutils:__subpackages__",
         "//packages/modules/Connectivity:__subpackages__",
         "//packages/modules/NetworkStack:__subpackages__",
-        "//frameworks/base/services/core",
     ],
     libs: [
         "androidx.annotation_annotation",
@@ -260,24 +259,15 @@
     ],
     lint: { strict_updatability_linting: true },
 }
-filegroup {
-    name: "net-utils-services-common-srcs",
+
+java_library {
+    name: "net-utils-services-common",
     srcs: [
         "device/android/net/NetworkFactory.java",
         "device/android/net/NetworkFactoryImpl.java",
         "device/android/net/NetworkFactoryLegacyImpl.java",
         "device/android/net/NetworkFactoryShim.java",
     ],
-    visibility: [
-        "//frameworks/base/services/net",
-    ],
-}
-
-java_library {
-    name: "net-utils-services-common",
-    srcs: [
-        ":net-utils-services-common-srcs",
-    ],
     sdk_version: "module_current",
     min_sdk_version: "30",
     static_libs: [
@@ -292,6 +282,7 @@
         "//apex_available:platform",
         "com.android.bluetooth",
         "com.android.tethering",
+        "com.android.wifi",
     ],
     visibility: [
         // TODO: remove after NetworkStatsService moves to the module.
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index 0ee862a..f7019a5 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -20,6 +20,7 @@
 
 import android.os.ParcelFileDescriptor;
 import android.system.ErrnoException;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -32,6 +33,7 @@
 import java.nio.ByteOrder;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * BpfMap is a key -> value mapping structure that is designed to maintained the bpf map entries.
@@ -65,6 +67,27 @@
     private final int mKeySize;
     private final int mValueSize;
 
+    private static ConcurrentHashMap<Pair<String, Integer>, ParcelFileDescriptor> sFdCache =
+            new ConcurrentHashMap<>();
+
+    private static ParcelFileDescriptor cachedBpfFdGet(String path, int mode)
+            throws ErrnoException, NullPointerException {
+        Pair<String, Integer> key = Pair.create(path, mode);
+        // unlocked fetch is safe: map is concurrent read capable, and only inserted into
+        ParcelFileDescriptor fd = sFdCache.get(key);
+        if (fd != null) return fd;
+        // ok, no cached fd present, need to grab a lock
+        synchronized (BpfMap.class) {
+            // need to redo the check
+            fd = sFdCache.get(key);
+            if (fd != null) return fd;
+            // okay, we really haven't opened this before...
+            fd = ParcelFileDescriptor.adoptFd(nativeBpfFdGet(path, mode));
+            sFdCache.put(key, fd);
+            return fd;
+        }
+    }
+
     /**
      * Create a BpfMap map wrapper with "path" of filesystem.
      *
@@ -74,7 +97,7 @@
      */
     public BpfMap(@NonNull final String path, final int flag, final Class<K> key,
             final Class<V> value) throws ErrnoException, NullPointerException {
-        mMapFd = ParcelFileDescriptor.adoptFd(bpfFdGet(path, flag));
+        mMapFd = cachedBpfFdGet(path, flag);
         mKeyClass = key;
         mValueClass = value;
         mKeySize = Struct.getSize(key);
@@ -90,7 +113,7 @@
      */
     @VisibleForTesting
     protected BpfMap(final Class<K> key, final Class<V> value) {
-        mMapFd = ParcelFileDescriptor.adoptFd(-1 /*invalid*/);  // unused
+        mMapFd = null;  // unused
         mKeyClass = key;
         mValueClass = value;
         mKeySize = Struct.getSize(key);
@@ -103,7 +126,7 @@
      */
     @Override
     public void updateEntry(K key, V value) throws ErrnoException {
-        writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_ANY);
+        nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_ANY);
     }
 
     /**
@@ -114,7 +137,8 @@
     public void insertEntry(K key, V value)
             throws ErrnoException, IllegalStateException {
         try {
-            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_NOEXIST);
         } catch (ErrnoException e) {
             if (e.errno == EEXIST) throw new IllegalStateException(key + " already exists");
 
@@ -130,7 +154,8 @@
     public void replaceEntry(K key, V value)
             throws ErrnoException, NoSuchElementException {
         try {
-            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_EXIST);
         } catch (ErrnoException e) {
             if (e.errno == ENOENT) throw new NoSuchElementException(key + " not found");
 
@@ -148,13 +173,15 @@
     public boolean insertOrReplaceEntry(K key, V value)
             throws ErrnoException {
         try {
-            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_NOEXIST);
             return true;   /* insert succeeded */
         } catch (ErrnoException e) {
             if (e.errno != EEXIST) throw e;
         }
         try {
-            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_EXIST);
             return false;   /* replace succeeded */
         } catch (ErrnoException e) {
             if (e.errno != ENOENT) throw e;
@@ -171,7 +198,7 @@
     /** Remove existing key from eBpf map. Return false if map was not modified. */
     @Override
     public boolean deleteEntry(K key) throws ErrnoException {
-        return deleteMapEntry(mMapFd.getFd(), key.writeToBytes());
+        return nativeDeleteMapEntry(mMapFd.getFd(), key.writeToBytes());
     }
 
     /** Returns {@code true} if this map contains no elements. */
@@ -204,7 +231,7 @@
 
     private byte[] getNextRawKey(@Nullable final byte[] key) throws ErrnoException {
         byte[] nextKey = new byte[mKeySize];
-        if (getNextMapKey(mMapFd.getFd(), key, nextKey)) return nextKey;
+        if (nativeGetNextMapKey(mMapFd.getFd(), key, nextKey)) return nextKey;
 
         return null;
     }
@@ -239,7 +266,7 @@
 
     private byte[] getRawValue(final byte[] key) throws ErrnoException {
         byte[] value = new byte[mValueSize];
-        if (findMapEntry(mMapFd.getFd(), key, value)) return value;
+        if (nativeFindMapEntry(mMapFd.getFd(), key, value)) return value;
 
         return null;
     }
@@ -263,9 +290,13 @@
         }
     }
 
+    /* Empty implementation to implement AutoCloseable, so we can use BpfMaps
+     * with try with resources, but due to persistent FD cache, there is no actual
+     * need to close anything.  File descriptors will actually be closed when we
+     * unlock the BpfMap class and destroy the ParcelFileDescriptor objects.
+     */
     @Override
     public void close() throws IOException {
-        mMapFd.close();
     }
 
     /**
@@ -283,17 +314,25 @@
         }
     }
 
-    private native int bpfFdGet(String path, int mode) throws ErrnoException, NullPointerException;
+    private static native int nativeBpfFdGet(String path, int mode)
+            throws ErrnoException, NullPointerException;
 
-    private native void writeToMapEntry(int fd, byte[] key, byte[] value, int flags)
+    // Note: the following methods appear to not require the object by virtue of taking the
+    // fd as an int argument, but the hidden reference to this is actually what prevents
+    // the object from being garbage collected (and thus potentially maps closed) prior
+    // to the native code actually running (with a possibly already closed fd).
+
+    private native void nativeWriteToMapEntry(int fd, byte[] key, byte[] value, int flags)
             throws ErrnoException;
 
-    private native boolean deleteMapEntry(int fd, byte[] key) throws ErrnoException;
+    private native boolean nativeDeleteMapEntry(int fd, byte[] key) throws ErrnoException;
 
     // If key is found, the operation returns true and the nextKey would reference to the next
     // element.  If key is not found, the operation returns true and the nextKey would reference to
     // the first element.  If key is the last element, false is returned.
-    private native boolean getNextMapKey(int fd, byte[] key, byte[] nextKey) throws ErrnoException;
+    private native boolean nativeGetNextMapKey(int fd, byte[] key, byte[] nextKey)
+            throws ErrnoException;
 
-    private native boolean findMapEntry(int fd, byte[] key, byte[] value) throws ErrnoException;
+    private native boolean nativeFindMapEntry(int fd, byte[] key, byte[] value)
+            throws ErrnoException;
 }
diff --git a/staticlibs/device/com/android/net/module/util/FdEventsReader.java b/staticlibs/device/com/android/net/module/util/FdEventsReader.java
index 71ae13d..ecf8e77 100644
--- a/staticlibs/device/com/android/net/module/util/FdEventsReader.java
+++ b/staticlibs/device/com/android/net/module/util/FdEventsReader.java
@@ -211,7 +211,7 @@
         return true;
     }
 
-    private boolean isRunning() {
+    protected boolean isRunning() {
         return (mFd != null) && mFd.valid();
     }
 
diff --git a/staticlibs/device/com/android/net/module/util/PacketBuilder.java b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
index c908528..33e5bfa 100644
--- a/staticlibs/device/com/android/net/module/util/PacketBuilder.java
+++ b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
@@ -17,6 +17,7 @@
 package com.android.net.module.util;
 
 import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 
@@ -25,6 +26,8 @@
 import static com.android.net.module.util.IpUtils.udpChecksum;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_LEN_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.TCP_CHECKSUM_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.UDP_CHECKSUM_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.UDP_LENGTH_OFFSET;
@@ -35,11 +38,13 @@
 
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Ipv4Header;
+import com.android.net.module.util.structs.Ipv6Header;
 import com.android.net.module.util.structs.TcpHeader;
 import com.android.net.module.util.structs.UdpHeader;
 
 import java.io.IOException;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
 
@@ -49,7 +54,7 @@
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  * |                Layer 2 header (EthernetHeader)                | (optional)
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- * |                  Layer 3 header (Ipv4Header)                  |
+ * |           Layer 3 header (Ipv4Header, Ipv6Header)             |
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  * |           Layer 4 header (TcpHeader, UdpHeader)               |
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
@@ -74,11 +79,14 @@
  * sendPacket(buf);
  */
 public class PacketBuilder {
+    private static final int INVALID_OFFSET = -1;
+
     private final ByteBuffer mBuffer;
 
-    private int mIpv4HeaderOffset = -1;
-    private int mTcpHeaderOffset = -1;
-    private int mUdpHeaderOffset = -1;
+    private int mIpv4HeaderOffset = INVALID_OFFSET;
+    private int mIpv6HeaderOffset = INVALID_OFFSET;
+    private int mTcpHeaderOffset = INVALID_OFFSET;
+    private int mUdpHeaderOffset = INVALID_OFFSET;
 
     public PacketBuilder(@NonNull ByteBuffer buffer) {
         mBuffer = buffer;
@@ -93,9 +101,9 @@
      */
     public void writeL2Header(MacAddress srcMac, MacAddress dstMac, short etherType) throws
             IOException {
-        final EthernetHeader ethv4Header = new EthernetHeader(dstMac, srcMac, etherType);
+        final EthernetHeader ethHeader = new EthernetHeader(dstMac, srcMac, etherType);
         try {
-            ethv4Header.writeToByteBuffer(mBuffer);
+            ethHeader.writeToByteBuffer(mBuffer);
         } catch (IllegalArgumentException | BufferOverflowException e) {
             throw new IOException("Error writing to buffer: ", e);
         }
@@ -130,6 +138,31 @@
     }
 
     /**
+     * Write an IPv6 header.
+     * The IP header length is calculated and written back in #finalizePacket.
+     *
+     * @param vtf version, traffic class and flow label
+     * @param nextHeader the transport layer protocol
+     * @param hopLimit hop limit
+     * @param srcIp source IP address
+     * @param dstIp destination IP address
+     */
+    public void writeIpv6Header(int vtf, byte nextHeader, short hopLimit,
+            @NonNull final Inet6Address srcIp, @NonNull final Inet6Address dstIp)
+            throws IOException {
+        mIpv6HeaderOffset = mBuffer.position();
+        final Ipv6Header ipv6Header = new Ipv6Header(vtf,
+                (short) 0 /* payloadLength, calculate in #finalizePacket */, nextHeader,
+                hopLimit, srcIp, dstIp);
+
+        try {
+            ipv6Header.writeToByteBuffer(mBuffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
+    /**
      * Write a TCP header.
      * The TCP header checksum is calculated and written back in #finalizePacket.
      *
@@ -186,32 +219,43 @@
      */
     @NonNull
     public ByteBuffer finalizePacket() throws IOException {
-        if (mIpv4HeaderOffset < 0) {
-            // TODO: add support for IPv6
-            throw new IOException("Packet is missing IPv4 header");
+        // [1] Finalize IPv4 or IPv6 header.
+        int ipHeaderOffset = INVALID_OFFSET;
+        if (mIpv4HeaderOffset != INVALID_OFFSET) {
+            ipHeaderOffset = mIpv4HeaderOffset;
+
+            // Populate the IPv4 totalLength field.
+            mBuffer.putShort(mIpv4HeaderOffset + IPV4_LENGTH_OFFSET,
+                    (short) (mBuffer.position() - mIpv4HeaderOffset));
+
+            // Populate the IPv4 header checksum field.
+            mBuffer.putShort(mIpv4HeaderOffset + IPV4_CHECKSUM_OFFSET,
+                    ipChecksum(mBuffer, mIpv4HeaderOffset /* headerOffset */));
+        } else if (mIpv6HeaderOffset != INVALID_OFFSET) {
+            ipHeaderOffset = mIpv6HeaderOffset;
+
+            // Populate the IPv6 payloadLength field.
+            // The payload length doesn't include IPv6 header length. See rfc8200 section 3.
+            mBuffer.putShort(mIpv6HeaderOffset + IPV6_LEN_OFFSET,
+                    (short) (mBuffer.position() - mIpv6HeaderOffset - IPV6_HEADER_LEN));
+        } else {
+            throw new IOException("Packet is missing neither IPv4 nor IPv6 header");
         }
 
-        // Populate the IPv4 totalLength field.
-        mBuffer.putShort(mIpv4HeaderOffset + IPV4_LENGTH_OFFSET,
-                (short) (mBuffer.position() - mIpv4HeaderOffset));
-
-        // Populate the IPv4 header checksum field.
-        mBuffer.putShort(mIpv4HeaderOffset + IPV4_CHECKSUM_OFFSET,
-                ipChecksum(mBuffer, mIpv4HeaderOffset /* headerOffset */));
-
-        if (mTcpHeaderOffset > 0) {
+        // [2] Finalize TCP or UDP header.
+        if (mTcpHeaderOffset != INVALID_OFFSET) {
             // Populate the TCP header checksum field.
             mBuffer.putShort(mTcpHeaderOffset + TCP_CHECKSUM_OFFSET, tcpChecksum(mBuffer,
-                    mIpv4HeaderOffset /* ipOffset */, mTcpHeaderOffset /* transportOffset */,
+                    ipHeaderOffset /* ipOffset */, mTcpHeaderOffset /* transportOffset */,
                     mBuffer.position() - mTcpHeaderOffset /* transportLen */));
-        } else if (mUdpHeaderOffset > 0) {
+        } else if (mUdpHeaderOffset != INVALID_OFFSET) {
             // Populate the UDP header length field.
             mBuffer.putShort(mUdpHeaderOffset + UDP_LENGTH_OFFSET,
                     (short) (mBuffer.position() - mUdpHeaderOffset));
 
             // Populate the UDP header checksum field.
             mBuffer.putShort(mUdpHeaderOffset + UDP_CHECKSUM_OFFSET, udpChecksum(mBuffer,
-                    mIpv4HeaderOffset /* ipOffset */, mUdpHeaderOffset /* transportOffset */));
+                    ipHeaderOffset /* ipOffset */, mUdpHeaderOffset /* transportOffset */));
         } else {
             throw new IOException("Packet is missing neither TCP nor UDP header");
         }
@@ -225,15 +269,15 @@
      *
      * @param hasEther has ethernet header. Set this flag to indicate that the packet has an
      *        ethernet header.
-     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} currently supported.
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} and {@code IPPROTO_IPV6}
+     *        currently supported.
      * @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} and {@code IPPROTO_UDP}
      *        currently supported.
      * @param payloadLen length of the payload.
      */
     @NonNull
     public static ByteBuffer allocate(boolean hasEther, int l3proto, int l4proto, int payloadLen) {
-        if (l3proto != IPPROTO_IP) {
-            // TODO: add support for IPv6
+        if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
             throw new IllegalArgumentException("Unsupported layer 3 protocol " + l3proto);
         }
 
@@ -247,7 +291,8 @@
 
         int packetLen = 0;
         if (hasEther) packetLen += Struct.getSize(EthernetHeader.class);
-        packetLen += Struct.getSize(Ipv4Header.class);
+        packetLen += (l3proto == IPPROTO_IP) ? Struct.getSize(Ipv4Header.class)
+                : Struct.getSize(Ipv6Header.class);
         packetLen += (l4proto == IPPROTO_TCP) ? Struct.getSize(TcpHeader.class)
                 : Struct.getSize(UdpHeader.class);
         packetLen += payloadLen;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index a216752..9e1e26e 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -51,8 +51,8 @@
             return null;
         }
 
-        int payloadLength = NetlinkConstants.alignedLengthOf(nlmsghdr.nlmsg_len);
-        payloadLength -= StructNlMsgHdr.STRUCT_SIZE;
+        final int messageLength = NetlinkConstants.alignedLengthOf(nlmsghdr.nlmsg_len);
+        final int payloadLength = messageLength - StructNlMsgHdr.STRUCT_SIZE;
         if (payloadLength < 0 || payloadLength > byteBuffer.remaining()) {
             // Malformed message or runt buffer.  Pretend the buffer was consumed.
             byteBuffer.position(byteBuffer.limit());
@@ -68,15 +68,22 @@
         // Netlink family messages. The netlink family is required. Note that the reason for using
         // if-statement is that switch-case can't be used because the OsConstants.NETLINK_* are
         // not constant.
+        final NetlinkMessage parsed;
         if (nlFamily == OsConstants.NETLINK_ROUTE) {
-            return parseRtMessage(nlmsghdr, byteBuffer);
+            parsed = parseRtMessage(nlmsghdr, byteBuffer);
         } else if (nlFamily == OsConstants.NETLINK_INET_DIAG) {
-            return parseInetDiagMessage(nlmsghdr, byteBuffer);
+            parsed = parseInetDiagMessage(nlmsghdr, byteBuffer);
         } else if (nlFamily == OsConstants.NETLINK_NETFILTER) {
-            return parseNfMessage(nlmsghdr, byteBuffer);
+            parsed = parseNfMessage(nlmsghdr, byteBuffer);
+        } else {
+            parsed = null;
         }
 
-        return null;
+        // Advance to the end of the message, regardless of whether the parsing code consumed
+        // all of it or not.
+        byteBuffer.position(startPosition + messageLength);
+
+        return parsed;
     }
 
     @NonNull
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
index bdffc0f..86c0756 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
@@ -102,6 +102,23 @@
     // Function that tries to get map from a pinned path.
     base::Result<void> init(const char* path);
 
+#ifdef TEST_BPF_MAP
+    // due to Android SELinux limitations which prevent map creation by anyone besides the bpfloader
+    // this should only ever be used by test code, it is equivalent to:
+    //   .reset(createMap(type, keysize, valuesize, max_entries, map_flags)
+    // TODO: derive map_flags from BpfMap vs BpfMapRO
+    base::Result<void> resetMap(bpf_map_type map_type, uint32_t max_entries, uint32_t map_flags = 0) {
+        int map_fd = createMap(map_type, sizeof(Key), sizeof(Value), max_entries, map_flags);
+        if (map_fd < 0) {
+             auto err = ErrnoErrorf("Unable to create map.");
+             mMapFd.reset();
+             return err;
+        };
+        mMapFd.reset(map_fd);
+        return {};
+    }
+#endif
+
     // Iterate through the map and handle each key retrieved based on the filter
     // without modification of map content.
     base::Result<void> iterate(
@@ -179,7 +196,7 @@
 
 template <class Key, class Value>
 base::Result<void> BpfMap<Key, Value>::init(const char* path) {
-    mMapFd = base::unique_fd(mapRetrieveRW(path));
+    mMapFd.reset(mapRetrieveRW(path));
     if (mMapFd == -1) {
         return ErrnoErrorf("Pinned map not accessible or does not exist: ({})", path);
     }
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
index 8f1b9a2..4429164 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
@@ -60,6 +60,9 @@
     // This is a temporary hack for network stats map swap on devices running
     // 4.9 kernels. The kernel code of socket release on pf_key socket will
     // explicitly call synchronize_rcu() which is exactly what we need.
+    //
+    // Linux 4.14/4.19/5.4/5.10/5.15 (and 5.18) still have this same behaviour.
+    // see net/key/af_key.c: pfkey_release() -> synchronize_rcu()
     int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
 
     if (pfSocket < 0) {
@@ -92,22 +95,22 @@
 
 #define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
 
-static inline unsigned kernelVersion() {
+static inline unsigned uncachedKernelVersion() {
     struct utsname buf;
-    int ret = uname(&buf);
-    if (ret) return 0;
+    if (uname(&buf)) return 0;
 
-    unsigned kver_major;
-    unsigned kver_minor;
-    unsigned kver_sub;
-    char unused;
-    ret = sscanf(buf.release, "%u.%u.%u%c", &kver_major, &kver_minor, &kver_sub, &unused);
-    // Check the device kernel version
-    if (ret < 3) return 0;
-
+    unsigned kver_major = 0;
+    unsigned kver_minor = 0;
+    unsigned kver_sub = 0;
+    (void)sscanf(buf.release, "%u.%u.%u", &kver_major, &kver_minor, &kver_sub);
     return KVER(kver_major, kver_minor, kver_sub);
 }
 
+static inline unsigned kernelVersion() {
+    static unsigned kver = uncachedKernelVersion();
+    return kver;
+}
+
 static inline bool isAtLeastKernelVersion(unsigned major, unsigned minor, unsigned sub) {
     return kernelVersion() >= KVER(major, minor, sub);
 }
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index ac9f9bc..4b035b9 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -19,6 +19,17 @@
  *                                                                            *
  ******************************************************************************/
 
+// The actual versions of the bpfloader that shipped in various Android releases
+
+// Android P/Q/R: BpfLoader was initially part of netd,
+// this was later split out into a standalone binary, but was unversioned.
+
+// Android S / 12 (api level 31) - added 'tethering' mainline eBPF support
+#define BPFLOADER_S_VERSION 2u
+
+// Android T / 13 Beta 3 (api level 33) - added support for 'netd_shared'
+#define BPFLOADER_T_BETA3_VERSION 13u
+
 /* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
  * before #include "bpf_helpers.h" to change which bpfloaders will
  * process the resulting .o file.
@@ -177,6 +188,8 @@
 static unsigned long long (*bpf_get_current_pid_tgid)(void) = (void*) BPF_FUNC_get_current_pid_tgid;
 static unsigned long long (*bpf_get_current_uid_gid)(void) = (void*) BPF_FUNC_get_current_uid_gid;
 static unsigned long long (*bpf_get_smp_processor_id)(void) = (void*) BPF_FUNC_get_smp_processor_id;
+static long (*bpf_get_stackid)(void* ctx, void* map, uint64_t flags) = (void*) BPF_FUNC_get_stackid;
+static long (*bpf_get_current_comm)(void* buf, uint32_t buf_size) = (void*) BPF_FUNC_get_current_comm;
 
 #define DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
                                        opt)                                                        \
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
index e3f48e5..2e88fc8 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -27,18 +27,18 @@
 
 namespace android {
 
-static jint com_android_net_module_util_BpfMap_bpfFdGet(JNIEnv *env, jobject clazz,
+static jint com_android_net_module_util_BpfMap_nativeBpfFdGet(JNIEnv *env, jclass clazz,
         jstring path, jint mode) {
     ScopedUtfChars pathname(env, path);
 
     jint fd = bpf::bpfFdGet(pathname.c_str(), static_cast<unsigned>(mode));
 
-    if (fd < 0) jniThrowErrnoException(env, "bpfFdGet", errno);
+    if (fd < 0) jniThrowErrnoException(env, "nativeBpfFdGet", errno);
 
     return fd;
 }
 
-static void com_android_net_module_util_BpfMap_writeToMapEntry(JNIEnv *env, jobject clazz,
+static void com_android_net_module_util_BpfMap_nativeWriteToMapEntry(JNIEnv *env, jobject self,
         jint fd, jbyteArray key, jbyteArray value, jint flags) {
     ScopedByteArrayRO keyRO(env, key);
     ScopedByteArrayRO valueRO(env, value);
@@ -46,7 +46,7 @@
     int ret = bpf::writeToMapEntry(static_cast<int>(fd), keyRO.get(), valueRO.get(),
             static_cast<int>(flags));
 
-    if (ret) jniThrowErrnoException(env, "writeToMapEntry", errno);
+    if (ret) jniThrowErrnoException(env, "nativeWriteToMapEntry", errno);
 }
 
 static jboolean throwIfNotEnoent(JNIEnv *env, const char* functionName, int ret, int err) {
@@ -56,7 +56,7 @@
     return false;
 }
 
-static jboolean com_android_net_module_util_BpfMap_deleteMapEntry(JNIEnv *env, jobject clazz,
+static jboolean com_android_net_module_util_BpfMap_nativeDeleteMapEntry(JNIEnv *env, jobject self,
         jint fd, jbyteArray key) {
     ScopedByteArrayRO keyRO(env, key);
 
@@ -64,10 +64,10 @@
     // to ENOENT.
     int ret = bpf::deleteMapEntry(static_cast<int>(fd), keyRO.get());
 
-    return throwIfNotEnoent(env, "deleteMapEntry", ret, errno);
+    return throwIfNotEnoent(env, "nativeDeleteMapEntry", ret, errno);
 }
 
-static jboolean com_android_net_module_util_BpfMap_getNextMapKey(JNIEnv *env, jobject clazz,
+static jboolean com_android_net_module_util_BpfMap_nativeGetNextMapKey(JNIEnv *env, jobject self,
         jint fd, jbyteArray key, jbyteArray nextKey) {
     // If key is found, the operation returns zero and sets the next key pointer to the key of the
     // next element.  If key is not found, the operation returns zero and sets the next key pointer
@@ -83,10 +83,10 @@
         ret = bpf::getNextMapKey(static_cast<int>(fd), keyRO.get(), nextKeyRW.get());
     }
 
-    return throwIfNotEnoent(env, "getNextMapKey", ret, errno);
+    return throwIfNotEnoent(env, "nativeGetNextMapKey", ret, errno);
 }
 
-static jboolean com_android_net_module_util_BpfMap_findMapEntry(JNIEnv *env, jobject clazz,
+static jboolean com_android_net_module_util_BpfMap_nativeFindMapEntry(JNIEnv *env, jobject self,
         jint fd, jbyteArray key, jbyteArray value) {
     ScopedByteArrayRO keyRO(env, key);
     ScopedByteArrayRW valueRW(env, value);
@@ -95,7 +95,7 @@
     // "value".  If no element is found, the operation returns -1 and sets errno to ENOENT.
     int ret = bpf::findMapEntry(static_cast<int>(fd), keyRO.get(), valueRW.get());
 
-    return throwIfNotEnoent(env, "findMapEntry", ret, errno);
+    return throwIfNotEnoent(env, "nativeFindMapEntry", ret, errno);
 }
 
 /*
@@ -103,16 +103,16 @@
  */
 static const JNINativeMethod gMethods[] = {
     /* name, signature, funcPtr */
-    { "bpfFdGet", "(Ljava/lang/String;I)I",
-        (void*) com_android_net_module_util_BpfMap_bpfFdGet },
-    { "writeToMapEntry", "(I[B[BI)V",
-        (void*) com_android_net_module_util_BpfMap_writeToMapEntry },
-    { "deleteMapEntry", "(I[B)Z",
-        (void*) com_android_net_module_util_BpfMap_deleteMapEntry },
-    { "getNextMapKey", "(I[B[B)Z",
-        (void*) com_android_net_module_util_BpfMap_getNextMapKey },
-    { "findMapEntry", "(I[B[B)Z",
-        (void*) com_android_net_module_util_BpfMap_findMapEntry },
+    { "nativeBpfFdGet", "(Ljava/lang/String;I)I",
+        (void*) com_android_net_module_util_BpfMap_nativeBpfFdGet },
+    { "nativeWriteToMapEntry", "(I[B[BI)V",
+        (void*) com_android_net_module_util_BpfMap_nativeWriteToMapEntry },
+    { "nativeDeleteMapEntry", "(I[B)Z",
+        (void*) com_android_net_module_util_BpfMap_nativeDeleteMapEntry },
+    { "nativeGetNextMapKey", "(I[B[B)Z",
+        (void*) com_android_net_module_util_BpfMap_nativeGetNextMapKey },
+    { "nativeFindMapEntry", "(I[B[B)Z",
+        (void*) com_android_net_module_util_BpfMap_nativeFindMapEntry },
 
 };
 
diff --git a/staticlibs/native/tcutils/kernelversion.h b/staticlibs/native/tcutils/kernelversion.h
index 3be1ad2..9aab31d 100644
--- a/staticlibs/native/tcutils/kernelversion.h
+++ b/staticlibs/native/tcutils/kernelversion.h
@@ -32,25 +32,22 @@
 
 namespace android {
 
-static inline unsigned kernelVersion() {
+static inline unsigned uncachedKernelVersion() {
   struct utsname buf;
-  int ret = uname(&buf);
-  if (ret)
-    return 0;
+  if (uname(&buf)) return 0;
 
-  unsigned kver_major;
-  unsigned kver_minor;
-  unsigned kver_sub;
-  char discard;
-  ret = sscanf(buf.release, "%u.%u.%u%c", &kver_major, &kver_minor, &kver_sub,
-               &discard);
-  // Check the device kernel version
-  if (ret < 3)
-    return 0;
-
+  unsigned kver_major = 0;
+  unsigned kver_minor = 0;
+  unsigned kver_sub = 0;
+  (void)sscanf(buf.release, "%u.%u.%u", &kver_major, &kver_minor, &kver_sub);
   return KVER(kver_major, kver_minor, kver_sub);
 }
 
+static unsigned kernelVersion() {
+  static unsigned kver = uncachedKernelVersion();
+  return kver;
+}
+
 static inline bool isAtLeastKernelVersion(unsigned major, unsigned minor,
                                           unsigned sub) {
   return kernelVersion() >= KVER(major, minor, sub);
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
index 8f9a1f9..e40cd6b 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
@@ -17,10 +17,12 @@
 package com.android.net.module.util;
 
 import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
 import static com.android.net.module.util.NetworkStackConstants.TCP_HEADER_MIN_LEN;
@@ -41,6 +43,7 @@
 
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Ipv4Header;
+import com.android.net.module.util.structs.Ipv6Header;
 import com.android.net.module.util.structs.TcpHeader;
 import com.android.net.module.util.structs.UdpHeader;
 
@@ -49,6 +52,7 @@
 
 import java.io.IOException;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.nio.ByteBuffer;
 
 @RunWith(AndroidJUnit4.class)
@@ -56,8 +60,10 @@
 public class PacketBuilderTest {
     private static final MacAddress SRC_MAC = MacAddress.fromString("11:22:33:44:55:66");
     private static final MacAddress DST_MAC = MacAddress.fromString("aa:bb:cc:dd:ee:ff");
-    private static final Inet4Address IPV4_SRC_ADDR = addr("192.0.2.1");
-    private static final Inet4Address IPV4_DST_ADDR = addr("198.51.100.1");
+    private static final Inet4Address IPV4_SRC_ADDR = addr4("192.0.2.1");
+    private static final Inet4Address IPV4_DST_ADDR = addr4("198.51.100.1");
+    private static final Inet6Address IPV6_SRC_ADDR = addr6("2001:db8::1");
+    private static final Inet6Address IPV6_DST_ADDR = addr6("2001:db8::2");
     private static final short SRC_PORT = 9876;
     private static final short DST_PORT = 433;
     private static final short SEQ_NO = 13579;
@@ -68,6 +74,9 @@
     private static final byte TIME_TO_LIVE = (byte) 0x40;
     private static final short WINDOW = (short) 0x2000;
     private static final short URGENT_POINTER = 0;
+    // version=6, traffic class=0x80, flowlabel=0x515ca;
+    private static final int VERSION_TRAFFICCLASS_FLOWLABEL = 0x680515ca;
+    private static final short HOP_LIMIT = 0x40;
     private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[] {
             (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
     });
@@ -256,15 +265,239 @@
                 (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
             };
 
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                     type='IPv6') /
+                //           scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433))
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x86, (byte) 0xdd,
+                // IP header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x08, (byte) 0x11, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x08, (byte) 0x7c, (byte) 0x24
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                       type='IPv6') /
+                //           scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433) /
+                //           b'\xde\xad\xbe\xef')
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x86, (byte) 0xdd,
+                // IP header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x0c, (byte) 0x11, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x0c, (byte) 0xde, (byte) 0x7e,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_TCPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                       type='IPv6') /
+                //           scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0) /
+                //           b'\xde\xad\xbe\xef')
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x86, (byte) 0xdd,
+                // IPv6 header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x18, (byte) 0x06, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0xd9, (byte) 0x05, (byte) 0x00, (byte) 0x00,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_TCPHDR =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                       type='IPv6') /
+                //           scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0))
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x86, (byte) 0xdd,
+                // IPv6 header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x14, (byte) 0x06, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0x76, (byte) 0xa7, (byte) 0x00, (byte) 0x00
+            };
+
+    private static final byte[] TEST_PACKET_IPV6HDR_TCPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0) /
+                //           b'\xde\xad\xbe\xef')
+                // IPv6 header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x18, (byte) 0x06, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0xd9, (byte) 0x05, (byte) 0x00, (byte) 0x00,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_IPV6HDR_TCPHDR =
+            new byte[] {
+                // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0))
+                // IPv6 header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x14, (byte) 0x06, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0x76, (byte) 0xa7, (byte) 0x00, (byte) 0x00
+            };
+
+    private static final byte[] TEST_PACKET_IPV6HDR_UDPHDR =
+            new byte[] {
+                // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433))
+                // IP header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x08, (byte) 0x11, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x08, (byte) 0x7c, (byte) 0x24
+            };
+
+    private static final byte[] TEST_PACKET_IPV6HDR_UDPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433) /
+                //           b'\xde\xad\xbe\xef')
+                // IP header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x0c, (byte) 0x11, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x0c, (byte) 0xde, (byte) 0x7e,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
     /**
-     * Build an IPv4 packet which has ether header, IPv4 header, TCP/UDP header and data.
+     * Build a packet which has ether header, IP header, TCP/UDP header and data.
      * The ethernet header and data are optional. Note that both source mac address and
      * destination mac address are required for ethernet header.
      *
      * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      * |                Layer 2 header (EthernetHeader)                | (optional)
      * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-     * |                  Layer 3 header (Ipv4Header)                  |
+     * |           Layer 3 header (Ipv4Header, Ipv6Header)             |
      * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      * |           Layer 4 header (TcpHeader, UdpHeader)               |
      * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
@@ -273,33 +506,55 @@
      *
      * @param srcMac source MAC address. used by L2 ether header.
      * @param dstMac destination MAC address. used by L2 ether header.
-     * @param l4proto the layer 4 protocol. support either IPPROTO_TCP or IPPROTO_UDP.
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} and {@code IPPROTO_IPV6}
+     *        currently supported.
+     * @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} and {@code IPPROTO_UDP}
+     *        currently supported.
      * @param payload the payload.
      */
     @NonNull
-    private ByteBuffer buildIpv4Packet(@Nullable final MacAddress srcMac,
-            @Nullable final MacAddress dstMac, final int l4proto,
+    private ByteBuffer buildPacket(@Nullable final MacAddress srcMac,
+            @Nullable final MacAddress dstMac, final int l3proto, final int l4proto,
             @Nullable final ByteBuffer payload)
             throws Exception {
+        if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
+            fail("Unsupported layer 3 protocol " + l3proto);
+        }
+
         if (l4proto != IPPROTO_TCP && l4proto != IPPROTO_UDP) {
             fail("Unsupported layer 4 protocol " + l4proto);
         }
 
         final boolean hasEther = (srcMac != null && dstMac != null);
         final int payloadLen = (payload == null) ? 0 : payload.limit();
-        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, IPPROTO_IP, l4proto,
+        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, l3proto, l4proto,
                 payloadLen);
         final PacketBuilder packetBuilder = new PacketBuilder(buffer);
 
-        if (hasEther) packetBuilder.writeL2Header(srcMac, dstMac, (short) ETHER_TYPE_IPV4);
-        packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
-                TIME_TO_LIVE, (byte) l4proto, IPV4_SRC_ADDR, IPV4_DST_ADDR);
+        // [1] Build ether header.
+        if (hasEther) {
+            final int etherType = (l3proto == IPPROTO_IP) ? ETHER_TYPE_IPV4 : ETHER_TYPE_IPV6;
+            packetBuilder.writeL2Header(srcMac, dstMac, (short) etherType);
+        }
+
+        // [2] Build IP header.
+        if (l3proto == IPPROTO_IP) {
+            packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                    TIME_TO_LIVE, (byte) l4proto, IPV4_SRC_ADDR, IPV4_DST_ADDR);
+        } else if (l3proto == IPPROTO_IPV6) {
+            packetBuilder.writeIpv6Header(VERSION_TRAFFICCLASS_FLOWLABEL,
+                    (byte) l4proto, HOP_LIMIT, IPV6_SRC_ADDR, IPV6_DST_ADDR);
+        }
+
+        // [3] Build TCP or UDP header.
         if (l4proto == IPPROTO_TCP) {
             packetBuilder.writeTcpHeader(SRC_PORT, DST_PORT, SEQ_NO, ACK_NO,
                     TCPHDR_ACK, WINDOW, URGENT_POINTER);
         } else if (l4proto == IPPROTO_UDP) {
             packetBuilder.writeUdpHeader(SRC_PORT, DST_PORT);
         }
+
+        // [4] Build payload.
         if (payload != null) {
             buffer.put(payload);
             // in case data might be reused by caller, restore the position and
@@ -313,19 +568,27 @@
     /**
      * Check ethernet header.
      *
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} and {@code IPPROTO_IPV6}
+     *        currently supported.
      * @param actual the packet to check.
      */
-    private void checkEtherHeader(final ByteBuffer actual) {
+    private void checkEtherHeader(final int l3proto, final ByteBuffer actual) {
+        if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
+            fail("Unsupported layer 3 protocol " + l3proto);
+        }
+
         final EthernetHeader eth = Struct.parse(EthernetHeader.class, actual);
         assertEquals(SRC_MAC, eth.srcMac);
         assertEquals(DST_MAC, eth.dstMac);
-        assertEquals(ETHER_TYPE_IPV4, eth.etherType);
+        final int expectedEtherType = (l3proto == IPPROTO_IP) ? ETHER_TYPE_IPV4 : ETHER_TYPE_IPV6;
+        assertEquals(expectedEtherType, eth.etherType);
     }
 
     /**
      * Check IPv4 header.
      *
-     * @param l4proto the layer 4 protocol. support either IPPROTO_TCP or IPPROTO_UDP.
+     * @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} and {@code IPPROTO_UDP}
+     *        currently supported.
      * @param hasData true if the packet has data payload; false otherwise.
      * @param actual the packet to check.
      */
@@ -359,19 +622,64 @@
     }
 
     /**
-     * Check TCPv4 packet.
+     * Check IPv6 header.
      *
-     * @param hasEther true if the packet has ether header; false otherwise.
+     * @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} and {@code IPPROTO_UDP}
+     *        currently supported.
      * @param hasData true if the packet has data payload; false otherwise.
      * @param actual the packet to check.
      */
-    private void checkTcpv4Packet(final boolean hasEther, final boolean hasData,
+    private void checkIpv6Header(final int l4proto, final boolean hasData,
             final ByteBuffer actual) {
-        if (hasEther) {
-            checkEtherHeader(actual);
+        if (l4proto != IPPROTO_TCP && l4proto != IPPROTO_UDP) {
+            fail("Unsupported layer 4 protocol " + l4proto);
         }
-        checkIpv4Header(IPPROTO_TCP, hasData, actual);
 
+        final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, actual);
+
+        assertEquals(VERSION_TRAFFICCLASS_FLOWLABEL, ipv6Header.vtf);
+        assertEquals(HOP_LIMIT, ipv6Header.hopLimit);
+        assertEquals(IPV6_SRC_ADDR, ipv6Header.srcIp);
+        assertEquals(IPV6_DST_ADDR, ipv6Header.dstIp);
+
+        final int dataLength = hasData ? DATA.limit() : 0;
+        if (l4proto == IPPROTO_TCP) {
+            assertEquals(TCP_HEADER_MIN_LEN + dataLength, ipv6Header.payloadLength);
+            assertEquals((byte) IPPROTO_TCP, ipv6Header.nextHeader);
+        } else if (l4proto == IPPROTO_UDP) {
+            assertEquals(UDP_HEADER_LEN + dataLength, ipv6Header.payloadLength);
+            assertEquals((byte) IPPROTO_UDP, ipv6Header.nextHeader);
+        }
+    }
+
+    /**
+     * Check TCP packet.
+     *
+     * @param hasEther true if the packet has ether header; false otherwise.
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} and {@code IPPROTO_IPV6}
+     *        currently supported.
+     * @param hasData true if the packet has data payload; false otherwise.
+     * @param actual the packet to check.
+     */
+    private void checkTcpPacket(final boolean hasEther, final int l3proto, final boolean hasData,
+            final ByteBuffer actual) {
+        if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
+            fail("Unsupported layer 3 protocol " + l3proto);
+        }
+
+        // [1] Check ether header.
+        if (hasEther) {
+            checkEtherHeader(l3proto, actual);
+        }
+
+        // [2] Check IP header.
+        if (l3proto == IPPROTO_IP) {
+            checkIpv4Header(IPPROTO_TCP, hasData, actual);
+        } else if (l3proto == IPPROTO_IPV6) {
+            checkIpv6Header(IPPROTO_TCP, hasData, actual);
+        }
+
+        // [3] Check TCP header.
         final TcpHeader tcpHeader = Struct.parse(TcpHeader.class, actual);
         assertEquals(SRC_PORT, tcpHeader.srcPort);
         assertEquals(DST_PORT, tcpHeader.dstPort);
@@ -380,35 +688,59 @@
         assertEquals((short) 0x5010 /* offset=5(*4bytes), control bits=ACK */,
                 tcpHeader.dataOffsetAndControlBits);
         assertEquals(WINDOW, tcpHeader.window);
-        assertEquals(hasData ? (short) 0x4844 : (short) 0xe5e5, tcpHeader.checksum);
         assertEquals(URGENT_POINTER, tcpHeader.urgentPointer);
+        if (l3proto == IPPROTO_IP) {
+            assertEquals(hasData ? (short) 0x4844 : (short) 0xe5e5, tcpHeader.checksum);
+        } else if (l3proto == IPPROTO_IPV6) {
+            assertEquals(hasData ? (short) 0xd905 : (short) 0x76a7, tcpHeader.checksum);
+        }
 
+        // [4] Check payload.
         if (hasData) {
             assertEquals(0xdeadbeef, actual.getInt());
         }
     }
 
     /**
-     * Check UDPv4 packet.
+     * Check UDP packet.
      *
      * @param hasEther true if the packet has ether header; false otherwise.
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} and {@code IPPROTO_IPV6}
+     *        currently supported.
      * @param hasData true if the packet has data payload; false otherwise.
      * @param actual the packet to check.
      */
-    private void checkUdpv4Packet(final boolean hasEther, final boolean hasData,
+    private void checkUdpPacket(final boolean hasEther, final int l3proto, final boolean hasData,
             final ByteBuffer actual) {
-        if (hasEther) {
-            checkEtherHeader(actual);
+        if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
+            fail("Unsupported layer 3 protocol " + l3proto);
         }
-        checkIpv4Header(IPPROTO_UDP, hasData, actual);
 
+        // [1] Check ether header.
+        if (hasEther) {
+            checkEtherHeader(l3proto, actual);
+        }
+
+        // [2] Check IP header.
+        if (l3proto == IPPROTO_IP) {
+            checkIpv4Header(IPPROTO_UDP, hasData, actual);
+        } else if (l3proto == IPPROTO_IPV6) {
+            checkIpv6Header(IPPROTO_UDP, hasData, actual);
+        }
+
+        // [3] Check UDP header.
         final UdpHeader udpHeader = Struct.parse(UdpHeader.class, actual);
         assertEquals(SRC_PORT, udpHeader.srcPort);
         assertEquals(DST_PORT, udpHeader.dstPort);
         final int dataLength = hasData ? DATA.limit() : 0;
         assertEquals(UDP_HEADER_LEN + dataLength, udpHeader.length);
-        assertEquals(hasData ? (short) 0x4dbd : (short) 0xeb62, udpHeader.checksum);
+        if (l3proto == IPPROTO_IP) {
+            assertEquals(hasData ? (short) 0x4dbd : (short) 0xeb62, udpHeader.checksum);
+        } else if (l3proto == IPPROTO_IPV6) {
+            assertEquals(hasData ? (short) 0xde7e : (short) 0x7c24, udpHeader.checksum);
+        }
 
+        // [4] Check payload.
         if (hasData) {
             assertEquals(0xdeadbeef, actual.getInt());
         }
@@ -416,66 +748,133 @@
 
     @Test
     public void testBuildPacketEtherIPv4Tcp() throws Exception {
-        final ByteBuffer packet = buildIpv4Packet(SRC_MAC, DST_MAC, IPPROTO_TCP, null /* data */);
-        checkTcpv4Packet(true /* hasEther */, false /* hasData */, packet);
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IP, IPPROTO_TCP,
+                null /* data */);
+        checkTcpPacket(true /* hasEther */, IPPROTO_IP, false /* hasData */, packet);
         assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR, packet.array());
     }
 
     @Test
     public void testBuildPacketEtherIPv4TcpData() throws Exception {
-        final ByteBuffer packet = buildIpv4Packet(SRC_MAC, DST_MAC, IPPROTO_TCP, DATA);
-        checkTcpv4Packet(true /* hasEther */, true /* hasData */, packet);
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IP, IPPROTO_TCP, DATA);
+        checkTcpPacket(true /* hasEther */, IPPROTO_IP, true /* hasData */, packet);
         assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR_DATA,
                 packet.array());
     }
 
     @Test
     public void testBuildPacketIPv4Tcp() throws Exception {
-        final ByteBuffer packet = buildIpv4Packet(null /* srcMac */, null /* dstMac */,
-                IPPROTO_TCP, null /* data */);
-        checkTcpv4Packet(false /* hasEther */, false /* hasData */, packet);
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IP, IPPROTO_TCP, null /* data */);
+        checkTcpPacket(false /* hasEther */, IPPROTO_IP, false /* hasData */, packet);
         assertArrayEquals(TEST_PACKET_IPV4HDR_TCPHDR, packet.array());
     }
 
     @Test
     public void testBuildPacketIPv4TcpData() throws Exception {
-        final ByteBuffer packet = buildIpv4Packet(null /* srcMac */, null /* dstMac */,
-                IPPROTO_TCP, DATA);
-        checkTcpv4Packet(false /* hasEther */, true /* hasData */, packet);
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IP, IPPROTO_TCP, DATA);
+        checkTcpPacket(false /* hasEther */, IPPROTO_IP, true /* hasData */, packet);
         assertArrayEquals(TEST_PACKET_IPV4HDR_TCPHDR_DATA, packet.array());
     }
 
     @Test
     public void testBuildPacketEtherIPv4Udp() throws Exception {
-        final ByteBuffer packet = buildIpv4Packet(SRC_MAC, DST_MAC, IPPROTO_UDP, null /* data */);
-        checkUdpv4Packet(true /* hasEther */, false /* hasData */, packet);
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IP, IPPROTO_UDP,
+                null /* data */);
+        checkUdpPacket(true /* hasEther */, IPPROTO_IP, false /* hasData */, packet);
         assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_UDPHDR, packet.array());
     }
 
     @Test
     public void testBuildPacketEtherIPv4UdpData() throws Exception {
-        final ByteBuffer packet = buildIpv4Packet(SRC_MAC, DST_MAC, IPPROTO_UDP, DATA);
-        checkUdpv4Packet(true /* hasEther */, true /* hasData */, packet);
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IP, IPPROTO_UDP, DATA);
+        checkUdpPacket(true /* hasEther */, IPPROTO_IP, true /* hasData */, packet);
         assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_UDPHDR_DATA, packet.array());
     }
 
     @Test
     public void testBuildPacketIPv4Udp() throws Exception {
-        final ByteBuffer packet = buildIpv4Packet(null /* srcMac */, null /* dstMac */,
-                IPPROTO_UDP, null /*data*/);
-        checkUdpv4Packet(false /* hasEther */, false /* hasData */, packet);
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IP, IPPROTO_UDP, null /*data*/);
+        checkUdpPacket(false /* hasEther */, IPPROTO_IP, false /* hasData */, packet);
         assertArrayEquals(TEST_PACKET_IPV4HDR_UDPHDR, packet.array());
     }
 
     @Test
     public void testBuildPacketIPv4UdpData() throws Exception {
-        final ByteBuffer packet = buildIpv4Packet(null /* srcMac */, null /* dstMac */,
-                IPPROTO_UDP, DATA);
-        checkUdpv4Packet(false /* hasEther */, true /* hasData */, packet);
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IP, IPPROTO_UDP, DATA);
+        checkUdpPacket(false /* hasEther */, IPPROTO_IP, true /* hasData */, packet);
         assertArrayEquals(TEST_PACKET_IPV4HDR_UDPHDR_DATA, packet.array());
     }
 
     @Test
+    public void testBuildPacketEtherIPv6TcpData() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_TCP, DATA);
+        checkTcpPacket(true /* hasEther */, IPPROTO_IPV6, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV6HDR_TCPHDR_DATA,
+                packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv6Tcp() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_TCP,
+                null /*data*/);
+        checkTcpPacket(true /* hasEther */, IPPROTO_IPV6, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV6HDR_TCPHDR,
+                packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv6TcpData() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */, IPPROTO_IPV6,
+                IPPROTO_TCP, DATA);
+        checkTcpPacket(false /* hasEther */, IPPROTO_IPV6, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV6HDR_TCPHDR_DATA, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv6Tcp() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */, IPPROTO_IPV6,
+                IPPROTO_TCP, null /*data*/);
+        checkTcpPacket(false /* hasEther */, IPPROTO_IPV6, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV6HDR_TCPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv6Udp() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_UDP,
+                null /* data */);
+        checkUdpPacket(true /* hasEther */, IPPROTO_IPV6, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv6UdpData() throws Exception {
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_UDP,
+                DATA);
+        checkUdpPacket(true /* hasEther */, IPPROTO_IPV6, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv6Udp() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IPV6, IPPROTO_UDP, null /*data*/);
+        checkUdpPacket(false /* hasEther */, IPPROTO_IPV6, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV6HDR_UDPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv6UdpData() throws Exception {
+        final ByteBuffer packet = buildPacket(null /* srcMac */, null /* dstMac */,
+                IPPROTO_IPV6, IPPROTO_UDP, DATA);
+        checkUdpPacket(false /* hasEther */, IPPROTO_IPV6, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV6HDR_UDPHDR_DATA, packet.array());
+    }
+
+    @Test
     public void testFinalizePacketWithoutIpv4Header() throws Exception {
         final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */, IPPROTO_IP,
                 IPPROTO_TCP, 0 /* payloadLen */);
@@ -526,7 +925,11 @@
         assertThrows(IOException.class, () -> packetBuilder.writeUdpHeader(SRC_PORT, DST_PORT));
     }
 
-    private static Inet4Address addr(String addr) {
+    private static Inet4Address addr4(String addr) {
         return (Inet4Address) InetAddresses.parseNumericAddress(addr);
     }
+
+    private static Inet6Address addr6(String addr) {
+        return (Inet6Address) InetAddresses.parseNumericAddress(addr);
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
index 392314f..55cfd50 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
@@ -63,15 +63,7 @@
         return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
     }
 
-    @Test
-    public void testParseRtmRouteAddress() {
-        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
-        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
-        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
-        assertNotNull(msg);
-        assertTrue(msg instanceof RtNetlinkRouteMessage);
-        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
-
+    private void assertRtmRouteMessage(final RtNetlinkRouteMessage routeMsg) {
         final StructNlMsgHdr hdr = routeMsg.getHeader();
         assertNotNull(hdr);
         assertEquals(136, hdr.nlmsg_len);
@@ -97,6 +89,18 @@
         assertEquals((Inet6Address) routeMsg.getGateway(), TEST_IPV6_LINK_LOCAL_GATEWAY);
     }
 
+    @Test
+    public void testParseRtmRouteMessage() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        assertRtmRouteMessage(routeMsg);
+    }
+
     private static final String RTM_NEWROUTE_PACK_HEX =
             "4C000000180000060000000000000000"             // struct nlmsghr
             + "0A400000FC02000100000000"                   // struct rtmsg
@@ -143,7 +147,7 @@
             + "08000400DF020000";                          // RTA_OIF
 
     @Test
-    public void testParseRtmRouteAddress_IPv4MappedIPv6Gateway() {
+    public void testParseRtmRouteMessage_IPv4MappedIPv6Gateway() {
         final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_IPV4_MAPPED_IPV6_GATEWAY_HEX);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
         final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
@@ -160,7 +164,7 @@
             + "08000400DF020000";                          // RTA_OIF
 
     @Test
-    public void testParseRtmRouteAddress_IPv4MappedIPv6Destination() {
+    public void testParseRtmRouteMessage_IPv4MappedIPv6Destination() {
         final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_IPV4_MAPPED_IPV6_DST_HEX);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
         final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
@@ -169,6 +173,32 @@
         assertNull(msg);
     }
 
+    // An example of the full RTM_NEWADDR message.
+    private static final String RTM_NEWADDR_HEX =
+            "48000000140000000000000000000000"            // struct nlmsghr
+            + "0A4080FD1E000000"                          // struct ifaddrmsg
+            + "14000100FE800000000000002C415CFFFE096665"  // IFA_ADDRESS
+            + "14000600100E0000201C00002A70000045700000"  // IFA_CACHEINFO
+            + "0800080080000000";                         // IFA_FLAGS
+
+    @Test
+    public void testParseMultipleRtmMessagesInOneByteBuffer() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX + RTM_NEWADDR_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+
+        // Try to parse the RTM_NEWROUTE message.
+        NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        assertRtmRouteMessage(routeMsg);
+
+        // Try to parse the RTM_NEWADDR message.
+        msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkAddressMessage);
+    }
+
     @Test
     public void testToString() {
         final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
diff --git a/staticlibs/tests/unit/src/com/android/testutils/DeviceInfoUtilsTest.java b/staticlibs/tests/unit/src/com/android/testutils/DeviceInfoUtilsTest.java
index f99700a..6f603d5 100644
--- a/staticlibs/tests/unit/src/com/android/testutils/DeviceInfoUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/testutils/DeviceInfoUtilsTest.java
@@ -17,6 +17,9 @@
 package com.android.testutils;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
 
 import androidx.test.filters.SmallTest;
 
@@ -42,4 +45,65 @@
         assertEquals(-1, DeviceInfoUtils.compareMajorMinorVersion("3.10", "4.8.0"));
         assertEquals(-1, DeviceInfoUtils.compareMajorMinorVersion("4.7.10.10", "4.8"));
     }
+
+    @Test
+    public void testGetMajorMinorSubminorVersion() throws Exception {
+        final DeviceInfoUtils.KVersion expected = new DeviceInfoUtils.KVersion(4, 19, 220);
+        assertEquals(expected, DeviceInfoUtils.getMajorMinorSubminorVersion("4.19.220"));
+        assertEquals(expected, DeviceInfoUtils.getMajorMinorSubminorVersion("4.19.220.50"));
+        assertEquals(expected, DeviceInfoUtils.getMajorMinorSubminorVersion(
+                "4.19.220-g500ede0aed22-ab8272303"));
+
+        final DeviceInfoUtils.KVersion expected2 = new DeviceInfoUtils.KVersion(5, 17, 0);
+        assertEquals(expected2, DeviceInfoUtils.getMajorMinorSubminorVersion("5.17"));
+        assertEquals(expected2, DeviceInfoUtils.getMajorMinorSubminorVersion("5.17."));
+        assertEquals(expected2, DeviceInfoUtils.getMajorMinorSubminorVersion("5.17.beta"));
+        assertEquals(expected2, DeviceInfoUtils.getMajorMinorSubminorVersion(
+                "5.17-rc6-g52099515ca00-ab8032400"));
+
+        final DeviceInfoUtils.KVersion invalid = new DeviceInfoUtils.KVersion(0, 0, 0);
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion(""));
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion("4"));
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion("4."));
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion("4-beta"));
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion("1.x.1"));
+        assertEquals(invalid, DeviceInfoUtils.getMajorMinorSubminorVersion("x.1.1"));
+    }
+
+    @Test
+    public void testVersion() throws Exception {
+        final DeviceInfoUtils.KVersion v1 = new DeviceInfoUtils.KVersion(4, 8, 1);
+        final DeviceInfoUtils.KVersion v2 = new DeviceInfoUtils.KVersion(4, 8, 1);
+        final DeviceInfoUtils.KVersion v3 = new DeviceInfoUtils.KVersion(4, 8, 2);
+        final DeviceInfoUtils.KVersion v4 = new DeviceInfoUtils.KVersion(4, 9, 1);
+        final DeviceInfoUtils.KVersion v5 = new DeviceInfoUtils.KVersion(5, 8, 1);
+
+        assertEquals(v1, v2);
+        assertNotEquals(v1, v3);
+        assertNotEquals(v1, v4);
+        assertNotEquals(v1, v5);
+
+        assertEquals(0, v1.compareTo(v2));
+        assertEquals(-1, v1.compareTo(v3));
+        assertEquals(1, v3.compareTo(v1));
+        assertEquals(-1, v1.compareTo(v4));
+        assertEquals(1, v4.compareTo(v1));
+        assertEquals(-1, v1.compareTo(v5));
+        assertEquals(1, v5.compareTo(v1));
+
+        assertTrue(v2.isInRange(v1, v5));
+        assertTrue(v3.isInRange(v1, v5));
+        assertTrue(v4.isInRange(v1, v5));
+        assertFalse(v5.isInRange(v1, v5));
+        assertFalse(v1.isInRange(v3, v5));
+        assertFalse(v5.isInRange(v2, v4));
+
+        assertTrue(v2.isAtLeast(v1));
+        assertTrue(v3.isAtLeast(v1));
+        assertTrue(v4.isAtLeast(v1));
+        assertTrue(v5.isAtLeast(v1));
+        assertFalse(v1.isAtLeast(v3));
+        assertFalse(v1.isAtLeast(v4));
+        assertFalse(v1.isAtLeast(v5));
+    }
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
index 8b58e71..6a073ea 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
@@ -17,16 +17,21 @@
 package com.android.testutils
 
 import android.os.Build
+import androidx.test.InstrumentationRegistry
 import com.android.modules.utils.build.SdkLevel
 import kotlin.test.fail
 import org.junit.Assume.assumeTrue
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
+import java.util.regex.Pattern
 
 // TODO: Remove it when Build.VERSION_CODES.SC_V2 is available
 const val SC_V2 = 32
 
+private val MAX_TARGET_SDK_ANNOTATION_RE = Pattern.compile("MaxTargetSdk([0-9]+)$")
+private val targetSdk = InstrumentationRegistry.getContext().applicationInfo.targetSdkVersion
+
 /**
  * Returns true if the development SDK version of the device is in the provided range.
  *
@@ -67,6 +72,14 @@
     }
 }
 
+private fun getMaxTargetSdk(description: Description): Int? {
+    return description.annotations.firstNotNullOfOrNull {
+        MAX_TARGET_SDK_ANNOTATION_RE.matcher(it::class.simpleName).let { m ->
+            if (m.find()) m.group(1).toIntOrNull() else null
+        }
+    }
+}
+
 /**
  * A test rule to ignore tests based on the development SDK level.
  *
@@ -108,11 +121,17 @@
             val ignoreAfter = description.getAnnotation(IgnoreAfter::class.java)
             val ignoreUpTo = description.getAnnotation(IgnoreUpTo::class.java)
 
-            val message = "Skipping test for build ${Build.VERSION.CODENAME} " +
+            val devSdkMessage = "Skipping test for build ${Build.VERSION.CODENAME} " +
                     "with SDK ${Build.VERSION.SDK_INT}"
-            assumeTrue(message, isDevSdkInRange(ignoreClassUpTo, ignoreClassAfter))
-            assumeTrue(message, isDevSdkInRange(ignoreUpTo?.value, ignoreAfter?.value))
+            assumeTrue(devSdkMessage, isDevSdkInRange(ignoreClassUpTo, ignoreClassAfter))
+            assumeTrue(devSdkMessage, isDevSdkInRange(ignoreUpTo?.value, ignoreAfter?.value))
+
+            val maxTargetSdk = getMaxTargetSdk(description)
+            if (maxTargetSdk != null) {
+                assumeTrue("Skipping test, target SDK $targetSdk greater than $maxTargetSdk",
+                        targetSdk <= maxTargetSdk)
+            }
             base.evaluate()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
index 69cb5f8..1925b55 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
@@ -26,7 +26,78 @@
  * Utilities for device information.
  */
 public class DeviceInfoUtils {
-    private static Pair<Integer, Integer> getVersionFromString(String version) {
+    /**
+     * Class for a three-part kernel version number.
+     */
+    public static class KVersion {
+        public final int major;
+        public final int minor;
+        public final int sub;
+
+        public KVersion(int major, int minor, int sub) {
+            this.major = major;
+            this.minor = minor;
+            this.sub = sub;
+        }
+
+        /**
+         * Compares with other version numerically.
+         *
+         * @param  other the other version to compare
+         * @return the value 0 if this == other;
+         *         a value less than 0 if this < other and
+         *         a value greater than 0 if this > other.
+         */
+        public int compareTo(final KVersion other) {
+            int res = Integer.compare(this.major, other.major);
+            if (res == 0) {
+                res = Integer.compare(this.minor, other.minor);
+            }
+            if (res == 0) {
+                res = Integer.compare(this.sub, other.sub);
+            }
+            return res;
+        }
+
+        /**
+         * At least satisfied with the given version.
+         *
+         * @param  from the start version to compare
+         * @return return true if this version is at least satisfied with the given version.
+         *         otherwise, return false.
+         */
+        public boolean isAtLeast(final KVersion from) {
+            return compareTo(from) >= 0;
+        }
+
+        /**
+         * Falls within the given range [from, to).
+         *
+         * @param  from the start version to compare
+         * @param  to   the end version to compare
+         * @return return true if this version falls within the given range.
+         *         otherwise, return false.
+         */
+        public boolean isInRange(final KVersion from, final KVersion to) {
+            return isAtLeast(from) && !isAtLeast(to);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof KVersion)) return false;
+            KVersion that = (KVersion) o;
+            return this.major == that.major
+                    && this.minor == that.minor
+                    && this.sub == that.sub;
+        }
+    };
+
+    /**
+     * Get a two-part kernel version number (major and minor) from a given string.
+     *
+     * TODO: use class KVersion.
+     */
+    private static Pair<Integer, Integer> getMajorMinorVersion(String version) {
         // Only gets major and minor number of the version string.
         final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*");
         final Matcher m = versionPattern.matcher(version);
@@ -49,10 +120,12 @@
      * @return the value 0 if s1 == s2;
      *         a value less than 0 if s1 < s2 and
      *         a value greater than 0 if s1 > s2.
+     *
+     * TODO: use class KVersion.
      */
     public static int compareMajorMinorVersion(final String s1, final String s2) {
-        final Pair<Integer, Integer> v1 = getVersionFromString(s1);
-        final Pair<Integer, Integer> v2 = getVersionFromString(s2);
+        final Pair<Integer, Integer> v1 = getMajorMinorVersion(s1);
+        final Pair<Integer, Integer> v2 = getMajorMinorVersion(s2);
 
         if (v1.first == v2.first) {
             return Integer.compare(v1.second, v2.second);
@@ -60,4 +133,28 @@
             return Integer.compare(v1.first, v2.first);
         }
     }
+
+    /**
+     * Get a three-part kernel version number (major, minor and subminor) from a given string.
+     * Any version string must at least have major and minor number. If the subminor number can't
+     * be parsed from string. Assign zero as subminor number. Invalid version is treated as
+     * version 0.0.0.
+     */
+    public static KVersion getMajorMinorSubminorVersion(final String version) {
+        // The kernel version is a three-part version number (major, minor and subminor). Get
+        // the three-part version numbers and discard the remaining stuff if any.
+        // For example:
+        //   4.19.220-g500ede0aed22-ab8272303 --> 4.19.220
+        //   5.17-rc6-g52099515ca00-ab8032400 --> 5.17.0
+        final Pattern versionPattern = Pattern.compile("^(\\d+)\\.(\\d+)(\\.(\\d+))?.*");
+        final Matcher m = versionPattern.matcher(version);
+        if (m.matches()) {
+            final int major = Integer.parseInt(m.group(1));
+            final int minor = Integer.parseInt(m.group(2));
+            final int sub = TextUtils.isEmpty(m.group(4)) ? 0 : Integer.parseInt(m.group(4));
+            return new KVersion(major, minor, sub);
+        } else {
+            return new KVersion(0, 0, 0);
+        }
+    }
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk31.kt b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk31.kt
new file mode 100644
index 0000000..be0103d
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk31.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.filters
+
+/**
+ * Only run this test in the CtsNetTestCasesMaxTargetSdk31 suite.
+ */
+annotation class CtsNetTestCasesMaxTargetSdk31(val reason: String)