Add DHCPv6 packet decode function.

Bug: 260934173
Test: m
Change-Id: I49b93e875b0acf387af130994fea5f2cc5e4a269
diff --git a/src/android/net/dhcp6/Dhcp6AdvertisePacket.java b/src/android/net/dhcp6/Dhcp6AdvertisePacket.java
index a5b3d4b..3eb5503 100644
--- a/src/android/net/dhcp6/Dhcp6AdvertisePacket.java
+++ b/src/android/net/dhcp6/Dhcp6AdvertisePacket.java
@@ -18,6 +18,8 @@
 
 import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
 
+import androidx.annotation.NonNull;
+
 import java.nio.ByteBuffer;
 
 /**
@@ -30,8 +32,8 @@
     /**
      * Generates an advertise packet with the specified parameters.
      */
-    Dhcp6AdvertisePacket(int transId, final byte[] clientDuid, final byte[] serverDuid,
-            final byte[] iapd) {
+    Dhcp6AdvertisePacket(int transId, @NonNull final byte[] clientDuid,
+            @NonNull final byte[] serverDuid, final byte[] iapd) {
         super(transId, (short) 0 /* secs */, clientDuid, serverDuid, iapd);
     }
 
diff --git a/src/android/net/dhcp6/Dhcp6Packet.java b/src/android/net/dhcp6/Dhcp6Packet.java
index d51307c..98c214b 100644
--- a/src/android/net/dhcp6/Dhcp6Packet.java
+++ b/src/android/net/dhcp6/Dhcp6Packet.java
@@ -19,8 +19,15 @@
 import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_OPTION_LEN;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.IaPrefixOption;
+
+import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
 
 /**
  * Defines basic data and operations needed to build and use packets for the
@@ -89,6 +96,8 @@
     public static final byte DHCP6_IA_PD = 25;
     @NonNull
     protected final byte[] mIaPd;
+    @NonNull
+    protected PrefixDelegation mPrefixDelegation;
 
     /**
      * The transaction identifier used in this particular DHCPv6 negotiation
@@ -139,6 +148,203 @@
     }
 
     /**
+     * A class to take DHCPv6 IA_PD option allocated from server.
+     * https://www.rfc-editor.org/rfc/rfc8415.html#section-21.21
+     */
+    public static class PrefixDelegation {
+        public int iaid;
+        public int t1;
+        public int t2;
+        public final IaPrefixOption ipo;
+
+        PrefixDelegation(int iaid, int t1, int t2, final IaPrefixOption ipo) {
+            this.iaid = iaid;
+            this.t1 = t1;
+            this.t2 = t2;
+            this.ipo = ipo;
+        }
+
+        @Override
+        public String toString() {
+            return "Prefix Delegation: iaid " + iaid + ", t1 " + t1 + ", t2 " + t2
+                    + ", prefix " + ipo;
+        }
+    }
+
+    /**
+     * DHCPv6 packet parsing exception.
+     */
+    public static class ParseException extends Exception {
+        ParseException(String msg) {
+            super(msg);
+        }
+    }
+
+    private static void skipOption(final ByteBuffer packet, int optionLen)
+            throws BufferUnderflowException {
+        for (int i = 0; i < optionLen; i++) {
+            packet.get();
+        }
+    }
+
+    /**
+     * Reads a string of specified length from the buffer.
+     *
+     * TODO: move to a common place which can be shared with DhcpClient.
+     */
+    private static String readAsciiString(@NonNull final ByteBuffer buf, int byteCount,
+            boolean isNullOk) {
+        final byte[] bytes = new byte[byteCount];
+        buf.get(bytes);
+        return readAsciiString(bytes, isNullOk);
+    }
+
+    private static String readAsciiString(@NonNull final byte[] payload, boolean isNullOk) {
+        final byte[] bytes = payload;
+        int length = bytes.length;
+        if (!isNullOk) {
+            // Stop at the first null byte. This is because some DHCP options (e.g., the domain
+            // name) are passed to netd via FrameworkListener, which refuses arguments containing
+            // null bytes. We don't do this by default because vendorInfo is an opaque string which
+            // could in theory contain null bytes.
+            for (length = 0; length < bytes.length; length++) {
+                if (bytes[length] == 0) {
+                    break;
+                }
+            }
+        }
+        return new String(bytes, 0, length, StandardCharsets.US_ASCII);
+    }
+
+    /**
+     * Creates a concrete Dhcp6Packet from the supplied ByteBuffer.
+     *
+     * The buffer only starts with a UDP encapsulation (i.e. DHCPv6 message). A subset of the
+     * optional parameters are parsed and are stored in object fields. Client/Server message
+     * format:
+     *
+     *  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
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |    msg-type   |               transaction-id                  |
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |                                                               |
+     * .                            options                            .
+     * .                 (variable number and length)                  .
+     * |                                                               |
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     */
+    @VisibleForTesting
+    static Dhcp6Packet decodePacket(@NonNull final ByteBuffer packet) throws ParseException {
+        short secs = 0;
+        byte[] iapd = null;
+        byte[] serverDuid = null;
+        byte[] clientDuid = null;
+        short statusCode = STATUS_SUCCESS;
+        String statusMsg = null;
+
+        packet.order(ByteOrder.BIG_ENDIAN);
+
+        // DHCPv6 message contents.
+        final int msgTypeAndTransId = packet.getInt();
+        final byte messageType = (byte) (msgTypeAndTransId >> 24);
+        final int transId = msgTypeAndTransId & 0x0FFF;
+
+        /**
+         * Parse DHCPv6 options, option format:
+         *
+         * 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
+         * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         * |          option-code          |           option-len          |
+         * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         * |                          option-data                          |
+         * |                      (option-len octets)                      |
+         * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         */
+        while (packet.hasRemaining()) {
+            try {
+                final short optionType = packet.getShort();
+                final int optionLen = packet.getShort() & 0xFFFF;
+                int expectedLen = 0;
+
+                switch(optionType) {
+                    case DHCP6_SERVER_IDENTIFIER:
+                        expectedLen = optionLen;
+                        final byte[] sduid = new byte[expectedLen];
+                        packet.get(sduid, 0 /* offset */, expectedLen);
+                        serverDuid = sduid;
+                        break;
+                    case DHCP6_CLIENT_IDENTIFIER:
+                        expectedLen = optionLen;
+                        final byte[] cduid = new byte[expectedLen];
+                        packet.get(cduid, 0 /* offset */, expectedLen);
+                        clientDuid = cduid;
+                        break;
+                    case DHCP6_IA_PD:
+                        expectedLen = optionLen;
+                        final byte[] bytes = new byte[expectedLen];
+                        packet.get(bytes, 0 /* offset */, expectedLen);
+                        iapd = bytes;
+                        break;
+                    case DHCP6_ELAPSED_TIME:
+                        expectedLen = 2;
+                        secs = packet.getShort();
+                        break;
+                    case DHCP6_STATUS_CODE:
+                        expectedLen = optionLen;
+                        statusCode = packet.getShort();
+                        statusMsg = readAsciiString(packet, expectedLen - 2, false /* isNullOk */);
+                        break;
+                    default:
+                        skipOption(packet, optionLen);
+                        break;
+                }
+                if (expectedLen != optionLen) {
+                    throw new ParseException(
+                            "Invalid length " + optionLen + " for option " + optionType
+                                    + ", expected " + expectedLen);
+                }
+            } catch (BufferUnderflowException e) {
+                throw new ParseException(e.getMessage());
+            }
+        }
+
+        Dhcp6Packet newPacket;
+
+        switch(messageType) {
+            case DHCP6_MESSAGE_TYPE_SOLICIT:
+                newPacket = new Dhcp6SolicitPacket(transId, secs, clientDuid, iapd);
+                break;
+            case DHCP6_MESSAGE_TYPE_ADVERTISE:
+                newPacket = new Dhcp6AdvertisePacket(transId, clientDuid, serverDuid, iapd);
+                break;
+            case DHCP6_MESSAGE_TYPE_REQUEST:
+                newPacket = new Dhcp6RequestPacket(transId, secs, clientDuid, serverDuid, iapd);
+                break;
+            case DHCP6_MESSAGE_TYPE_REPLY:
+                newPacket = new Dhcp6ReplyPacket(transId, clientDuid, serverDuid, iapd);
+                break;
+            default:
+                throw new ParseException("Unimplemented DHCP6 message type %d" + messageType);
+        }
+
+        if (iapd != null) {
+            final ByteBuffer buffer = ByteBuffer.wrap(iapd);
+            final int iaid = buffer.getInt();
+            final int t1 = buffer.getInt();
+            final int t2 = buffer.getInt();
+            final IaPrefixOption ipo = Struct.parse(IaPrefixOption.class, buffer);
+            newPacket.mPrefixDelegation = new PrefixDelegation(iaid, t1, t2, ipo);
+            newPacket.mIaId = iaid;
+        }
+        newPacket.mStatusCode = statusCode;
+        newPacket.mStatusMsg = statusMsg;
+
+        return newPacket;
+    }
+
+    /**
      * Adds an optional parameter containing an array of bytes.
      */
     protected static void addTlv(ByteBuffer buf, short type, @NonNull byte[] payload) {
@@ -163,17 +369,17 @@
     /**
      * Builds a DHCPv6 SOLICIT packet from the required specified parameters.
      */
-    public static ByteBuffer buildSolicitPacket(int transId, short secs, final byte[] iapd,
-            final byte[] duid) {
-        final Dhcp6SolicitPacket pkt = new Dhcp6SolicitPacket(transId, secs, duid, iapd);
+    public static ByteBuffer buildSolicitPacket(int transId, short secs, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid) {
+        final Dhcp6SolicitPacket pkt = new Dhcp6SolicitPacket(transId, secs, clientDuid, iapd);
         return pkt.buildPacket();
     }
 
     /**
      * Builds a DHCPv6 ADVERTISE packet from the required specified parameters.
      */
-    public static ByteBuffer buildAdvertisePacket(int transId, final byte[] iapd,
-            final byte[] clientDuid, final byte[] serverDuid) {
+    public static ByteBuffer buildAdvertisePacket(int transId, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid, @NonNull final byte[] serverDuid) {
         final Dhcp6AdvertisePacket pkt =
                 new Dhcp6AdvertisePacket(transId, clientDuid, serverDuid, iapd);
         return pkt.buildPacket();
@@ -182,8 +388,8 @@
     /**
      * Builds a DHCPv6 REPLY packet from the required specified parameters.
      */
-    public static ByteBuffer buildReplyPacket(int transId, final byte[] iapd,
-            final byte[] clientDuid, final byte[] serverDuid) {
+    public static ByteBuffer buildReplyPacket(int transId, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid, @NonNull final byte[] serverDuid) {
         final Dhcp6ReplyPacket pkt = new Dhcp6ReplyPacket(transId, clientDuid, serverDuid, iapd);
         return pkt.buildPacket();
     }
@@ -191,8 +397,8 @@
     /**
      * Builds a DHCPv6 REQUEST packet from the required specified parameters.
      */
-    public static ByteBuffer buildRequestPacket(int transId, short secs, final byte[] iapd,
-            final byte[] clientDuid, final byte[] serverDuid) {
+    public static ByteBuffer buildRequestPacket(int transId, short secs, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid, @NonNull final byte[] serverDuid) {
         final Dhcp6RequestPacket pkt =
                 new Dhcp6RequestPacket(transId, secs, clientDuid, serverDuid, iapd);
         return pkt.buildPacket();
diff --git a/src/android/net/dhcp6/Dhcp6ReplyPacket.java b/src/android/net/dhcp6/Dhcp6ReplyPacket.java
index 2da9bbb..4b37478 100644
--- a/src/android/net/dhcp6/Dhcp6ReplyPacket.java
+++ b/src/android/net/dhcp6/Dhcp6ReplyPacket.java
@@ -18,6 +18,8 @@
 
 import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
 
+import androidx.annotation.NonNull;
+
 import java.nio.ByteBuffer;
 
 /**
@@ -31,8 +33,8 @@
     /**
      * Generates a reply packet with the specified parameters.
      */
-    Dhcp6ReplyPacket(int transId, final byte[] clientDuid, final byte[] serverDuid,
-            final byte[] iapd) {
+    Dhcp6ReplyPacket(int transId, @NonNull final byte[] clientDuid,
+            @NonNull final byte[] serverDuid, final byte[] iapd) {
         super(transId, (short) 0 /* secs */, clientDuid, serverDuid, iapd);
     }
 
diff --git a/src/android/net/dhcp6/Dhcp6RequestPacket.java b/src/android/net/dhcp6/Dhcp6RequestPacket.java
index eaad8d0..f2f398d 100644
--- a/src/android/net/dhcp6/Dhcp6RequestPacket.java
+++ b/src/android/net/dhcp6/Dhcp6RequestPacket.java
@@ -18,6 +18,8 @@
 
 import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
 
+import androidx.annotation.NonNull;
+
 import java.nio.ByteBuffer;
 
 /**
@@ -30,8 +32,8 @@
     /**
      * Generates a request packet with the specified parameters.
      */
-    Dhcp6RequestPacket(int transId, short secs, final byte[] clientDuid, final byte[] serverDuid,
-            final byte[] iapd) {
+    Dhcp6RequestPacket(int transId, short secs, @NonNull final byte[] clientDuid,
+            @NonNull final byte[] serverDuid, final byte[] iapd) {
         super(transId, secs, clientDuid, serverDuid, iapd);
     }
 
diff --git a/src/android/net/dhcp6/Dhcp6SolicitPacket.java b/src/android/net/dhcp6/Dhcp6SolicitPacket.java
index 23de8c9..909ff2c 100644
--- a/src/android/net/dhcp6/Dhcp6SolicitPacket.java
+++ b/src/android/net/dhcp6/Dhcp6SolicitPacket.java
@@ -18,6 +18,8 @@
 
 import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
 
+import androidx.annotation.NonNull;
+
 import java.nio.ByteBuffer;
 
 /**
@@ -29,7 +31,8 @@
     /**
      * Generates a solicit packet with the specified parameters.
      */
-    Dhcp6SolicitPacket(int transId, short secs, final byte[] clientDuid, final byte[] iapd) {
+    Dhcp6SolicitPacket(int transId, short secs, @NonNull final byte[] clientDuid,
+            final byte[] iapd) {
         super(transId, secs, clientDuid, null /* serverDuid */, iapd);
     }
 
diff --git a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
new file mode 100644
index 0000000..264f785
--- /dev/null
+++ b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 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 android.net.dhcp6
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.net.module.util.HexDump
+import com.android.testutils.assertThrows
+import java.nio.ByteBuffer
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class Dhcp6PacketTest {
+    @Test
+    fun testDecodeDhcp6SolicitPacket() {
+        val solicitHex =
+                // Solicit, Transaction ID
+                "01000F51" +
+                // client identifier option(option_len=12)
+                "0001000C0003001B024CCBFFFE5F6EA9" +
+                // elapsed time option(option_len=2)
+                "000800020000" +
+                // IA_PD option(option_len=41, including IA prefix option)
+                "00190029DE3570F50000000000000000" +
+                // IA prefix option(option_len=25)
+                "001A001900000000000000004000000000000000000000000000000000"
+        val bytes = HexDump.hexStringToByteArray(solicitHex)
+        val packet = Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        assertTrue(packet is Dhcp6SolicitPacket)
+    }
+
+    @Test
+    fun testDecodeDhcp6SolicitPacket_incorrectOptionLength() {
+        val solicitHex =
+                // Solicit, Transaction ID
+                "01000F51" +
+                // client identifier option(option_len=12)
+                "0001000C0003001B024CCBFFFE5F6EA9" +
+                // elapsed time option(wrong option_len: 4)
+                "000800040000" +
+                // IA_PD option(option_len=41, including IA prefix option)
+                "00190029DE3570F50000000000000000" +
+                // IA prefix option(option_len=25)
+                "001A001900000000000000004000000000000000000000000000000000"
+        val bytes = HexDump.hexStringToByteArray(solicitHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+                Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        }
+    }
+
+    @Test
+    fun testDecodeDhcp6SolicitPacket_lastTruncatedOption() {
+        val solicitHex =
+                // Solicit, Transaction ID
+                "01000F51" +
+                // client identifier option(option_len=12)
+                "0001000C0003001B024CCBFFFE5F6EA9" +
+                // elapsed time option(option_len=2)
+                "000800020000" +
+                // IA_PD option(option_len=41, including IA prefix option)
+                "00190029DE3570F50000000000000000" +
+                // IA prefix option(option_len=25, missing one byte)
+                "001A0019000000000000000040000000000000000000000000000000"
+        val bytes = HexDump.hexStringToByteArray(solicitHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+                Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        }
+    }
+
+    @Test
+    fun testDecodeDhcp6SolicitPacket_middleTruncatedOption() {
+        val solicitHex =
+                // Solicit, Transaction ID
+                "01000F51" +
+                // client identifier option(option_len=12, missing one byte)
+                "0001000C0003001B024CCBFFFE5F6E" +
+                // elapsed time option(option_len=2)
+                "000800020000" +
+                // IA_PD option(option_len=41, including IA prefix option)
+                "00190029DE3570F50000000000000000" +
+                // IA prefix option(option_len=25)
+                "001A001900000000000000004000000000000000000000000000000000"
+        val bytes = HexDump.hexStringToByteArray(solicitHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+                Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        }
+    }
+}