Add data structures to parse netlink link messages.

Bug: 163492391
Test: atest NetworkStaticLibsTests
Change-Id: Ida8c22a782eeaca738487d0dc041a459ef6bb936
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index cf9d2c5..11aee84 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -70,6 +70,7 @@
         if (family == OsConstants.AF_INET) return "AF_INET";
         if (family == OsConstants.AF_INET6) return "AF_INET6";
         if (family == OsConstants.AF_NETLINK) return "AF_NETLINK";
+        if (family == OsConstants.AF_UNSPEC) return "AF_UNSPEC";
         return String.valueOf(family);
     }
 
@@ -142,9 +143,13 @@
     public static final short SOCK_DIAG_BY_FAMILY = 20;
 
     // Netlink groups.
+    public static final int RTMGRP_LINK = 1;
     public static final int RTNLGRP_ND_USEROPT = 20;
     public static final int RTMGRP_ND_USEROPT = 1 << (RTNLGRP_ND_USEROPT - 1);
 
+    // Device flags.
+    public static final int IFF_LOWER_UP = 1 << 16;
+
     /**
      * Convert a netlink message type to a string for control message.
      */
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index 723d682..d83c12f 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -120,6 +120,9 @@
     private static NetlinkMessage parseRtMessage(@NonNull StructNlMsgHdr nlmsghdr,
             @NonNull ByteBuffer byteBuffer) {
         switch (nlmsghdr.nlmsg_type) {
+            case NetlinkConstants.RTM_NEWLINK:
+            case NetlinkConstants.RTM_DELLINK:
+                return (NetlinkMessage) RtNetlinkLinkMessage.parse(nlmsghdr, byteBuffer);
             case NetlinkConstants.RTM_NEWNEIGH:
             case NetlinkConstants.RTM_DELNEIGH:
             case NetlinkConstants.RTM_GETNEIGH:
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
new file mode 100644
index 0000000..92ec0c4
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import android.net.MacAddress;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.nio.ByteBuffer;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink link messages.
+ *
+ * RtNetlinkLinkMessage.parse() must be called with a ByteBuffer that contains exactly one netlink
+ * message.
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class RtNetlinkLinkMessage extends NetlinkMessage {
+    public static final short IFLA_ADDRESS   = 1;
+    public static final short IFLA_IFNAME    = 3;
+    public static final short IFLA_MTU       = 4;
+
+    private int mMtu;
+    @NonNull
+    private StructIfinfoMsg mIfinfomsg;
+    @Nullable
+    private MacAddress mHardwareAddress;
+    @Nullable
+    private String mInterfaceName;
+
+    private RtNetlinkLinkMessage(@NonNull StructNlMsgHdr header) {
+        super(header);
+        mIfinfomsg = null;
+        mMtu = 0;
+        mHardwareAddress = null;
+        mInterfaceName = null;
+    }
+
+    public int getMtu() {
+        return mMtu;
+    }
+
+    @NonNull
+    public StructIfinfoMsg getIfinfoHeader() {
+        return mIfinfomsg;
+    }
+
+    @Nullable
+    public MacAddress getHardwareAddress() {
+        return mHardwareAddress;
+    }
+
+    @Nullable
+    public String getInterfaceName() {
+        return mInterfaceName;
+    }
+
+    /**
+     * Parse rtnetlink link message from {@link ByteBuffer}. This method must be called with a
+     * ByteBuffer that contains exactly one netlink message.
+     *
+     * @param header netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+     */
+    @Nullable
+    public static RtNetlinkLinkMessage parse(@NonNull final StructNlMsgHdr header,
+            @NonNull final ByteBuffer byteBuffer) {
+        final RtNetlinkLinkMessage linkMsg = new RtNetlinkLinkMessage(header);
+
+        linkMsg.mIfinfomsg = StructIfinfoMsg.parse(byteBuffer);
+        if (linkMsg.mIfinfomsg == null) return null;
+
+        // IFLA_MTU
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(IFLA_MTU, byteBuffer);
+        if (nlAttr != null) {
+            linkMsg.mMtu = nlAttr.getValueAsInt(0 /* default value */);
+        }
+
+        // IFLA_ADDRESS
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(IFLA_ADDRESS, byteBuffer);
+        if (nlAttr != null) {
+            linkMsg.mHardwareAddress = nlAttr.getValueAsMacAddress();
+        }
+
+        // IFLA_IFNAME
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(IFLA_IFNAME, byteBuffer);
+        if (nlAttr != null) {
+            linkMsg.mInterfaceName = nlAttr.getValueAsString();
+        }
+
+        return linkMsg;
+    }
+
+    /**
+     * Write a rtnetlink link message to {@link ByteBuffer}.
+     */
+    @VisibleForTesting
+    protected void pack(ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer);
+        mIfinfomsg.pack(byteBuffer);
+
+        if (mMtu != 0) {
+            final StructNlAttr mtu = new StructNlAttr(IFLA_MTU, mMtu);
+            mtu.pack(byteBuffer);
+        }
+        if (mHardwareAddress != null) {
+            final StructNlAttr hardwareAddress = new StructNlAttr(IFLA_ADDRESS, mHardwareAddress);
+            hardwareAddress.pack(byteBuffer);
+        }
+        if (mInterfaceName != null) {
+            final StructNlAttr ifname = new StructNlAttr(IFLA_IFNAME, mInterfaceName);
+            ifname.pack(byteBuffer);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "RtNetlinkLinkMessage{ "
+                + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+                + "Ifinfomsg{" + mIfinfomsg.toString() + "}, "
+                + "Hardware Address{" + mHardwareAddress + "}, "
+                + "MTU{" + mMtu + "}, "
+                + "Ifname{" + mInterfaceName + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructIfinfoMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructIfinfoMsg.java
new file mode 100644
index 0000000..881dc18
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructIfinfoMsg.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct ifinfomsg
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class StructIfinfoMsg extends Struct {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 16;
+
+    @Field(order = 0, type = Type.U8, padding = 1)
+    public final short family;
+    @Field(order = 1, type = Type.U16)
+    public final int type;
+    @Field(order = 2, type = Type.S32)
+    public final int index;
+    @Field(order = 3, type = Type.U32)
+    public final long flags;
+    @Field(order = 4, type = Type.U32)
+    public final long change;
+
+    StructIfinfoMsg(short family, int type, int index, long flags, long change) {
+        this.family = family;
+        this.type = type;
+        this.index = index;
+        this.flags = flags;
+        this.change = change;
+    }
+
+    /**
+     * Parse a rtnetlink link message header from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the rtnetlink link message header.
+     * @return the parsed rtnetlink link message header, or {@code null} if the rtnetlink message
+     *         header could not be parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructIfinfoMsg parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) return null;
+
+        // The ByteOrder must already have been set to native order.
+        return StructIfinfoMsg.parse(StructIfinfoMsg.class, byteBuffer);
+    }
+
+    /**
+     * Write ifinfomsg to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        this.writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
index 80f0057..485e67c 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
@@ -16,12 +16,17 @@
 
 package com.android.net.module.util.netlink;
 
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Arrays;
 
 /**
  * struct nlattr
@@ -173,11 +178,35 @@
         }
     }
 
-    public StructNlAttr(short type, InetAddress ip) {
+    public StructNlAttr(short type, @NonNull final byte[] value) {
+        nla_type = type;
+        setValue(value);
+    }
+
+    public StructNlAttr(short type, @NonNull final InetAddress ip) {
         nla_type = type;
         setValue(ip.getAddress());
     }
 
+    public StructNlAttr(short type, @NonNull final MacAddress mac) {
+        nla_type = type;
+        setValue(mac.toByteArray());
+    }
+
+    public StructNlAttr(short type, @NonNull final String string) {
+        nla_type = type;
+        byte[] value = null;
+        try {
+            final byte[] stringBytes = string.getBytes("UTF-8");
+            // Append '\0' at the end of interface name string bytes.
+            value = Arrays.copyOf(stringBytes, stringBytes.length + 1);
+        } catch (UnsupportedEncodingException ignored) {
+            // Do nothing.
+        } finally {
+            setValue(value);
+        }
+    }
+
     public StructNlAttr(short type, StructNlAttr... nested) {
         this();
         nla_type = makeNestedType(type);
@@ -270,7 +299,11 @@
 
     /**
      * Get attribute value as InetAddress.
+     *
+     * @return the InetAddress instance representation of attribute value or null if IP address
+     *         is of illegal length.
      */
+    @Nullable
     public InetAddress getValueAsInetAddress() {
         if (nla_value == null) return null;
 
@@ -282,6 +315,43 @@
     }
 
     /**
+     * Get attribute value as MacAddress.
+     *
+     * @return the MacAddress instance representation of attribute value or null if the given byte
+     *         array is not a valid representation(e.g, not all link layers have 6-byte link-layer
+     *         addresses)
+     */
+    @Nullable
+    public MacAddress getValueAsMacAddress() {
+        if (nla_value == null) return null;
+
+        try {
+            return MacAddress.fromBytes(nla_value);
+        } catch (IllegalArgumentException ignored) {
+            return null;
+        }
+    }
+
+    /**
+     * Get attribute value as a unicode string.
+     *
+     * @return a unicode string or null if UTF-8 charset is not supported.
+     */
+    @Nullable
+    public String getValueAsString() {
+        if (nla_value == null) return null;
+        // Check the attribute value length after removing string termination flag '\0'.
+        if (nla_value.length < (nla_len - NLA_HEADERLEN - 1)) return null;
+
+        try {
+            final byte[] array = Arrays.copyOf(nla_value, nla_len - NLA_HEADERLEN - 1);
+            return new String(array, "UTF-8");
+        } catch (UnsupportedEncodingException | NegativeArraySizeException ignored) {
+            return null;
+        }
+    }
+
+    /**
      * Write the netlink attribute to {@link ByteBuffer}.
      */
     public void pack(ByteBuffer byteBuffer) {
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
index 9567cce..5052cb8 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
@@ -166,7 +166,7 @@
         return "StructNlMsgHdr{ "
                 + "nlmsg_len{" + nlmsg_len + "}, "
                 + "nlmsg_type{" + typeStr + "}, "
-                + "nlmsg_flags{" + flagsStr + ")}, "
+                + "nlmsg_flags{" + flagsStr + "}, "
                 + "nlmsg_seq{" + nlmsg_seq + "}, "
                 + "nlmsg_pid{" + nlmsg_pid + "} "
                 + "}";
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/ConntrackMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/ConntrackMessageTest.java
index cea763c..f02b4cb 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/ConntrackMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/ConntrackMessageTest.java
@@ -422,7 +422,7 @@
         final String expected = ""
                 + "ConntrackMessage{"
                 + "nlmsghdr{StructNlMsgHdr{ nlmsg_len{140}, nlmsg_type{256(IPCTNL_MSG_CT_NEW)}, "
-                + "nlmsg_flags{1536(NLM_F_MATCH))}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "nlmsg_flags{1536(NLM_F_MATCH)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
                 + "nfgenmsg{NfGenMsg{ nfgen_family{AF_INET}, version{0}, res_id{4660} }}, "
                 + "tuple_orig{Tuple{IPPROTO_TCP: 192.168.80.12:62449 -> 140.112.8.116:443}}, "
                 + "tuple_reply{Tuple{IPPROTO_TCP: 140.112.8.116:443 -> 100.81.179.1:62449}}, "
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
new file mode 100644
index 0000000..58a7478
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
+
+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 android.net.MacAddress;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkLinkMessageTest {
+    private static final String TAG = "RtNetlinkLinkMessageTest";
+
+    // An example of the full RTM_NEWLINK message.
+    private static final String RTM_NEWLINK_HEX =
+            "64000000100000000000000000000000"   // struct nlmsghr
+            + "000001001E0000000210000000000000" // struct ifinfo
+            + "0A000300776C616E30000000"         // IFLA_IFNAME
+            + "08000D00B80B0000"                 // IFLA_PROTINFO
+            + "0500100002000000"                 // IFLA_OPERSTATE
+            + "0500110001000000"                 // IFLA_LINKMODE
+            + "08000400DC050000"                 // IFLA_MTU
+            + "0A00010092C3E3C9374E0000"         // IFLA_ADDRESS
+            + "0A000200FFFFFFFFFFFF0000";        // IFLA_BROADCAST
+
+    private ByteBuffer toByteBuffer(final String hexString) {
+        return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+    }
+
+    @Test
+    public void testParseRtmNewLink() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkLinkMessage);
+        final RtNetlinkLinkMessage linkMsg = (RtNetlinkLinkMessage) msg;
+
+        final StructNlMsgHdr hdr = linkMsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(100, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWLINK, hdr.nlmsg_type);
+        assertEquals(0, hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructIfinfoMsg ifinfomsgHdr = linkMsg.getIfinfoHeader();
+        assertNotNull(ifinfomsgHdr);
+        assertEquals((byte) OsConstants.AF_UNSPEC, ifinfomsgHdr.family);
+        assertEquals(OsConstants.ARPHRD_ETHER, ifinfomsgHdr.type);
+        assertEquals(30, ifinfomsgHdr.index);
+        assertEquals(0, ifinfomsgHdr.change);
+
+        assertEquals(ETHER_MTU, linkMsg.getMtu());
+        assertEquals(MacAddress.fromString("92:C3:E3:C9:37:4E"), linkMsg.getHardwareAddress());
+        assertTrue(linkMsg.getInterfaceName().equals("wlan0"));
+    }
+
+    private static final String RTM_NEWLINK_PACK_HEX =
+            "34000000100000000000000000000000"   // struct nlmsghr
+            + "000001001E0000000210000000000000" // struct ifinfo
+            + "08000400DC050000"                 // IFLA_MTU
+            + "0A00010092C3E3C9374E0000"         // IFLA_ADDRESS
+            + "0A000300776C616E30000000";        // IFLA_IFNAME
+
+    @Test
+    public void testPackRtmNewLink() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_PACK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkLinkMessage);
+        final RtNetlinkLinkMessage linkMsg = (RtNetlinkLinkMessage) msg;
+
+        final ByteBuffer packBuffer = ByteBuffer.allocate(64);
+        packBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        linkMsg.pack(packBuffer);
+        assertEquals(RTM_NEWLINK_PACK_HEX, HexDump.toHexString(packBuffer.array()));
+    }
+
+    private static final String RTM_NEWLINK_TRUNCATED_HEX =
+            "54000000100000000000000000000000"   // struct nlmsghr
+            + "000001001E0000000210000000000000" // struct ifinfo
+            + "08000D00B80B0000"                 // IFLA_PROTINFO
+            + "0500100002000000"                 // IFLA_OPERSTATE
+            + "0800010092C3E3C9"                 // IFLA_ADDRESS(truncated)
+            + "0500110001000000"                 // IFLA_LINKMODE
+            + "0A000300776C616E30000000"         // IFLA_IFNAME
+            + "08000400DC050000";                // IFLA_MTU
+
+    @Test
+    public void testTruncatedRtmNewLink() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_TRUNCATED_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkLinkMessage);
+        final RtNetlinkLinkMessage linkMsg = (RtNetlinkLinkMessage) msg;
+
+        // Truncated IFLA_ADDRESS attribute doesn't affect parsing other attrs.
+        assertNull(linkMsg.getHardwareAddress());
+        assertEquals(ETHER_MTU, linkMsg.getMtu());
+        assertTrue(linkMsg.getInterfaceName().equals("wlan0"));
+    }
+
+    @Test
+    public void testToString() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkLinkMessage);
+        final RtNetlinkLinkMessage linkMsg = (RtNetlinkLinkMessage) msg;
+        final String expected = "RtNetlinkLinkMessage{ "
+                + "nlmsghdr{"
+                + "StructNlMsgHdr{ nlmsg_len{100}, nlmsg_type{16(RTM_NEWLINK)}, nlmsg_flags{0()}, "
+                + "nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "Ifinfomsg{"
+                + "family: 0, type: 1, index: 30, flags: 4098, change: 0}, "
+                + "Hardware Address{92:c3:e3:c9:37:4e}, " + "MTU{1500}, "
+                + "Ifname{wlan0} "
+                + "}";
+        assertEquals(expected, linkMsg.toString());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
new file mode 100644
index 0000000..72e179b
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static com.android.net.module.util.netlink.RtNetlinkLinkMessage.IFLA_ADDRESS;
+import static com.android.net.module.util.netlink.RtNetlinkLinkMessage.IFLA_IFNAME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.net.MacAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructNlAttrTest {
+    private static final MacAddress TEST_MAC_ADDRESS = MacAddress.fromString("00:11:22:33:44:55");
+    private static final String TEST_INTERFACE_NAME = "wlan0";
+
+    @Test
+    public void testGetValueAsMacAddress() {
+        final StructNlAttr attr1 = new StructNlAttr(IFLA_ADDRESS, TEST_MAC_ADDRESS);
+        final MacAddress address1 = attr1.getValueAsMacAddress();
+        assertEquals(address1, TEST_MAC_ADDRESS);
+
+        // Invalid mac address byte array.
+        final byte[] array = new byte[] {
+                (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33,
+                (byte) 0x44, (byte) 0x55, (byte) 0x66,
+        };
+        final StructNlAttr attr2 = new StructNlAttr(IFLA_ADDRESS, array);
+        final MacAddress address2 = attr2.getValueAsMacAddress();
+        assertNull(address2);
+    }
+
+    @Test
+    public void testGetValueAsString() {
+        final StructNlAttr attr1 = new StructNlAttr(IFLA_IFNAME, TEST_INTERFACE_NAME);
+        final String str1 = attr1.getValueAsString();
+        assertEquals(str1, TEST_INTERFACE_NAME);
+
+        final byte[] array = new byte[] {
+                (byte) 0x77, (byte) 0x6c, (byte) 0x61, (byte) 0x6E, (byte) 0x30, (byte) 0x00,
+        };
+        final StructNlAttr attr2 = new StructNlAttr(IFLA_IFNAME, array);
+        final String str2 = attr2.getValueAsString();
+        assertEquals(str2, TEST_INTERFACE_NAME);
+    }
+}