PacketBuilder: add IPv6 support

Support IPv6 packet builder and verify with UDP packet unit test.

Bug: 215655463
Test: atest NetworkStaticLibTests

Change-Id: I5edf0ffbc9b37ecf0e0a6da0dc7d04716fc4c38c
diff --git a/staticlibs/device/com/android/net/module/util/PacketBuilder.java b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
index c908528..8acb296 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,7 @@
 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_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 +37,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 +53,7 @@
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  * |                Layer 2 header (EthernetHeader)                | (optional)
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- * |                  Layer 3 header (Ipv4Header)                  |
+ * |           Layer 3 header (Ipv4Header, Ipv6Header)             |
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  * |           Layer 4 header (TcpHeader, UdpHeader)               |
  * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
@@ -74,11 +78,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;
@@ -130,6 +137,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 +218,42 @@
      */
     @NonNull
     public ByteBuffer finalizePacket() throws IOException {
-        if (mIpv4HeaderOffset < 0) {
-            // TODO: add support for IPv6
-            throw new IOException("Packet is missing IPv4 header");
+        // 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.
+            mBuffer.putShort(mIpv6HeaderOffset + IPV6_LEN_OFFSET,
+                    (short) (mBuffer.position() - mIpv6HeaderOffset));
+        } 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) {
+        // 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 +267,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 +289,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/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
index 8f9a1f9..c02763a 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,11 +17,14 @@
 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.IPV6_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
 import static com.android.net.module.util.NetworkStackConstants.TCP_HEADER_MIN_LEN;
 import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
@@ -41,6 +44,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 +53,7 @@
 
 import java.io.IOException;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.nio.ByteBuffer;
 
 @RunWith(AndroidJUnit4.class)
@@ -56,8 +61,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 +75,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 +266,123 @@
                 (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, plen=48, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433))
+                // Note that plen(48) = ipv6hdr(40) + udphdr(8).
+                // 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) 0x30, (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, plen=52, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433) /
+                //           b'\xde\xad\xbe\xef')
+                // Note that plen(52) = ipv6hdr(40) + udphdr(8) + data(4).
+                // 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) 0x34, (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_IPV6HDR_UDPHDR =
+            new byte[] {
+                // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80,
+                //                      fl=0x515ca, plen=48, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433))
+                // Note that plen(48) = ipv6hdr(40) + udphdr(8).
+                // IP header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x30, (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, plen=52, hlim=0x40) /
+                //           scapy.UDP(sport=9876, dport=433) /
+                //           b'\xde\xad\xbe\xef')
+                // Note that plen(52) = ipv6hdr(40) + udphdr(8) + data(4).
+                // IP header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0x34, (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 +391,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 +453,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,16 +507,57 @@
     }
 
     /**
-     * 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 (l4proto != IPPROTO_TCP && l4proto != IPPROTO_UDP) {
+            fail("Unsupported layer 4 protocol " + l4proto);
+        }
+
+        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(IPV6_HEADER_LEN + TCP_HEADER_MIN_LEN + dataLength,
+                    ipv6Header.payloadLength);
+            assertEquals((byte) IPPROTO_TCP, ipv6Header.nextHeader);
+        } else if (l4proto == IPPROTO_UDP) {
+            assertEquals(IPV6_HEADER_LEN + UDP_HEADER_LEN + dataLength,
+                    ipv6Header.payloadLength);
+            assertEquals((byte) IPPROTO_UDP, ipv6Header.nextHeader);
+        }
+    }
+
+    /**
+     * Check TCPv4 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.
+     *
+     * TODO: support IPv6
+     */
+    private void checkTcpv4Packet(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);
+        }
+
         if (hasEther) {
-            checkEtherHeader(actual);
+            checkEtherHeader(l3proto, actual);
         }
         checkIpv4Header(IPPROTO_TCP, hasData, actual);
 
@@ -389,26 +578,45 @@
     }
 
     /**
-     * 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 +624,100 @@
 
     @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 */);
+        checkTcpv4Packet(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);
+        checkTcpv4Packet(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 */);
+        checkTcpv4Packet(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);
+        checkTcpv4Packet(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 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 +768,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);
+    }
 }