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))
+ }
+ }
+}