[NFCT.NS.8] Parse conntrack attribute CTA_STATUS and CTA_TIMEOUT

Parse CTA_STATUS and CTA_TIMEOUT for the IPv4 BPF tethering offload.
More attributes need to be parsed in the follow-up commits.

Test: atest TetheringCoverageTest
Change-Id: Ic2080d33d0517ef81ce92e189506fb1bb2425003
diff --git a/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java b/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
index 4326f7a..30a1165 100644
--- a/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
+++ b/common/netlinkclient/src/android/net/netlink/ConntrackMessage.java
@@ -22,6 +22,7 @@
 
 import static java.nio.ByteOrder.BIG_ENDIAN;
 
+import android.annotation.NonNull;
 import android.system.OsConstants;
 
 import java.net.Inet4Address;
@@ -42,6 +43,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
@@ -105,35 +107,78 @@
         // 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 = StructNlAttr.findNextAttrOfType(CTA_STATUS, byteBuffer);
+        int status = 0;
+        if (nlAttr != null) {
+            status = nlAttr.getValueAsBe32(0);
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(CTA_TIMEOUT, byteBuffer);
+        int timeoutSec = 0;
+        if (nlAttr != null) {
+            timeoutSec = nlAttr.getValueAsBe32(0);
+        }
+
+        // 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, status, timeoutSec);
     }
 
-    protected StructNfGenMsg mNfGenMsg;
+    /**
+     * Netfilter header.
+     */
+    public final StructNfGenMsg nfGenMsg;
+    /**
+     * 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);
+        status = 0;
+        timeoutSec = 0;
     }
 
-    private ConntrackMessage(StructNlMsgHdr header) {
+    private ConntrackMessage(@NonNull StructNlMsgHdr header, @NonNull StructNfGenMsg nfGenMsg,
+            int status, int timeoutSec) {
         super(header);
-        mNfGenMsg = null;
-    }
-
-    public StructNfGenMsg getNfHeader() {
-        return mNfGenMsg;
+        this.nfGenMsg = nfGenMsg;
+        this.status = status;
+        this.timeoutSec = timeoutSec;
     }
 
     public void pack(ByteBuffer byteBuffer) {
         mHeader.pack(byteBuffer);
-        mNfGenMsg.pack(byteBuffer);
+        nfGenMsg.pack(byteBuffer);
     }
 }
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 59458c1..a3a26b9 100644
--- a/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
+++ b/common/netlinkclient/src/android/net/netlink/StructNlAttr.java
@@ -16,6 +16,8 @@
 
 package android.net.netlink;
 
+import androidx.annotation.Nullable;
+
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
@@ -85,6 +87,33 @@
         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;
diff --git a/tests/unit/src/android/net/netlink/ConntrackMessageTest.java b/tests/unit/src/android/net/netlink/ConntrackMessageTest.java
index 18a0c52..6af1046 100644
--- a/tests/unit/src/android/net/netlink/ConntrackMessageTest.java
+++ b/tests/unit/src/android/net/netlink/ConntrackMessageTest.java
@@ -82,6 +82,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 +123,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 +160,30 @@
         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.
+        // TODO: Parse the CTA_TUPLE_ORIG.
+        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,20 +200,22 @@
         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.
+        // TODO: Parse the CTA_TUPLE_ORIG.
+        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 =
             // CHECKSTYLE:OFF IndentationCheck
             // struct nlmsghdr
-            "14000000" +      // length = 20
+            "24000000" +      // length = 36
             "0001" +          // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
             "0006" +          // flags = NLM_F_CREATE | NLM_F_EXCL
             "00000000" +      // seqno = 0
@@ -199,7 +223,17 @@
             // 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
+            "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);
@@ -217,17 +251,20 @@
 
         final StructNlMsgHdr hdr = conntrackMessage.getHeader();
         assertNotNull(hdr);
-        assertEquals(20, hdr.nlmsg_len);
+        assertEquals(36, 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(0x198, conntrackMessage.status);
+        assertEquals(120, conntrackMessage.timeoutSec);
     }
 }