Merge "Support for Venue friendly name"
diff --git a/common/moduleutils/src/android/net/ip/ConntrackMonitor.java b/common/moduleutils/src/android/net/ip/ConntrackMonitor.java
index b0fc48a..95eda96 100644
--- a/common/moduleutils/src/android/net/ip/ConntrackMonitor.java
+++ b/common/moduleutils/src/android/net/ip/ConntrackMonitor.java
@@ -16,7 +16,11 @@
 
 package android.net.ip;
 
+import static android.net.netlink.ConntrackMessage.DYING_MASK;
+import static android.net.netlink.ConntrackMessage.ESTABLISHED_MASK;
+
 import android.net.netlink.ConntrackMessage;
+import android.net.netlink.NetlinkConstants;
 import android.net.netlink.NetlinkMessage;
 import android.net.util.SharedLog;
 import android.os.Handler;
@@ -24,6 +28,11 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+
+
 /**
  * ConntrackMonitor.
  *
@@ -45,7 +54,96 @@
     /**
      * A class for describing parsed netfilter conntrack events.
      */
-    public static class ConntrackEvent { /*TODO*/ }
+    public static class ConntrackEvent {
+        /**
+         * Conntrack event type.
+         */
+        public final short msgType;
+        /**
+         * Original direction conntrack tuple.
+         */
+        public final ConntrackMessage.Tuple tupleOrig;
+        /**
+         * Reply direction conntrack tuple.
+         */
+        public final ConntrackMessage.Tuple tupleReply;
+        /**
+         * Connection status. A bitmask of ip_conntrack_status enum flags.
+         */
+        public final int status;
+        /**
+         * Conntrack timeout.
+         */
+        public final int timeoutSec;
+
+        public ConntrackEvent(ConntrackMessage msg) {
+            this.msgType = msg.getHeader().nlmsg_type;
+            this.tupleOrig = msg.tupleOrig;
+            this.tupleReply = msg.tupleReply;
+            this.status = msg.status;
+            this.timeoutSec = msg.timeoutSec;
+        }
+
+        @VisibleForTesting
+        public ConntrackEvent(short msgType, ConntrackMessage.Tuple tupleOrig,
+                ConntrackMessage.Tuple tupleReply, int status, int timeoutSec) {
+            this.msgType = msgType;
+            this.tupleOrig = tupleOrig;
+            this.tupleReply = tupleReply;
+            this.status = status;
+            this.timeoutSec = timeoutSec;
+        }
+
+        @Override
+        @VisibleForTesting
+        public boolean equals(Object o) {
+            if (!(o instanceof ConntrackEvent)) return false;
+            ConntrackEvent that = (ConntrackEvent) o;
+            return this.msgType == that.msgType
+                    && Objects.equals(this.tupleOrig, that.tupleOrig)
+                    && Objects.equals(this.tupleReply, that.tupleReply)
+                    && this.status == that.status
+                    && this.timeoutSec == that.timeoutSec;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(msgType, tupleOrig, tupleReply, status, timeoutSec);
+        }
+
+        /**
+         * Check the established NAT session conntrack message.
+         *
+         * @param msg the conntrack message to check.
+         * @return true if an established NAT message, false if not.
+         */
+        public static boolean isEstablishedNatSession(@NonNull ConntrackMessage msg) {
+            if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_NEW) return false;
+            if (msg.tupleOrig == null) return false;
+            if (msg.tupleReply == null) return false;
+            if (msg.timeoutSec == 0) return false;
+            if ((msg.status & ESTABLISHED_MASK) != ESTABLISHED_MASK) return false;
+
+            return true;
+        }
+
+        /**
+         * Check the dying NAT session conntrack message.
+         * Note that IPCTNL_MSG_CT_DELETE event has no CTA_TIMEOUT attribute.
+         *
+         * @param msg the conntrack message to check.
+         * @return true if a dying NAT message, false if not.
+         */
+        public static boolean isDyingNatSession(@NonNull ConntrackMessage msg) {
+            if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_DELETE) return false;
+            if (msg.tupleOrig == null) return false;
+            if (msg.tupleReply == null) return false;
+            if (msg.timeoutSec != 0) return false;
+            if ((msg.status & DYING_MASK) != DYING_MASK) return false;
+
+            return true;
+        }
+    }
 
     /**
      * A callback to caller for conntrack event.
@@ -74,6 +172,12 @@
             return;
         }
 
-        mConsumer.accept(new ConntrackEvent() /* TODO */);
+        final ConntrackMessage conntrackMsg = (ConntrackMessage) nlMsg;
+        if (!(ConntrackEvent.isEstablishedNatSession(conntrackMsg)
+                || ConntrackEvent.isDyingNatSession(conntrackMsg))) {
+            return;
+        }
+
+        mConsumer.accept(new ConntrackEvent(conntrackMsg));
     }
 }
diff --git a/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java b/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
index 30a1165..7f34547 100644
--- a/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
+++ b/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
@@ -16,18 +16,27 @@
 
 package android.net.netlink;
 
+import static android.net.netlink.StructNlAttr.findNextAttrOfType;
+import static android.net.netlink.StructNlAttr.makeNestedType;
 import static android.net.netlink.StructNlMsgHdr.NLM_F_ACK;
 import static android.net.netlink.StructNlMsgHdr.NLM_F_REPLACE;
 import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
 
 import static java.nio.ByteOrder.BIG_ENDIAN;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.system.OsConstants;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.net.Inet4Address;
+import java.net.InetAddress;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Objects;
 
 
 /**
@@ -59,6 +68,106 @@
     public static final short CTA_PROTO_SRC_PORT = 2;
     public static final short CTA_PROTO_DST_PORT = 3;
 
+    // enum ip_conntrack_status
+    public static final int IPS_EXPECTED      = 0x00000001;
+    public static final int IPS_SEEN_REPLY    = 0x00000002;
+    public static final int IPS_ASSURED       = 0x00000004;
+    public static final int IPS_CONFIRMED     = 0x00000008;
+    public static final int IPS_SRC_NAT       = 0x00000010;
+    public static final int IPS_DST_NAT       = 0x00000020;
+    public static final int IPS_SEQ_ADJUST    = 0x00000040;
+    public static final int IPS_SRC_NAT_DONE  = 0x00000080;
+    public static final int IPS_DST_NAT_DONE  = 0x00000100;
+    public static final int IPS_DYING         = 0x00000200;
+    public static final int IPS_FIXED_TIMEOUT = 0x00000400;
+    public static final int IPS_TEMPLATE      = 0x00000800;
+    public static final int IPS_UNTRACKED     = 0x00001000;
+    public static final int IPS_HELPER        = 0x00002000;
+    public static final int IPS_OFFLOAD       = 0x00004000;
+    public static final int IPS_HW_OFFLOAD    = 0x00008000;
+
+    // ip_conntrack_status mask
+    // Interesting on the NAT conntrack session which has already seen two direction traffic.
+    // TODO: Probably IPS_{SRC, DST}_NAT_DONE are also interesting.
+    public static final int ESTABLISHED_MASK = IPS_CONFIRMED | IPS_ASSURED | IPS_SEEN_REPLY
+            | IPS_SRC_NAT;
+    // Interesting on the established NAT conntrack session which is dying.
+    public static final int DYING_MASK = ESTABLISHED_MASK | IPS_DYING;
+
+    /**
+     * A tuple for the conntrack connection information.
+     *
+     * see also CTA_TUPLE_ORIG and CTA_TUPLE_REPLY.
+     */
+    public static class Tuple {
+        public final Inet4Address srcIp;
+        public final Inet4Address dstIp;
+
+        // Both port and protocol number are unsigned numbers stored in signed integers, and that
+        // callers that want to compare them to integers should either cast those integers, or
+        // convert them to unsigned using Byte.toUnsignedInt() and Short.toUnsignedInt().
+        public final short srcPort;
+        public final short dstPort;
+        public final byte protoNum;
+
+        public Tuple(TupleIpv4 ip, TupleProto proto) {
+            this.srcIp = ip.src;
+            this.dstIp = ip.dst;
+            this.srcPort = proto.srcPort;
+            this.dstPort = proto.dstPort;
+            this.protoNum = proto.protoNum;
+        }
+
+        @Override
+        @VisibleForTesting
+        public boolean equals(Object o) {
+            if (!(o instanceof Tuple)) return false;
+            Tuple that = (Tuple) o;
+            return Objects.equals(this.srcIp, that.srcIp)
+                    && Objects.equals(this.dstIp, that.dstIp)
+                    && this.srcPort == that.srcPort
+                    && this.dstPort == that.dstPort
+                    && this.protoNum == that.protoNum;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(srcIp, dstIp, srcPort, dstPort, protoNum);
+        }
+    }
+
+    /**
+     * A tuple for the conntrack connection address.
+     *
+     * see also CTA_TUPLE_IP.
+     */
+    public static class TupleIpv4 {
+        public final Inet4Address src;
+        public final Inet4Address dst;
+
+        public TupleIpv4(Inet4Address src, Inet4Address dst) {
+            this.src = src;
+            this.dst = dst;
+        }
+    }
+
+    /**
+     * A tuple for the conntrack connection protocol.
+     *
+     * see also CTA_TUPLE_PROTO.
+     */
+    public static class TupleProto {
+        public final byte protoNum;
+        public final short srcPort;
+        public final short dstPort;
+
+        public TupleProto(byte protoNum, short srcPort, short dstPort) {
+            this.protoNum = protoNum;
+            this.srcPort = srcPort;
+            this.dstPort = dstPort;
+        }
+    }
+
     public static byte[] newIPv4TimeoutUpdateRequest(
             int proto, Inet4Address src, int sport, Inet4Address dst, int dport, int timeoutSec) {
         // *** STYLE WARNING ***
@@ -113,19 +222,33 @@
         }
 
         final int baseOffset = byteBuffer.position();
-        StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(CTA_STATUS, byteBuffer);
+        StructNlAttr nlAttr = findNextAttrOfType(CTA_STATUS, byteBuffer);
         int status = 0;
         if (nlAttr != null) {
             status = nlAttr.getValueAsBe32(0);
         }
 
         byteBuffer.position(baseOffset);
-        nlAttr = StructNlAttr.findNextAttrOfType(CTA_TIMEOUT, byteBuffer);
+        nlAttr = findNextAttrOfType(CTA_TIMEOUT, byteBuffer);
         int timeoutSec = 0;
         if (nlAttr != null) {
             timeoutSec = nlAttr.getValueAsBe32(0);
         }
 
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_ORIG), byteBuffer);
+        Tuple tupleOrig = null;
+        if (nlAttr != null) {
+            tupleOrig = parseTuple(nlAttr.getValueAsByteBuffer());
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_REPLY), byteBuffer);
+        Tuple tupleReply = null;
+        if (nlAttr != null) {
+            tupleReply = parseTuple(nlAttr.getValueAsByteBuffer());
+        }
+
         // Advance to the end of the message.
         byteBuffer.position(baseOffset);
         final int kMinConsumed = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
@@ -136,7 +259,126 @@
         }
         byteBuffer.position(baseOffset + kAdditionalSpace);
 
-        return new ConntrackMessage(header, nfGenMsg, status, timeoutSec);
+        return new ConntrackMessage(header, nfGenMsg, tupleOrig, tupleReply, status, timeoutSec);
+    }
+
+    /**
+     * Parses a conntrack tuple from a {@link ByteBuffer}.
+     *
+     * The attribute parsing is interesting on:
+     * - CTA_TUPLE_IP
+     *     CTA_IP_V4_SRC
+     *     CTA_IP_V4_DST
+     * - CTA_TUPLE_PROTO
+     *     CTA_PROTO_NUM
+     *     CTA_PROTO_SRC_PORT
+     *     CTA_PROTO_DST_PORT
+     *
+     * Assume that the minimum size is the sum of CTA_TUPLE_IP (size: 20) and CTA_TUPLE_PROTO
+     * (size: 28). Here is an example for an expected CTA_TUPLE_ORIG message in raw data:
+     * +--------------------------------------------------------------------------------------+
+     * | CTA_TUPLE_ORIG                                                                       |
+     * +--------------------------+-----------------------------------------------------------+
+     * | 1400                     | nla_len = 20                                              |
+     * | 0180                     | nla_type = nested CTA_TUPLE_IP                            |
+     * |     0800 0100 C0A8500C   |     nla_type=CTA_IP_V4_SRC, ip=192.168.80.12              |
+     * |     0800 0200 8C700874   |     nla_type=CTA_IP_V4_DST, ip=140.112.8.116              |
+     * | 1C00                     | nla_len = 28                                              |
+     * | 0280                     | nla_type = nested CTA_TUPLE_PROTO                         |
+     * |     0500 0100 06 000000  |     nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)         |
+     * |     0600 0200 F3F1 0000  |     nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)  |
+     * |     0600 0300 01BB 0000  |     nla_type=CTA_PROTO_DST_PORT, port=433 (big endian)    |
+     * +--------------------------+-----------------------------------------------------------+
+     *
+     * The position of the byte buffer doesn't set to the end when the function returns. It is okay
+     * because the caller ConntrackMessage#parse has passed a copy which is used for this parser
+     * only. Moreover, the parser behavior is the same as other existing netlink struct class
+     * parser. Ex: StructInetDiagMsg#parse.
+     */
+    @Nullable
+    private static Tuple parseTuple(@Nullable ByteBuffer byteBuffer) {
+        if (byteBuffer == null) return null;
+
+        TupleIpv4 tupleIpv4 = null;
+        TupleProto tupleProto = null;
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_IP), byteBuffer);
+        if (nlAttr != null) {
+            tupleIpv4 = parseTupleIpv4(nlAttr.getValueAsByteBuffer());
+        }
+        if (tupleIpv4 == null) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(makeNestedType(CTA_TUPLE_PROTO), byteBuffer);
+        if (nlAttr != null) {
+            tupleProto = parseTupleProto(nlAttr.getValueAsByteBuffer());
+        }
+        if (tupleProto == null) return null;
+
+        return new Tuple(tupleIpv4, tupleProto);
+    }
+
+    @Nullable
+    private static Inet4Address castToInet4Address(@Nullable InetAddress address) {
+        if (address == null || !(address instanceof Inet4Address)) return null;
+        return (Inet4Address) address;
+    }
+
+    @Nullable
+    private static TupleIpv4 parseTupleIpv4(@Nullable ByteBuffer byteBuffer) {
+        if (byteBuffer == null) return null;
+
+        Inet4Address src = null;
+        Inet4Address dst = null;
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(CTA_IP_V4_SRC, byteBuffer);
+        if (nlAttr != null) {
+            src = castToInet4Address(nlAttr.getValueAsInetAddress());
+        }
+        if (src == null) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(CTA_IP_V4_DST, byteBuffer);
+        if (nlAttr != null) {
+            dst = castToInet4Address(nlAttr.getValueAsInetAddress());
+        }
+        if (dst == null) return null;
+
+        return new TupleIpv4(src, dst);
+    }
+
+    @Nullable
+    private static TupleProto parseTupleProto(@Nullable ByteBuffer byteBuffer) {
+        if (byteBuffer == null) return null;
+
+        byte protoNum = 0;
+        short srcPort = 0;
+        short dstPort = 0;
+
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(CTA_PROTO_NUM, byteBuffer);
+        if (nlAttr != null) {
+            protoNum = nlAttr.getValueAsByte((byte) 0);
+        }
+        if (!(protoNum == IPPROTO_TCP || protoNum == IPPROTO_UDP)) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(CTA_PROTO_SRC_PORT, byteBuffer);
+        if (nlAttr != null) {
+            srcPort = nlAttr.getValueAsBe16((short) 0);
+        }
+        if (srcPort == 0) return null;
+
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(CTA_PROTO_DST_PORT, byteBuffer);
+        if (nlAttr != null) {
+            dstPort = nlAttr.getValueAsBe16((short) 0);
+        }
+        if (dstPort == 0) return null;
+
+        return new TupleProto(protoNum, srcPort, dstPort);
     }
 
     /**
@@ -144,6 +386,22 @@
      */
     public final StructNfGenMsg nfGenMsg;
     /**
+     * Original direction conntrack tuple.
+     *
+     * The tuple is determined by the parsed attribute value CTA_TUPLE_ORIG, or null if the
+     * tuple could not be parsed successfully (for example, if it was truncated or absent).
+     */
+    @Nullable
+    public final Tuple tupleOrig;
+    /**
+     * Reply direction conntrack tuple.
+     *
+     * The tuple is determined by the parsed attribute value CTA_TUPLE_REPLY, or null if the
+     * tuple could not be parsed successfully (for example, if it was truncated or absent).
+     */
+    @Nullable
+    public final Tuple tupleReply;
+    /**
      * Connection status. A bitmask of ip_conntrack_status enum flags.
      *
      * The status is determined by the parsed attribute value CTA_STATUS, or 0 if the status could
@@ -165,14 +423,21 @@
     private ConntrackMessage() {
         super(new StructNlMsgHdr());
         nfGenMsg = new StructNfGenMsg((byte) OsConstants.AF_INET);
+
+        // This constructor is only used by #newIPv4TimeoutUpdateRequest which doesn't use these
+        // data member for packing message. Simply fill them to null or 0.
+        tupleOrig = null;
+        tupleReply = null;
         status = 0;
         timeoutSec = 0;
     }
 
     private ConntrackMessage(@NonNull StructNlMsgHdr header, @NonNull StructNfGenMsg nfGenMsg,
-            int status, int timeoutSec) {
+            @Nullable Tuple tupleOrig, @Nullable Tuple tupleReply, int status, int timeoutSec) {
         super(header);
         this.nfGenMsg = nfGenMsg;
+        this.tupleOrig = tupleOrig;
+        this.tupleReply = tupleReply;
         this.status = status;
         this.timeoutSec = timeoutSec;
     }
@@ -181,4 +446,8 @@
         mHeader.pack(byteBuffer);
         nfGenMsg.pack(byteBuffer);
     }
+
+    public short getMessageType() {
+        return (short) (getHeader().nlmsg_type & ~(NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8));
+    }
 }
diff --git a/common/netlinkclient/src/android/net/netlink/StructNlAttr.java b/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
index a3a26b9..b6e1d3f 100644
--- a/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
+++ b/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
@@ -183,6 +183,23 @@
         return NetlinkConstants.alignedLengthOf(nla_len);
     }
 
+    /**
+     * Get attribute value as BE16.
+     */
+    public short getValueAsBe16(short defaultValue) {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Short.BYTES) {
+            return defaultValue;
+        }
+        final ByteOrder originalOrder = byteBuffer.order();
+        try {
+            byteBuffer.order(ByteOrder.BIG_ENDIAN);
+            return byteBuffer.getShort();
+        } finally {
+            byteBuffer.order(originalOrder);
+        }
+    }
+
     public int getValueAsBe32(int defaultValue) {
         final ByteBuffer byteBuffer = getValueAsByteBuffer();
         if (byteBuffer == null || byteBuffer.remaining() != Integer.BYTES) {
@@ -207,6 +224,17 @@
         return byteBuffer;
     }
 
+    /**
+     * Get attribute value as byte.
+     */
+    public byte getValueAsByte(byte defaultValue) {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Byte.BYTES) {
+            return defaultValue;
+        }
+        return getValueAsByteBuffer().get();
+    }
+
     public int getValueAsInt(int defaultValue) {
         final ByteBuffer byteBuffer = getValueAsByteBuffer();
         if (byteBuffer == null || byteBuffer.remaining() != Integer.BYTES) {
diff --git a/tests/unit/src/android/net/ip/ConntrackMonitorTest.java b/tests/unit/src/android/net/ip/ConntrackMonitorTest.java
index 578ee08..406ecff 100644
--- a/tests/unit/src/android/net/ip/ConntrackMonitorTest.java
+++ b/tests/unit/src/android/net/ip/ConntrackMonitorTest.java
@@ -15,14 +15,25 @@
  */
 package android.net.ip;
 
+import static android.net.ip.ConntrackMonitor.ConntrackEvent;
+import static android.net.netlink.ConntrackMessage.Tuple;
+import static android.net.netlink.ConntrackMessage.TupleIpv4;
+import static android.net.netlink.ConntrackMessage.TupleProto;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
 import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.SOCK_DGRAM;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 
+import android.net.InetAddresses;
+import android.net.netlink.NetlinkConstants;
 import android.net.netlink.NetlinkSocket;
 import android.net.util.SharedLog;
 import android.os.ConditionVariable;
@@ -46,6 +57,7 @@
 
 import java.io.FileDescriptor;
 import java.io.InterruptedIOException;
+import java.net.Inet4Address;
 
 
 /**
@@ -128,26 +140,182 @@
         mHandlerThread.quitSafely();
     }
 
-    // TODO: Add conntrack message attributes to have further verification.
-    public static final String CT_V4NEW_HEX =
+    public static final String CT_V4NEW_TCP_HEX =
             // CHECKSTYLE:OFF IndentationCheck
             // struct nlmsghdr
-            "14000000" +      // length = 20
+            "8C000000" +      // length = 140
             "0001" +          // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
-            "0006" +          // flags = NLM_F_CREATE | NLM_F_EXCL
+            "0006" +          // flags = NLM_F_CREATE (1 << 10) | NLM_F_EXCL (1 << 9)
             "00000000" +      // seqno = 0
             "00000000" +      // pid = 0
             // struct nfgenmsg
             "02" +            // nfgen_family = AF_INET
             "00" +            // version = NFNETLINK_V0
-            "0000";           // res_id
+            "1234" +          // res_id = 0x1234 (big endian)
+             // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 C0A8500C" +  // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+                    "0800 0200 8C700874" +  // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 F3F1 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0280" +          // nla_type = nested CTA_TUPLE_REPLY
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 8C700874" +  // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+                    "0800 0200 6451B301" +  // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 01BB 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=443 (big endian)
+                    "0600 0300 F3F1 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0300" +          // nla_type = CTA_STATUS
+            "0000019e" +      // nla_value = 0b110011110 (big endian)
+                              // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+                              // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+                              // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0700" +          // nla_type = CTA_TIMEOUT
+            "00000078";       // nla_value = 120 (big endian)
             // CHECKSTYLE:ON IndentationCheck
-    public static final byte[] CT_V4NEW_BYTES =
-            HexEncoding.decode(CT_V4NEW_HEX.replaceAll(" ", "").toCharArray(), false);
+    public static final byte[] CT_V4NEW_TCP_BYTES =
+            HexEncoding.decode(CT_V4NEW_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @NonNull
+    private ConntrackEvent makeTestConntrackEvent(short msgType, int status, int timeoutSec) {
+        final Inet4Address privateIp =
+                (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
+        final Inet4Address remoteIp =
+                (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
+        final Inet4Address publicIp =
+                (Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
+
+        return new ConntrackEvent(
+                (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
+                new Tuple(new TupleIpv4(privateIp, remoteIp),
+                        new TupleProto((byte) IPPROTO_TCP, (short) 62449, (short) 443)),
+                new Tuple(new TupleIpv4(remoteIp, publicIp),
+                        new TupleProto((byte) IPPROTO_TCP, (short) 443, (short) 62449)),
+                status,
+                timeoutSec);
+    }
 
     @Test
-    public void testConntrackEvent_New() throws Exception {
-        mConntrackMonitor.sendMessage(CT_V4NEW_BYTES);
-        verify(mConsumer, timeout(TIMEOUT_MS)).accept(any() /* TODO: check the content */);
+    public void testConntrackEventNew() throws Exception {
+        final ConntrackEvent expectedEvent = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+                0x19e /* status */, 120 /* timeoutSec */);
+        mConntrackMonitor.sendMessage(CT_V4NEW_TCP_BYTES);
+        verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
     }
+
+    @Test
+    public void testConntrackEventEquals() {
+        final ConntrackEvent event1 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+                5678 /* timeoutSec*/);
+        final ConntrackEvent event2 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+                5678 /* timeoutSec*/);
+        assertEquals(event1, event2);
+    }
+
+    @Test
+    public void testConntrackEventNotEquals() {
+        final ConntrackEvent e = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+                5678 /* timeoutSec*/);
+
+        final ConntrackEvent typeNotEqual = new ConntrackEvent((short) (e.msgType + 1) /* diff */,
+                e.tupleOrig, e.tupleReply, e.status, e.timeoutSec);
+        assertNotEquals(e, typeNotEqual);
+
+        final ConntrackEvent tupleOrigNotEqual = new ConntrackEvent(e.msgType,
+                null /* diff */, e.tupleReply, e.status, e.timeoutSec);
+        assertNotEquals(e, tupleOrigNotEqual);
+
+        final ConntrackEvent tupleReplyNotEqual = new ConntrackEvent(e.msgType,
+                e.tupleOrig, null /* diff */, e.status, e.timeoutSec);
+        assertNotEquals(e, tupleReplyNotEqual);
+
+        final ConntrackEvent statusNotEqual = new ConntrackEvent(e.msgType,
+                e.tupleOrig, e.tupleReply, e.status + 1 /* diff */, e.timeoutSec);
+        assertNotEquals(e, statusNotEqual);
+
+        final ConntrackEvent timeoutSecNotEqual = new ConntrackEvent(e.msgType,
+                e.tupleOrig, e.tupleReply, e.status, e.timeoutSec + 1 /* diff */);
+        assertNotEquals(e, timeoutSecNotEqual);
+    }
+
+    public static final String CT_V4DELETE_TCP_HEX =
+            // CHECKSTYLE:OFF IndentationCheck
+            // struct nlmsghdr
+            "84000000" +      // length = 132
+            "0201" +          // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_DELETE (2)
+            "0000" +          // flags = 0
+            "00000000" +      // seqno = 0
+            "00000000" +      // pid = 0
+            // struct nfgenmsg
+            "02" +            // nfgen_family  = AF_INET
+            "00" +            // version = NFNETLINK_V0
+            "1234" +          // res_id = 0x1234 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 C0A8500C" +  // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+                    "0800 0200 8C700874" +  // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 F3F1 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=433 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0280" +          // nla_type = nested CTA_TUPLE_REPLY
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 8C700874" +  // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+                    "0800 0200 6451B301" +  // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 01BB 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=433 (big endian)
+                    "0600 0300 F3F1 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+            // struct nlattr
+            "0800" +          // nla_len = 8
+            "0300" +          // nla_type = CTA_STATUS
+            "0000039E";       // nla_value = 0b1110011110 (big endian)
+                              // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+                              // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+                              // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8) |
+                              // IPS_DYING (1 << 9)
+            // CHECKSTYLE:ON IndentationCheck
+    public static final byte[] CT_V4DELETE_TCP_BYTES =
+            HexEncoding.decode(CT_V4DELETE_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @Test
+    public void testConntrackEventDelete() throws Exception {
+        final ConntrackEvent expectedEvent =
+                makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, 0x39e /* status */,
+                        0 /* timeoutSec (absent) */);
+        mConntrackMonitor.sendMessage(CT_V4DELETE_TCP_BYTES);
+        verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+    }
+
 }
diff --git a/tests/unit/src/android/net/netlink/ConntrackMessageTest.java b/tests/unit/src/android/net/netlink/ConntrackMessageTest.java
index 6af1046..575d53e 100644
--- a/tests/unit/src/android/net/netlink/ConntrackMessageTest.java
+++ b/tests/unit/src/android/net/netlink/ConntrackMessageTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -39,6 +40,7 @@
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Arrays;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -166,7 +168,16 @@
         assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
         assertEquals((short) 0, nfmsgHdr.res_id);
 
-        // TODO: Parse the CTA_TUPLE_ORIG.
+        assertEquals(InetAddress.parseNumericAddress("192.168.43.209"),
+                conntrackMessage.tupleOrig.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("23.211.13.26"),
+                conntrackMessage.tupleOrig.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_TCP, conntrackMessage.tupleOrig.protoNum);
+        assertEquals((short) 44333, conntrackMessage.tupleOrig.srcPort);
+        assertEquals((short) 443, conntrackMessage.tupleOrig.dstPort);
+
+        assertNull(conntrackMessage.tupleReply);
+
         assertEquals(0 /* absent */, conntrackMessage.status);
         assertEquals(432000, conntrackMessage.timeoutSec);
     }
@@ -206,24 +217,60 @@
         assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
         assertEquals((short) 0, nfmsgHdr.res_id);
 
-        // TODO: Parse the CTA_TUPLE_ORIG.
+        assertEquals(InetAddress.parseNumericAddress("100.96.167.146"),
+                conntrackMessage.tupleOrig.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("216.58.197.10"),
+                conntrackMessage.tupleOrig.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_UDP, conntrackMessage.tupleOrig.protoNum);
+        assertEquals((short) 37069, conntrackMessage.tupleOrig.srcPort);
+        assertEquals((short) 443, conntrackMessage.tupleOrig.dstPort);
+
+        assertNull(conntrackMessage.tupleReply);
+
         assertEquals(0 /* absent */, conntrackMessage.status);
         assertEquals(180, conntrackMessage.timeoutSec);
     }
 
-    // TODO: Add conntrack message attributes to have further verification.
-    public static final String CT_V4NEW_HEX =
+    public static final String CT_V4NEW_TCP_HEX =
             // CHECKSTYLE:OFF IndentationCheck
             // struct nlmsghdr
-            "24000000" +      // length = 36
+            "8C000000" +      // length = 140
             "0001" +          // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
-            "0006" +          // flags = NLM_F_CREATE | NLM_F_EXCL
+            "0006" +          // flags = NLM_F_CREATE (1 << 10) | NLM_F_EXCL (1 << 9)
             "00000000" +      // seqno = 0
             "00000000" +      // pid = 0
             // struct nfgenmsg
             "02" +            // nfgen_family = AF_INET
             "00" +            // version = NFNETLINK_V0
             "1234" +          // res_id = 0x1234 (big endian)
+             // struct nlattr
+            "3400" +          // nla_len = 52
+            "0180" +          // nla_type = nested CTA_TUPLE_ORIG
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 C0A8500C" +  // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+                    "0800 0200 8C700874" +  // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 F3F1 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+                    "0600 0300 01BB 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+            // struct nlattr
+            "3400" +          // nla_len = 52
+            "0280" +          // nla_type = nested CTA_TUPLE_REPLY
+                // struct nlattr
+                "1400" +      // nla_len = 20
+                "0180" +      // nla_type = nested CTA_TUPLE_IP
+                    "0800 0100 8C700874" +  // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+                    "0800 0200 6451B301" +  // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+                // struct nlattr
+                "1C00" +      // nla_len = 28
+                "0280" +      // nla_type = nested CTA_TUPLE_PROTO
+                    "0500 0100 06 000000" +  // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+                    "0600 0200 01BB 0000" +  // nla_type=CTA_PROTO_SRC_PORT, port=443 (big endian)
+                    "0600 0300 F3F1 0000" +  // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
             // struct nlattr
             "0800" +          // nla_len = 8
             "0300" +          // nla_type = CTA_STATUS
@@ -235,14 +282,14 @@
             "0700" +          // nla_type = CTA_TIMEOUT
             "00000078";       // nla_value = 120 (big endian)
             // CHECKSTYLE:ON IndentationCheck
-    public static final byte[] CT_V4NEW_BYTES =
-            HexEncoding.decode(CT_V4NEW_HEX.replaceAll(" ", "").toCharArray(), false);
+    public static final byte[] CT_V4NEW_TCP_BYTES =
+            HexEncoding.decode(CT_V4NEW_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
 
     @Test
     public void testParseCtNew() {
         assumeTrue(USING_LE);
 
-        final ByteBuffer byteBuffer = ByteBuffer.wrap(CT_V4NEW_BYTES);
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(CT_V4NEW_TCP_BYTES);
         byteBuffer.order(ByteOrder.nativeOrder());
         final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
         assertNotNull(msg);
@@ -251,7 +298,7 @@
 
         final StructNlMsgHdr hdr = conntrackMessage.getHeader();
         assertNotNull(hdr);
-        assertEquals(36, hdr.nlmsg_len);
+        assertEquals(140, hdr.nlmsg_len);
         assertEquals(makeCtType(IPCTNL_MSG_CT_NEW), hdr.nlmsg_type);
         assertEquals((short) (StructNlMsgHdr.NLM_F_CREATE | StructNlMsgHdr.NLM_F_EXCL),
                 hdr.nlmsg_flags);
@@ -264,7 +311,95 @@
         assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
         assertEquals((short) 0x1234, nfmsgHdr.res_id);
 
+        assertEquals(InetAddress.parseNumericAddress("192.168.80.12"),
+                conntrackMessage.tupleOrig.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("140.112.8.116"),
+                conntrackMessage.tupleOrig.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_TCP, conntrackMessage.tupleOrig.protoNum);
+        assertEquals((short) 62449, conntrackMessage.tupleOrig.srcPort);
+        assertEquals((short) 443, conntrackMessage.tupleOrig.dstPort);
+
+        assertEquals(InetAddress.parseNumericAddress("140.112.8.116"),
+                conntrackMessage.tupleReply.srcIp);
+        assertEquals(InetAddress.parseNumericAddress("100.81.179.1"),
+                conntrackMessage.tupleReply.dstIp);
+        assertEquals((byte) OsConstants.IPPROTO_TCP, conntrackMessage.tupleReply.protoNum);
+        assertEquals((short) 443, conntrackMessage.tupleReply.srcPort);
+        assertEquals((short) 62449, conntrackMessage.tupleReply.dstPort);
+
         assertEquals(0x198, conntrackMessage.status);
         assertEquals(120, conntrackMessage.timeoutSec);
     }
+
+    @Test
+    public void testParseTruncation() {
+        assumeTrue(USING_LE);
+
+        // Expect no crash while parsing the truncated message which has been truncated to every
+        // length between 0 and its full length - 1.
+        for (int len = 0; len < CT_V4NEW_TCP_BYTES.length; len++) {
+            final byte[] truncated = Arrays.copyOfRange(CT_V4NEW_TCP_BYTES, 0, len);
+
+            final ByteBuffer byteBuffer = ByteBuffer.wrap(truncated);
+            byteBuffer.order(ByteOrder.nativeOrder());
+            final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer,
+                    OsConstants.NETLINK_NETFILTER);
+        }
+    }
+
+    @Test
+    public void testParseTruncationWithInvalidByte() {
+        assumeTrue(USING_LE);
+
+        // Expect no crash while parsing the message which is truncated by invalid bytes. The
+        // message has been truncated to every length between 0 and its full length - 1.
+        for (byte invalid : new byte[]{(byte) 0x00, (byte) 0xff}) {
+            for (int len = 0; len < CT_V4NEW_TCP_BYTES.length; len++) {
+                final byte[] truncated = new byte[CT_V4NEW_TCP_BYTES.length];
+                Arrays.fill(truncated, (byte) invalid);
+                System.arraycopy(CT_V4NEW_TCP_BYTES, 0, truncated, 0, len);
+
+                final ByteBuffer byteBuffer = ByteBuffer.wrap(truncated);
+                byteBuffer.order(ByteOrder.nativeOrder());
+                final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer,
+                        OsConstants.NETLINK_NETFILTER);
+            }
+        }
+    }
+
+    // Malformed conntrack messages.
+    public static final String CT_MALFORMED_HEX =
+            // CHECKSTYLE:OFF IndentationCheck
+            // <--           nlmsghr           -->|<-nfgenmsg->|<--    CTA_TUPLE_ORIG     -->|
+            // CTA_TUPLE_ORIG has no nla_value.
+            "18000000 0001 0006 00000000 00000000   02 00 0000 0400 0180"
+            // nested CTA_TUPLE_IP has no nla_value.
+            + "1C000000 0001 0006 00000000 00000000 02 00 0000 0800 0180 0400 0180"
+            // nested CTA_IP_V4_SRC has no nla_value.
+            + "20000000 0001 0006 00000000 00000000 02 00 0000 0C00 0180 0800 0180 0400 0100"
+            // nested CTA_TUPLE_PROTO has no nla_value.
+            // <--           nlmsghr           -->|<-nfgenmsg->|<--    CTA_TUPLE_ORIG
+            + "30000000 0001 0006 00000000 00000000 02 00 0000 1C00 0180 1400 0180 0800 0100"
+            //                                  -->|
+            + "C0A8500C 0800 0200 8C700874 0400 0280";
+            // CHECKSTYLE:ON IndentationCheck
+    public static final byte[] CT_MALFORMED_BYTES =
+            HexEncoding.decode(CT_MALFORMED_HEX.replaceAll(" ", "").toCharArray(), false);
+
+    @Test
+    public void testParseMalformation() {
+        assumeTrue(USING_LE);
+
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(CT_MALFORMED_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        // Expect no crash while parsing the malformed message.
+        int messageCount = 0;
+        while (byteBuffer.remaining() > 0) {
+            final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer,
+                    OsConstants.NETLINK_NETFILTER);
+            messageCount++;
+        }
+        assertEquals(4, messageCount);
+    }
 }
diff --git a/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt b/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
index c2bdcd4..9fb4d8c 100644
--- a/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
+++ b/tests/unit/src/com/android/net/module/util/TrackRecordTest.kt
@@ -30,6 +30,7 @@
 import org.junit.runners.JUnit4
 import java.util.concurrent.CyclicBarrier
 import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
 import kotlin.system.measureTimeMillis
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -183,6 +184,33 @@
     }
 
     @Test
+    fun testConcurrentPollDisallowed() {
+        val failures = AtomicInteger(0)
+        val readHead = ArrayTrackRecord<Int>().newReadHead()
+        val barrier = CyclicBarrier(2)
+        Thread {
+            barrier.await(LONG_TIMEOUT, TimeUnit.MILLISECONDS) // barrier 1
+            try {
+                readHead.poll(LONG_TIMEOUT)
+            } catch (e: ConcurrentModificationException) {
+                failures.incrementAndGet()
+                // Unblock the other thread
+                readHead.add(0)
+            }
+        }.start()
+        barrier.await() // barrier 1
+        try {
+            readHead.poll(LONG_TIMEOUT)
+        } catch (e: ConcurrentModificationException) {
+            failures.incrementAndGet()
+            // Unblock the other thread
+            readHead.add(0)
+        }
+        // One of the threads must have gotten an exception.
+        assertEquals(failures.get(), 1)
+    }
+
+    @Test
     fun testPollWakesUp() {
         val record = ArrayTrackRecord<Int>()
         val barrier = CyclicBarrier(2)