Merge "Cleanup testOverrideVendorId static member after running unittest."
diff --git a/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
index 681ba1a..42216a9 100644
--- a/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
+++ b/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.networkstack.apishim.api29;
 
+import android.net.CaptivePortalData;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
@@ -48,8 +50,25 @@
         throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29");
     }
 
+    @Override
+    public String getVenueFriendlyName() {
+        // Not supported in API level 29
+        return null;
+    }
+
     @VisibleForTesting
     public static boolean isSupported() {
         return false;
     }
+
+    /**
+     * Generate a {@link CaptivePortalData} object with a friendly name set
+     *
+     * @param friendlyName The friendly name to set
+     * @return a {@link CaptivePortalData} object with a friendly name set
+     */
+    public CaptivePortalData withVenueFriendlyName(String friendlyName) {
+        // Not supported in API level 29
+        return null;
+    }
 }
diff --git a/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java
index 1fa4907..8dc7b5c 100644
--- a/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java
+++ b/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java
@@ -16,6 +16,7 @@
 
 package com.android.networkstack.apishim.api29;
 
+import android.net.CaptivePortalData;
 import android.net.IpPrefix;
 import android.net.LinkProperties;
 import android.net.NetworkCapabilities;
@@ -57,6 +58,14 @@
         return false;
     }
 
+    /**
+     * Indicates whether the shim can use APIs above the R SDK.
+     */
+    @VisibleForTesting
+    public static boolean useApiAboveR() {
+        return false;
+    }
+
     @Nullable
     @Override
     public Uri getCaptivePortalApiUrl(@Nullable LinkProperties lp) {
@@ -105,4 +114,17 @@
             @NonNull Inet4Address serverAddress) {
         // Not supported on this API level: no-op
     }
+
+    /**
+     * Set captive portal data in {@link LinkProperties}
+     * @param lp Link properties object to be updated
+     * @param captivePortalData Captive portal data to be used
+     */
+    public void setCaptivePortalData(@NonNull LinkProperties lp,
+            @Nullable CaptivePortalData captivePortalData) {
+        if (lp == null) {
+            return;
+        }
+        lp.setCaptivePortalData(captivePortalData);
+    }
 }
diff --git a/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java
index 5639386..19a41db 100644
--- a/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java
+++ b/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java
@@ -37,7 +37,7 @@
 public class CaptivePortalDataShimImpl
         extends com.android.networkstack.apishim.api29.CaptivePortalDataShimImpl {
     @NonNull
-    private final CaptivePortalData mData;
+    protected final CaptivePortalData mData;
 
     protected CaptivePortalDataShimImpl(@NonNull CaptivePortalData data) {
         mData = data;
diff --git a/apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java b/apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
index 6ff6adb..955167d 100644
--- a/apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
+++ b/apishim/31/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
@@ -20,13 +20,31 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.networkstack.apishim.common.CaptivePortalDataShim;
+
 /**
  * Compatibility implementation of {@link CaptivePortalDataShim}.
  */
 public class CaptivePortalDataShimImpl
         extends com.android.networkstack.apishim.api30.CaptivePortalDataShimImpl {
-    // Currently, this is the same as the API 30 shim, so inherit everything from that.
     protected CaptivePortalDataShimImpl(@NonNull CaptivePortalData data) {
         super(data);
     }
+
+    @Override
+    public String getVenueFriendlyName() {
+        return mData.getVenueFriendlyName();
+    }
+
+    /**
+     * Generate a {@link CaptivePortalData} object with a friendly name set
+     *
+     * @param friendlyName The friendly name to set
+     * @return a {@link CaptivePortalData} object with a friendly name set
+     */
+    public CaptivePortalData withVenueFriendlyName(String friendlyName) {
+        return new CaptivePortalData.Builder(mData)
+                .setVenueFriendlyName(friendlyName)
+                .build();
+    }
 }
diff --git a/apishim/31/com/android/networkstack/apishim/NetworkInformationShimImpl.java b/apishim/31/com/android/networkstack/apishim/NetworkInformationShimImpl.java
index 828063c..d668d7e 100644
--- a/apishim/31/com/android/networkstack/apishim/NetworkInformationShimImpl.java
+++ b/apishim/31/com/android/networkstack/apishim/NetworkInformationShimImpl.java
@@ -16,11 +16,45 @@
 
 package com.android.networkstack.apishim;
 
+import android.net.LinkProperties;
+import android.os.Build;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.networkstack.apishim.common.CaptivePortalDataShim;
+import com.android.networkstack.apishim.common.NetworkInformationShim;
+import com.android.networkstack.apishim.common.ShimUtils;
+
 /**
  * Compatibility implementation of {@link NetworkInformationShim}.
  */
 public class NetworkInformationShimImpl
         extends com.android.networkstack.apishim.api30.NetworkInformationShimImpl {
-    // Currently, this is the same as the API 30 shim, so inherit everything from that.
     protected NetworkInformationShimImpl() {}
+
+    /**
+     * Indicates whether the shim can use APIs above the R SDK.
+     */
+    @VisibleForTesting
+    public static boolean useApiAboveR() {
+        return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.R);
+    }
+
+    /**
+     * Get a new instance of {@link NetworkInformationShim}.
+     */
+    public static NetworkInformationShim newInstance() {
+        if (!useApiAboveR()) {
+            return com.android.networkstack.apishim.api30.NetworkInformationShimImpl.newInstance();
+        }
+        return new com.android.networkstack.apishim.NetworkInformationShimImpl();
+    }
+
+    @Nullable
+    @Override
+    public CaptivePortalDataShim getCaptivePortalData(@Nullable LinkProperties lp) {
+        if (lp == null || lp.getCaptivePortalData() == null) return null;
+        return new CaptivePortalDataShimImpl(lp.getCaptivePortalData());
+    }
 }
diff --git a/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java b/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java
index a18ba49..4bd5532 100644
--- a/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java
+++ b/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java
@@ -16,6 +16,8 @@
 
 package com.android.networkstack.apishim.common;
 
+import android.annotation.NonNull;
+import android.net.CaptivePortalData;
 import android.net.INetworkMonitorCallbacks;
 import android.net.Uri;
 import android.os.RemoteException;
@@ -50,7 +52,20 @@
     Uri getVenueInfoUrl();
 
     /**
+     * @see CaptivePortalData#getVenueFriendlyName()
+     */
+    String getVenueFriendlyName();
+
+    /**
      * @see INetworkMonitorCallbacks#notifyCaptivePortalDataChanged(android.net.CaptivePortalData)
      */
     void notifyChanged(INetworkMonitorCallbacks cb) throws RemoteException;
+
+    /**
+     * Generate a {@link CaptivePortalData} object with a friendly name set
+     *
+     * @param friendlyName The friendly name to set
+     * @return a {@link CaptivePortalData} object with a friendly name set
+     */
+    CaptivePortalData withVenueFriendlyName(@NonNull String friendlyName);
 }
diff --git a/apishim/common/com/android/networkstack/apishim/common/NetworkInformationShim.java b/apishim/common/com/android/networkstack/apishim/common/NetworkInformationShim.java
index ee19759..6cdcf8c 100644
--- a/apishim/common/com/android/networkstack/apishim/common/NetworkInformationShim.java
+++ b/apishim/common/com/android/networkstack/apishim/common/NetworkInformationShim.java
@@ -16,6 +16,7 @@
 
 package com.android.networkstack.apishim.common;
 
+import android.net.CaptivePortalData;
 import android.net.IpPrefix;
 import android.net.LinkProperties;
 import android.net.NetworkCapabilities;
@@ -76,4 +77,11 @@
      */
     void setDhcpServerAddress(@NonNull LinkProperties lp, @NonNull Inet4Address serverAddress);
 
+    /**
+     * Set captive portal data in {@link LinkProperties}
+     * @param lp Link properties object to be updated
+     * @param captivePortalData Captive portal data to be used
+     */
+    void setCaptivePortalData(@NonNull LinkProperties lp,
+            @Nullable CaptivePortalData captivePortalData);
 }
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 4326f7a..7f34547 100644
--- a/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
+++ b/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
@@ -16,17 +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;
 
 
 /**
@@ -42,6 +52,7 @@
     // enum ctattr_type
     public static final short CTA_TUPLE_ORIG  = 1;
     public static final short CTA_TUPLE_REPLY = 2;
+    public static final short CTA_STATUS      = 3;
     public static final short CTA_TIMEOUT     = 7;
 
     // enum ctattr_tuple
@@ -57,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 ***
@@ -105,35 +216,238 @@
         // Just build the netlink header and netfilter header for now and pretend the whole message
         // was consumed.
         // TODO: Parse the conntrack attributes.
-        final ConntrackMessage conntrackMsg = new ConntrackMessage(header);
-
-        conntrackMsg.mNfGenMsg = StructNfGenMsg.parse(byteBuffer);
-        if (conntrackMsg.mNfGenMsg == null) {
+        final StructNfGenMsg nfGenMsg = StructNfGenMsg.parse(byteBuffer);
+        if (nfGenMsg == null) {
             return null;
         }
 
-        byteBuffer.position(byteBuffer.limit());
-        return conntrackMsg;
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(CTA_STATUS, byteBuffer);
+        int status = 0;
+        if (nlAttr != null) {
+            status = nlAttr.getValueAsBe32(0);
+        }
+
+        byteBuffer.position(baseOffset);
+        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;
+        final int kAdditionalSpace = NetlinkConstants.alignedLengthOf(
+                header.nlmsg_len - kMinConsumed);
+        if (byteBuffer.remaining() < kAdditionalSpace) {
+            return null;
+        }
+        byteBuffer.position(baseOffset + kAdditionalSpace);
+
+        return new ConntrackMessage(header, nfGenMsg, tupleOrig, tupleReply, status, timeoutSec);
     }
 
-    protected StructNfGenMsg mNfGenMsg;
+    /**
+     * 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);
+    }
+
+    /**
+     * Netfilter header.
+     */
+    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
+     * not be parsed successfully (for example, if it was truncated or absent). For the message
+     * from kernel, the valid status is non-zero. For the message from user space, the status may
+     * be 0 (absent).
+     */
+    public final int status;
+    /**
+     * Conntrack timeout.
+     *
+     * The timeout is determined by the parsed attribute value CTA_TIMEOUT, or 0 if the timeout
+     * could not be parsed successfully (for example, if it was truncated or absent). For
+     * IPCTNL_MSG_CT_NEW event, the valid timeout is non-zero. For IPCTNL_MSG_CT_DELETE event, the
+     * timeout is 0 (absent).
+     */
+    public final int timeoutSec;
 
     private ConntrackMessage() {
         super(new StructNlMsgHdr());
-        mNfGenMsg = new StructNfGenMsg((byte) OsConstants.AF_INET);
+        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(StructNlMsgHdr header) {
+    private ConntrackMessage(@NonNull StructNlMsgHdr header, @NonNull StructNfGenMsg nfGenMsg,
+            @Nullable Tuple tupleOrig, @Nullable Tuple tupleReply, int status, int timeoutSec) {
         super(header);
-        mNfGenMsg = null;
-    }
-
-    public StructNfGenMsg getNfHeader() {
-        return mNfGenMsg;
+        this.nfGenMsg = nfGenMsg;
+        this.tupleOrig = tupleOrig;
+        this.tupleReply = tupleReply;
+        this.status = status;
+        this.timeoutSec = timeoutSec;
     }
 
     public void pack(ByteBuffer byteBuffer) {
         mHeader.pack(byteBuffer);
-        mNfGenMsg.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/RtNetlinkNeighborMessage.java b/common/netlinkclient/src/android/net/netlink/RtNetlinkNeighborMessage.java
index 6ae22c0..099ff07 100644
--- a/common/netlinkclient/src/android/net/netlink/RtNetlinkNeighborMessage.java
+++ b/common/netlinkclient/src/android/net/netlink/RtNetlinkNeighborMessage.java
@@ -48,23 +48,6 @@
     public static final short NDA_IFINDEX   = 8;
     public static final short NDA_MASTER    = 9;
 
-    private static StructNlAttr findNextAttrOfType(short attrType, ByteBuffer byteBuffer) {
-        while (byteBuffer != null && byteBuffer.remaining() > 0) {
-            final StructNlAttr nlAttr = StructNlAttr.peek(byteBuffer);
-            if (nlAttr == null) {
-                break;
-            }
-            if (nlAttr.nla_type == attrType) {
-                return StructNlAttr.parse(byteBuffer);
-            }
-            if (byteBuffer.remaining() < nlAttr.getAlignedLength()) {
-                break;
-            }
-            byteBuffer.position(byteBuffer.position() + nlAttr.getAlignedLength());
-        }
-        return null;
-    }
-
     public static RtNetlinkNeighborMessage parse(StructNlMsgHdr header, ByteBuffer byteBuffer) {
         final RtNetlinkNeighborMessage neighMsg = new RtNetlinkNeighborMessage(header);
 
@@ -75,25 +58,25 @@
 
         // Some of these are message-type dependent, and not always present.
         final int baseOffset = byteBuffer.position();
-        StructNlAttr nlAttr = findNextAttrOfType(NDA_DST, byteBuffer);
+        StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(NDA_DST, byteBuffer);
         if (nlAttr != null) {
             neighMsg.mDestination = nlAttr.getValueAsInetAddress();
         }
 
         byteBuffer.position(baseOffset);
-        nlAttr = findNextAttrOfType(NDA_LLADDR, byteBuffer);
+        nlAttr = StructNlAttr.findNextAttrOfType(NDA_LLADDR, byteBuffer);
         if (nlAttr != null) {
             neighMsg.mLinkLayerAddr = nlAttr.nla_value;
         }
 
         byteBuffer.position(baseOffset);
-        nlAttr = findNextAttrOfType(NDA_PROBES, byteBuffer);
+        nlAttr = StructNlAttr.findNextAttrOfType(NDA_PROBES, byteBuffer);
         if (nlAttr != null) {
             neighMsg.mNumProbes = nlAttr.getValueAsInt(0);
         }
 
         byteBuffer.position(baseOffset);
-        nlAttr = findNextAttrOfType(NDA_CACHEINFO, byteBuffer);
+        nlAttr = StructNlAttr.findNextAttrOfType(NDA_CACHEINFO, byteBuffer);
         if (nlAttr != null) {
             neighMsg.mCacheInfo = StructNdaCacheInfo.parse(nlAttr.getValueAsByteBuffer());
         }
diff --git a/common/netlinkclient/src/android/net/netlink/StructNfGenMsg.java b/common/netlinkclient/src/android/net/netlink/StructNfGenMsg.java
index e0f4333..fca78b8 100644
--- a/common/netlinkclient/src/android/net/netlink/StructNfGenMsg.java
+++ b/common/netlinkclient/src/android/net/netlink/StructNfGenMsg.java
@@ -79,8 +79,11 @@
     public void pack(ByteBuffer byteBuffer) {
         byteBuffer.put(nfgen_family);
         byteBuffer.put(version);
-        // TODO: probably need to handle the little endian case.
+
+        final ByteOrder originalOrder = byteBuffer.order();
+        byteBuffer.order(ByteOrder.BIG_ENDIAN);
         byteBuffer.putShort(res_id);
+        byteBuffer.order(originalOrder);
     }
 
     private static boolean hasAvailableSpace(@NonNull ByteBuffer byteBuffer) {
diff --git a/common/netlinkclient/src/android/net/netlink/StructNlAttr.java b/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
index 747998d..b6e1d3f 100644
--- a/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
+++ b/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
@@ -16,10 +16,12 @@
 
 package android.net.netlink;
 
+import androidx.annotation.Nullable;
+
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.nio.ByteOrder;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 
 /**
@@ -48,9 +50,7 @@
         }
         final int baseOffset = byteBuffer.position();
 
-        // Assume the byte order of the buffer is the expected byte order of the value.
-        final StructNlAttr struct = new StructNlAttr(byteBuffer.order());
-        // The byte order of nla_len and nla_type is always native.
+        final StructNlAttr struct = new StructNlAttr();
         final ByteOrder originalOrder = byteBuffer.order();
         byteBuffer.order(ByteOrder.nativeOrder());
         try {
@@ -87,20 +87,39 @@
         return struct;
     }
 
+    /**
+     * Find next netlink attribute with a given type from {@link ByteBuffer}.
+     *
+     * @param attrType The given netlink attribute type is requested for.
+     * @param byteBuffer The buffer from which to find the netlink attribute.
+     * @return the found netlink attribute, or {@code null} if the netlink attribute could not be
+     *         found or parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructNlAttr findNextAttrOfType(short attrType,
+            @Nullable ByteBuffer byteBuffer) {
+        while (byteBuffer != null && byteBuffer.remaining() > 0) {
+            final StructNlAttr nlAttr = StructNlAttr.peek(byteBuffer);
+            if (nlAttr == null) {
+                break;
+            }
+            if (nlAttr.nla_type == attrType) {
+                return StructNlAttr.parse(byteBuffer);
+            }
+            if (byteBuffer.remaining() < nlAttr.getAlignedLength()) {
+                break;
+            }
+            byteBuffer.position(byteBuffer.position() + nlAttr.getAlignedLength());
+        }
+        return null;
+    }
+
     public short nla_len = (short) NLA_HEADERLEN;
     public short nla_type;
     public byte[] nla_value;
 
-    // The byte order used to read/write the value member. Netlink length and
-    // type members are always read/written in native order.
-    private ByteOrder mByteOrder = ByteOrder.nativeOrder();
-
     public StructNlAttr() {}
 
-    public StructNlAttr(ByteOrder byteOrder) {
-        mByteOrder = byteOrder;
-    }
-
     public StructNlAttr(short type, byte value) {
         nla_type = type;
         setValue(new byte[1]);
@@ -112,10 +131,16 @@
     }
 
     public StructNlAttr(short type, short value, ByteOrder order) {
-        this(order);
         nla_type = type;
         setValue(new byte[Short.BYTES]);
-        getValueAsByteBuffer().putShort(value);
+        final ByteBuffer buf = getValueAsByteBuffer();
+        final ByteOrder originalOrder = buf.order();
+        try {
+            buf.order(order);
+            buf.putShort(value);
+        } finally {
+            buf.order(originalOrder);
+        }
     }
 
     public StructNlAttr(short type, int value) {
@@ -123,10 +148,16 @@
     }
 
     public StructNlAttr(short type, int value, ByteOrder order) {
-        this(order);
         nla_type = type;
         setValue(new byte[Integer.BYTES]);
-        getValueAsByteBuffer().putInt(value);
+        final ByteBuffer buf = getValueAsByteBuffer();
+        final ByteOrder originalOrder = buf.order();
+        try {
+            buf.order(order);
+            buf.putInt(value);
+        } finally {
+            buf.order(originalOrder);
+        }
     }
 
     public StructNlAttr(short type, InetAddress ip) {
@@ -152,13 +183,58 @@
         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) {
+            return defaultValue;
+        }
+        final ByteOrder originalOrder = byteBuffer.order();
+        try {
+            byteBuffer.order(ByteOrder.BIG_ENDIAN);
+            return byteBuffer.getInt();
+        } finally {
+            byteBuffer.order(originalOrder);
+        }
+    }
+
     public ByteBuffer getValueAsByteBuffer() {
         if (nla_value == null) { return null; }
         final ByteBuffer byteBuffer = ByteBuffer.wrap(nla_value);
-        byteBuffer.order(mByteOrder);
+        // By convention, all buffers in this library are in native byte order because netlink is in
+        // native byte order. It's the order that is used by NetlinkSocket.recvMessage and the only
+        // order accepted by NetlinkMessage.parse.
+        byteBuffer.order(ByteOrder.nativeOrder());
         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/src/com/android/networkstack/NetworkStackNotifier.java b/src/com/android/networkstack/NetworkStackNotifier.java
index dbb62b1..0558d3a 100644
--- a/src/com/android/networkstack/NetworkStackNotifier.java
+++ b/src/com/android/networkstack/NetworkStackNotifier.java
@@ -198,7 +198,6 @@
         // Don't show the notification when SSID is unknown to prevent sending something vague to
         // the user.
         final boolean hasSsid = !TextUtils.isEmpty(getSsid(networkStatus));
-
         final CaptivePortalDataShim capportData = getCaptivePortalData(networkStatus);
         final boolean showVenueInfo = capportData != null && capportData.getVenueInfoUrl() != null
                 // Only show venue info on validated networks, to prevent misuse of the notification
@@ -235,7 +234,14 @@
             // channel even if the notification contains venue info: the "venue info" notification
             // then doubles as a "connected" notification.
             final String channel = showValidated ? CHANNEL_CONNECTED : CHANNEL_VENUE_INFO;
-            builder = getNotificationBuilder(channel, networkStatus, res, getSsid(networkStatus))
+
+            // If the venue friendly name is available (in Passpoint use-case), display it.
+            // Otherwise, display the SSID.
+            final String friendlyName = capportData.getVenueFriendlyName();
+            final String venueDisplayName = TextUtils.isEmpty(friendlyName)
+                    ? getSsid(networkStatus) : friendlyName;
+
+            builder = getNotificationBuilder(channel, networkStatus, res, venueDisplayName)
                     .setContentText(res.getString(R.string.tap_for_info))
                     .setContentIntent(mDependencies.getActivityPendingIntent(
                             getContextAsUser(mContext, UserHandle.CURRENT),
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 18a0c52..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
@@ -82,6 +84,14 @@
     public static final byte[] CT_V4UPDATE_TCP_BYTES =
             HexEncoding.decode(CT_V4UPDATE_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
 
+    private byte[] makeIPv4TimeoutUpdateRequestTcp() throws Exception {
+        return ConntrackMessage.newIPv4TimeoutUpdateRequest(
+                OsConstants.IPPROTO_TCP,
+                (Inet4Address) InetAddress.getByName("192.168.43.209"), 44333,
+                (Inet4Address) InetAddress.getByName("23.211.13.26"), 443,
+                432000);
+    }
+
     // Example 2: UDP (100.96.167.146, 37069) -> (216.58.197.10, 443)
     public static final String CT_V4UPDATE_UDP_HEX =
             // struct nlmsghdr
@@ -115,17 +125,27 @@
     public static final byte[] CT_V4UPDATE_UDP_BYTES =
             HexEncoding.decode(CT_V4UPDATE_UDP_HEX.replaceAll(" ", "").toCharArray(), false);
 
+    private byte[] makeIPv4TimeoutUpdateRequestUdp() throws Exception {
+        return ConntrackMessage.newIPv4TimeoutUpdateRequest(
+                OsConstants.IPPROTO_UDP,
+                (Inet4Address) InetAddress.getByName("100.96.167.146"), 37069,
+                (Inet4Address) InetAddress.getByName("216.58.197.10"), 443,
+                180);
+    }
+
     @Test
-    public void testConntrackIPv4TcpTimeoutUpdate() throws Exception {
+    public void testConntrackMakeIPv4TcpTimeoutUpdate() throws Exception {
         assumeTrue(USING_LE);
 
-        final byte[] tcp = ConntrackMessage.newIPv4TimeoutUpdateRequest(
-                OsConstants.IPPROTO_TCP,
-                (Inet4Address) InetAddress.getByName("192.168.43.209"), 44333,
-                (Inet4Address) InetAddress.getByName("23.211.13.26"), 443,
-                432000);
+        final byte[] tcp = makeIPv4TimeoutUpdateRequestTcp();
         assertArrayEquals(CT_V4UPDATE_TCP_BYTES, tcp);
+    }
 
+    @Test
+    public void testConntrackParseIPv4TcpTimeoutUpdate() throws Exception {
+        assumeTrue(USING_LE);
+
+        final byte[] tcp = makeIPv4TimeoutUpdateRequestTcp();
         final ByteBuffer byteBuffer = ByteBuffer.wrap(tcp);
         byteBuffer.order(ByteOrder.nativeOrder());
         final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
@@ -142,26 +162,39 @@
         assertEquals(1, hdr.nlmsg_seq);
         assertEquals(0, hdr.nlmsg_pid);
 
-        final StructNfGenMsg nfmsgHdr = conntrackMessage.getNfHeader();
+        final StructNfGenMsg nfmsgHdr = conntrackMessage.nfGenMsg;
         assertNotNull(nfmsgHdr);
         assertEquals((byte) OsConstants.AF_INET, nfmsgHdr.nfgen_family);
         assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
         assertEquals((short) 0, nfmsgHdr.res_id);
 
-        // TODO: Parse the CTA_TUPLE_ORIG and CTA_TIMEOUT.
+        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);
     }
 
     @Test
-    public void testConntrackIPv4UdpTimeoutUpdate() throws Exception {
+    public void testConntrackMakeIPv4UdpTimeoutUpdate() throws Exception {
         assumeTrue(USING_LE);
 
-        final byte[] udp = ConntrackMessage.newIPv4TimeoutUpdateRequest(
-                OsConstants.IPPROTO_UDP,
-                (Inet4Address) InetAddress.getByName("100.96.167.146"), 37069,
-                (Inet4Address) InetAddress.getByName("216.58.197.10"), 443,
-                180);
+        final byte[] udp = makeIPv4TimeoutUpdateRequestUdp();
         assertArrayEquals(CT_V4UPDATE_UDP_BYTES, udp);
+    }
 
+    @Test
+    public void testConntrackParseIPv4UdpTimeoutUpdate() throws Exception {
+        assumeTrue(USING_LE);
+
+        final byte[] udp = makeIPv4TimeoutUpdateRequestUdp();
         final ByteBuffer byteBuffer = ByteBuffer.wrap(udp);
         byteBuffer.order(ByteOrder.nativeOrder());
         final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, OsConstants.NETLINK_NETFILTER);
@@ -178,37 +211,85 @@
         assertEquals(1, hdr.nlmsg_seq);
         assertEquals(0, hdr.nlmsg_pid);
 
-        final StructNfGenMsg nfmsgHdr = conntrackMessage.getNfHeader();
+        final StructNfGenMsg nfmsgHdr = conntrackMessage.nfGenMsg;
         assertNotNull(nfmsgHdr);
         assertEquals((byte) OsConstants.AF_INET, nfmsgHdr.nfgen_family);
         assertEquals((byte) StructNfGenMsg.NFNETLINK_V0, nfmsgHdr.version);
         assertEquals((short) 0, nfmsgHdr.res_id);
 
-        // TODO: Parse the CTA_TUPLE_ORIG and CTA_TIMEOUT.
+        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
-            "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
-            "1234";           // res_id = 0x1234 (big endian)
+            "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
+            "00000198" +      // nla_value = 0b110011000 (big endian)
+                              // 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);
 
     @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);
@@ -217,17 +298,108 @@
 
         final StructNlMsgHdr hdr = conntrackMessage.getHeader();
         assertNotNull(hdr);
-        assertEquals(20, 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);
         assertEquals(0, hdr.nlmsg_seq);
         assertEquals(0, hdr.nlmsg_pid);
 
-        final StructNfGenMsg nfmsgHdr = conntrackMessage.getNfHeader();
+        final StructNfGenMsg nfmsgHdr = conntrackMessage.nfGenMsg;
         assertNotNull(nfmsgHdr);
         assertEquals((byte) OsConstants.AF_INET, nfmsgHdr.nfgen_family);
         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/android/net/netlink/RtNetlinkNeighborMessageTest.java b/tests/unit/src/android/net/netlink/RtNetlinkNeighborMessageTest.java
index c3f9068..a1f1d44 100644
--- a/tests/unit/src/android/net/netlink/RtNetlinkNeighborMessageTest.java
+++ b/tests/unit/src/android/net/netlink/RtNetlinkNeighborMessageTest.java
@@ -151,6 +151,13 @@
             assertEquals(0, hdr.nlmsg_seq);
             assertEquals(11070, hdr.nlmsg_pid);
 
+            final int probes = neighMsg.getProbes();
+            assertTrue("Unexpected number of probes. Got " +  probes + ", max=5",
+                    probes < 5);
+            final int ndm_refcnt = neighMsg.getCacheInfo().ndm_refcnt;
+            assertTrue("nda_cacheinfo has unexpectedly high ndm_refcnt: " + ndm_refcnt,
+                    ndm_refcnt < 0x100);
+
             messageCount++;
         }
         // TODO: add more detailed spot checks.
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)
diff --git a/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt b/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt
index 348392d..3b44597 100644
--- a/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt
+++ b/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt
@@ -112,6 +112,22 @@
         }
     }
 
+    private val mTestCapportVenueUrlWithFriendlyNameLp by lazy {
+        LinkProperties().apply {
+            captivePortalData = CaptivePortalData.Builder()
+                    .setCaptive(false)
+                    .setVenueInfoUrl(Uri.parse(TEST_VENUE_INFO_URL))
+                    .build()
+            val networkShim = NetworkInformationShimImpl.newInstance()
+            val captivePortalDataShim = networkShim.getCaptivePortalData(this)
+
+            if (captivePortalDataShim != null) {
+                networkShim.setCaptivePortalData(this, captivePortalDataShim
+                        .withVenueFriendlyName(TEST_NETWORK_FRIENDLY_NAME))
+            }
+        }
+    }
+
     private val TEST_NETWORK = Network(42)
     private val TEST_NETWORK_TAG = TEST_NETWORK.networkHandle.toString()
     private val TEST_SSID = "TestSsid"
@@ -125,6 +141,7 @@
 
     private val TEST_VENUE_INFO_URL = "https://testvenue.example.com/info"
     private val EMPTY_CAPPORT_LP = LinkProperties()
+    private val TEST_NETWORK_FRIENDLY_NAME = "Network Friendly Name"
 
     @Before
     fun setUp() {
@@ -333,6 +350,28 @@
         verify(mNm, never()).notify(eq(TEST_NETWORK_TAG), anyInt(), any())
     }
 
+    @Test
+    fun testConnectedVenueInfoWithFriendlyNameNotification() {
+        // Venue info (CaptivePortalData) with friendly name is not available for API <= R
+        assumeTrue(NetworkInformationShimImpl.useApiAboveR())
+        mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK)
+        onLinkPropertiesChanged(mTestCapportVenueUrlWithFriendlyNameLp)
+        onDefaultNetworkAvailable(TEST_NETWORK)
+        val capabilities = NetworkCapabilities(VALIDATED_CAPABILITIES).setSSID(TEST_SSID)
+        onCapabilitiesChanged(capabilities)
+
+        mLooper.processAllMessages()
+
+        verifyConnectedNotification(timeout = 0)
+        verifyVenueInfoIntent(mIntentCaptor.value)
+        verify(mResources).getString(R.string.tap_for_info)
+        verify(mNm).notify(eq(TEST_NETWORK_TAG), mNoteIdCaptor.capture(), mNoteCaptor.capture())
+        val note = mNoteCaptor.value
+        assertEquals(TEST_NETWORK_FRIENDLY_NAME, note.extras
+                .getCharSequence(Notification.EXTRA_TITLE))
+        verifyCanceledNotificationAfterDefaultNetworkLost()
+    }
+
     private fun verifyVenueInfoIntent(intent: Intent) {
         assertEquals(Intent.ACTION_VIEW, intent.action)
         assertEquals(Uri.parse(TEST_VENUE_INFO_URL), intent.data)
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index 4d032c2..8f42a61 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -2285,9 +2285,10 @@
         setStatus(mFallbackConnection, 204);
         nm.forceReevaluation(Process.myUid());
         // Expect to send HTTP, HTTPs, FALLBACK and evaluation results.
-        runNetworkTest(VALIDATION_RESULT_INVALID,
+        verifyNetworkTested(VALIDATION_RESULT_INVALID,
                 NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK,
                 null /* redirectUrl */);
+        HandlerUtils.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS);
     }
 
     @Test