Merge "Add StructNdOptRdnss class to parse RDNSS option from netlink message."
diff --git a/common/device/com/android/net/module/util/netlink/NdOption.java b/common/device/com/android/net/module/util/netlink/NdOption.java
index 50a3496..6755497 100644
--- a/common/device/com/android/net/module/util/netlink/NdOption.java
+++ b/common/device/com/android/net/module/util/netlink/NdOption.java
@@ -62,6 +62,9 @@
             case StructNdOptPref64.TYPE:
                 return StructNdOptPref64.parse(buf);
 
+            case StructNdOptRdnss.TYPE:
+                return StructNdOptRdnss.parse(buf);
+
             default:
                 int newPosition = Math.min(buf.limit(), buf.position() + length * 8);
                 buf.position(newPosition);
diff --git a/common/device/com/android/net/module/util/netlink/StructNdOptRdnss.java b/common/device/com/android/net/module/util/netlink/StructNdOptRdnss.java
new file mode 100644
index 0000000..6dee0c4
--- /dev/null
+++ b/common/device/com/android/net/module/util/netlink/StructNdOptRdnss.java
@@ -0,0 +1,134 @@
+/*
+ * 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.NetworkStackConstants.IPV6_ADDR_LEN;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.RdnssOption;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * The Recursive DNS Server Option. RFC 8106.
+ *
+ * 0                   1                   2                   3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |     Type      |     Length    |           Reserved            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                           Lifetime                            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * :            Addresses of IPv6 Recursive DNS Servers            :
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class StructNdOptRdnss extends NdOption {
+    private static final String TAG = StructNdOptRdnss.class.getSimpleName();
+    public static final int TYPE = 25;
+    // Length in 8-byte units, only if one IPv6 address included.
+    public static final byte MIN_OPTION_LEN = 3;
+
+    public final RdnssOption header;
+    @NonNull
+    public final Inet6Address[] servers;
+
+    public StructNdOptRdnss(@NonNull final Inet6Address[] servers, long lifetime) {
+        super((byte) TYPE, servers.length * 2 + 1);
+
+        Objects.requireNonNull(servers, "Recursive DNS Servers address array must not be null");
+        if (servers.length == 0) {
+            throw new IllegalArgumentException("DNS server address array must not be empty");
+        }
+
+        this.header = new RdnssOption((byte) TYPE, (byte) (servers.length * 2 + 1),
+                (short) 0 /* reserved */, lifetime);
+        this.servers = servers.clone();
+    }
+
+    /**
+     * Parses an RDNSS option from a {@link ByteBuffer}.
+     *
+     * @param buf The buffer from which to parse the option. The buffer's byte order must be
+     *            {@link java.nio.ByteOrder#BIG_ENDIAN}.
+     * @return the parsed option, or {@code null} if the option could not be parsed successfully.
+     */
+    public static StructNdOptRdnss parse(@NonNull ByteBuffer buf) {
+        if (buf == null || buf.remaining() < MIN_OPTION_LEN * 8) return null;
+        try {
+            final RdnssOption header = Struct.parse(RdnssOption.class, buf);
+            if (header.type != TYPE) {
+                throw new IllegalArgumentException("Invalid type " + header.type);
+            }
+            if (header.length < MIN_OPTION_LEN || (header.length % 2 == 0)) {
+                throw new IllegalArgumentException("Invalid length " + header.length);
+            }
+
+            final int numOfDnses = (header.length - 1) / 2;
+            final Inet6Address[] servers = new Inet6Address[numOfDnses];
+            for (int i = 0; i < numOfDnses; i++) {
+                byte[] rawAddress = new byte[IPV6_ADDR_LEN];
+                buf.get(rawAddress);
+                servers[i] = (Inet6Address) InetAddress.getByAddress(rawAddress);
+            }
+            return new StructNdOptRdnss(servers, header.lifetime);
+        } catch (IllegalArgumentException | BufferUnderflowException | UnknownHostException e) {
+            // Not great, but better than throwing an exception that might crash the caller.
+            // Convention in this package is that null indicates that the option was truncated
+            // or malformed, so callers must already handle it.
+            Log.d(TAG, "Invalid RDNSS option: " + e);
+            return null;
+        }
+    }
+
+    protected void writeToByteBuffer(ByteBuffer buf) {
+        header.writeToByteBuffer(buf);
+        for (int i = 0; i < servers.length; i++) {
+            buf.put(servers[i].getAddress());
+        }
+    }
+
+    /** Outputs the wire format of the option to a new big-endian ByteBuffer. */
+    public ByteBuffer toByteBuffer() {
+        final ByteBuffer buf = ByteBuffer.allocate(Struct.getSize(RdnssOption.class)
+                + servers.length * IPV6_ADDR_LEN);
+        writeToByteBuffer(buf);
+        buf.flip();
+        return buf;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        final StringJoiner sj = new StringJoiner(",", "[", "]");
+        for (int i = 0; i < servers.length; i++) {
+            sj.add(servers[i].getHostAddress());
+        }
+        return String.format("NdOptRdnss(%s,servers:%s)", header.toString(), sj.toString());
+    }
+}
diff --git a/common/device/com/android/net/module/util/structs/RdnssOption.java b/common/device/com/android/net/module/util/structs/RdnssOption.java
index b7c2b0c..4a5bd7e 100644
--- a/common/device/com/android/net/module/util/structs/RdnssOption.java
+++ b/common/device/com/android/net/module/util/structs/RdnssOption.java
@@ -53,7 +53,8 @@
     @Field(order = 3, type = Type.U32)
     public final long lifetime;
 
-    RdnssOption(final byte type, final byte length, final short reserved, final long lifetime) {
+    public RdnssOption(final byte type, final byte length, final short reserved,
+            final long lifetime) {
         this.type = type;
         this.length = length;
         this.reserved = reserved;
diff --git a/common/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java b/common/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
index 538c09b..f297108 100644
--- a/common/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
+++ b/common/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
@@ -20,11 +20,13 @@
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.NETLINK_ROUTE;
 
+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 android.net.InetAddresses;
 import android.net.IpPrefix;
 
 import androidx.test.filters.SmallTest;
@@ -35,6 +37,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
@@ -113,11 +116,34 @@
     }
 
     @Test
+    public void testParseRdnssOptionWithinNetlinkMessage() throws Exception {
+        final String hexBytes =
+                "4C000000440000000000000000000000"
+                + "0A0018001E0000008600000000000000"
+                + "1903000000001770FD123456789000000000000000000001"  // RDNSS option
+                + "14000100FE800000000000000250B6FFFEB7C499";
+
+        ByteBuffer buf = toBuffer(hexBytes);
+        assertEquals(76, buf.limit());
+        buf.order(ByteOrder.nativeOrder());
+
+        NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_ROUTE);
+        assertNotNull(nlMsg);
+        assertTrue(nlMsg instanceof NduseroptMessage);
+
+        NduseroptMessage msg = (NduseroptMessage) nlMsg;
+        InetAddress srcaddr = InetAddress.getByName("fe80::250:b6ff:feb7:c499%30");
+        assertMatches(AF_INET6, 24, 30, ICMP_TYPE_RA, (byte) 0, srcaddr, msg);
+        assertRdnssOption(msg.option, 6000 /* lifetime */,
+                (Inet6Address) InetAddresses.parseNumericAddress("fd12:3456:7890::1"));
+    }
+
+    @Test
     public void testParseUnknownOptionWithinNetlinkMessage() throws Exception {
         final String hexBytes =
-                "4C0000004400000000000000000000000"
-                + "A0018001E0000008600000000000000"
-                + "1903000000001770FD123456789000000000000000000001"  // RDNSS option
+                "4C000000440000000000000000000000"
+                + "0A0018001E0000008600000000000000"
+                + "310300000000177006676F6F676C652E03636F6D00000000"  // DNSSL option: "google.com"
                 + "14000100FE800000000000000250B6FFFEB7C499";
 
         ByteBuffer buf = toBuffer(hexBytes);
@@ -243,4 +269,14 @@
         StructNdOptPref64 pref64Opt = (StructNdOptPref64) opt;
         assertEquals(new IpPrefix(prefix), pref64Opt.prefix);
     }
+
+    private void assertRdnssOption(NdOption opt, long lifetime, Inet6Address... servers) {
+        assertNotNull(opt);
+        assertTrue(opt instanceof StructNdOptRdnss);
+        StructNdOptRdnss rdnss = (StructNdOptRdnss) opt;
+        assertEquals(StructNdOptRdnss.TYPE, rdnss.type);
+        assertEquals((byte) (servers.length * 2 + 1), rdnss.header.length);
+        assertEquals(lifetime, rdnss.header.lifetime);
+        assertArrayEquals(servers, rdnss.servers);
+    }
 }
diff --git a/common/tests/unit/src/com/android/net/module/util/netlink/StructNdOptRdnssTest.java b/common/tests/unit/src/com/android/net/module/util/netlink/StructNdOptRdnssTest.java
new file mode 100644
index 0000000..7df8fa7
--- /dev/null
+++ b/common/tests/unit/src/com/android/net/module/util/netlink/StructNdOptRdnssTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.NetworkStackConstants.ICMPV6_ND_OPTION_RDNSS;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.net.InetAddresses;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.structs.RdnssOption;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructNdOptRdnssTest {
+    private static final String DNS_SERVER1 = "2001:4860:4860::64";
+    private static final String DNS_SERVER2 = "2001:4860:4860::6464";
+
+    private static final Inet6Address[] DNS_SERVER_ADDRESSES = new Inet6Address[] {
+            (Inet6Address) InetAddresses.parseNumericAddress(DNS_SERVER1),
+            (Inet6Address) InetAddresses.parseNumericAddress(DNS_SERVER2),
+    };
+
+    private static final String RDNSS_OPTION_BYTES =
+            "1905"                                 // type=25, len=5 (40 bytes)
+            + "0000"                               // reserved
+            + "00000E10"                           // lifetime=3600
+            + "20014860486000000000000000000064"   // 2001:4860:4860::64
+            + "20014860486000000000000000006464";  // 2001:4860:4860::6464
+
+    private static final String RDNSS_INFINITY_LIFETIME_OPTION_BYTES =
+            "1905"                                 // type=25, len=3 (24 bytes)
+            + "0000"                               // reserved
+            + "FFFFFFFF"                           // lifetime=0xffffffff
+            + "20014860486000000000000000000064"   // 2001:4860:4860::64
+            + "20014860486000000000000000006464";  // 2001:4860:4860::6464
+
+    private void assertRdnssOptMatches(final StructNdOptRdnss opt, int length, long lifetime,
+            final Inet6Address[] servers) {
+        assertEquals(StructNdOptRdnss.TYPE, opt.type);
+        assertEquals(length, opt.length);
+        assertEquals(lifetime, opt.header.lifetime);
+        assertEquals(servers, opt.servers);
+    }
+
+    private ByteBuffer makeRdnssOption(byte type, byte length, long lifetime, String... servers)
+            throws Exception {
+        final ByteBuffer buf = ByteBuffer.allocate(8 + servers.length * 16)
+                .put(type)
+                .put(length)
+                .putShort((short) 0) // Reserved
+                .putInt((int) (lifetime & 0xFFFFFFFFL));
+        for (int i = 0; i < servers.length; i++) {
+            final byte[] rawBytes =
+                    ((Inet6Address) InetAddresses.parseNumericAddress(servers[i])).getAddress();
+            buf.put(rawBytes);
+        }
+        buf.flip();
+        return buf;
+    }
+
+    private void assertToByteBufferMatches(StructNdOptRdnss opt, String expected) {
+        String actual = HexEncoding.encodeToString(opt.toByteBuffer().array());
+        assertEquals(expected, actual);
+    }
+
+    private void doRdnssOptionParsing(final String optionHexString, int length, long lifetime,
+            final Inet6Address[] servers) {
+        final byte[] rawBytes = HexEncoding.decode(optionHexString);
+        final StructNdOptRdnss opt = StructNdOptRdnss.parse(ByteBuffer.wrap(rawBytes));
+        assertRdnssOptMatches(opt, length, lifetime, servers);
+        assertToByteBufferMatches(opt, optionHexString);
+    }
+
+    @Test
+    public void testParsing() throws Exception {
+        doRdnssOptionParsing(RDNSS_OPTION_BYTES, 5 /* length */, 3600 /* lifetime */,
+                DNS_SERVER_ADDRESSES);
+    }
+
+    @Test
+    public void testParsing_infinityLifetime() throws Exception {
+        doRdnssOptionParsing(RDNSS_INFINITY_LIFETIME_OPTION_BYTES, 5 /* length */,
+                0xffffffffL /* lifetime */, DNS_SERVER_ADDRESSES);
+    }
+
+    @Test
+    public void testToByteBuffer() {
+        final StructNdOptRdnss rdnss = new StructNdOptRdnss(DNS_SERVER_ADDRESSES, 3600);
+        assertToByteBufferMatches(rdnss, RDNSS_OPTION_BYTES);
+    }
+
+    @Test
+    public void testToByteBuffer_infinityLifetime() {
+        final StructNdOptRdnss rdnss = new StructNdOptRdnss(DNS_SERVER_ADDRESSES, 0xffffffffL);
+        assertToByteBufferMatches(rdnss, RDNSS_INFINITY_LIFETIME_OPTION_BYTES);
+    }
+
+    @Test
+    public void testParsing_invalidType() throws Exception {
+        final ByteBuffer buf = makeRdnssOption((byte) 38, (byte) 5 /* length */,
+                3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        assertNull(StructNdOptRdnss.parse(buf));
+    }
+
+    @Test
+    public void testParsing_smallOptionLength() throws Exception {
+        final ByteBuffer buf = makeRdnssOption((byte) ICMPV6_ND_OPTION_RDNSS,
+                (byte) 2 /* length */, 3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        assertNull(StructNdOptRdnss.parse(buf));
+    }
+
+    @Test
+    public void testParsing_oddOptionLength() throws Exception {
+        final ByteBuffer buf = makeRdnssOption((byte) ICMPV6_ND_OPTION_RDNSS,
+                (byte) 6 /* length */, 3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        assertNull(StructNdOptRdnss.parse(buf));
+    }
+
+    @Test
+    public void testParsing_truncatedByteBuffer() throws Exception {
+        ByteBuffer buf = makeRdnssOption((byte) ICMPV6_ND_OPTION_RDNSS,
+                (byte) 5 /* length */, 3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        final int len = buf.limit();
+        for (int i = 0; i < buf.limit() - 1; i++) {
+            buf.flip();
+            buf.limit(i);
+            assertNull("Option truncated to " + i + " bytes, should have returned null",
+                    StructNdOptRdnss.parse(buf));
+        }
+        buf.flip();
+        buf.limit(len);
+
+        final StructNdOptRdnss opt = StructNdOptRdnss.parse(buf);
+        assertRdnssOptMatches(opt, 5 /* length */, 3600 /* lifetime */, DNS_SERVER_ADDRESSES);
+    }
+
+    @Test
+    public void testParsing_nullByteBuffer() {
+        assertNull(StructNdOptRdnss.parse(null));
+    }
+
+    @Test
+    public void testParsing_invalidByteBufferLength() throws Exception {
+        final ByteBuffer buf = makeRdnssOption((byte) ICMPV6_ND_OPTION_RDNSS,
+                (byte) 5 /* length */, 3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        buf.limit(20); // less than MIN_OPT_LEN * 8
+        assertNull(StructNdOptRdnss.parse(buf));
+    }
+
+    @Test
+    public void testConstructor_nullDnsServerAddressArray() {
+        assertThrows(NullPointerException.class,
+                () -> new StructNdOptRdnss(null /* servers */, 3600 /* lifetime */));
+    }
+
+    @Test
+    public void testConstructor_emptyDnsServerAddressArray() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new StructNdOptRdnss(new Inet6Address[0] /* empty server array */,
+                                           3600 /* lifetime*/));
+    }
+
+    @Test
+    public void testToString() {
+        final ByteBuffer buf = RdnssOption.build(3600 /* lifetime */, DNS_SERVER1, DNS_SERVER2);
+        final StructNdOptRdnss opt = StructNdOptRdnss.parse(buf);
+        final String expected = "NdOptRdnss(type: 25, length: 5, reserved: 0, lifetime: 3600,"
+                + "servers:[2001:4860:4860::64,2001:4860:4860::6464])";
+        assertRdnssOptMatches(opt, 5 /* length */, 3600 /* lifetime */, DNS_SERVER_ADDRESSES);
+        assertEquals(expected, opt.toString());
+    }
+}