blob: 9eb2a748c2a4d7f235f48d05cf5e34ecd9b5b22c [file] [log] [blame]
/*
* 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 static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_OPTION_LEN;
import android.net.MacAddress;
import android.util.Log;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.HexDump;
import com.android.net.module.util.Struct;
import com.android.net.module.util.structs.IaPdOption;
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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
/**
* Defines basic data and operations needed to build and use packets for the
* DHCPv6 protocol. Subclasses create the specific packets used at each
* stage of the negotiation.
*
* @hide
*/
public class Dhcp6Packet {
private static final String TAG = Dhcp6Packet.class.getSimpleName();
/**
* DHCPv6 Message Type.
*/
public static final byte DHCP6_MESSAGE_TYPE_SOLICIT = 1;
public static final byte DHCP6_MESSAGE_TYPE_ADVERTISE = 2;
public static final byte DHCP6_MESSAGE_TYPE_REQUEST = 3;
public static final byte DHCP6_MESSAGE_TYPE_CONFIRM = 4;
public static final byte DHCP6_MESSAGE_TYPE_RENEW = 5;
public static final byte DHCP6_MESSAGE_TYPE_REBIND = 6;
public static final byte DHCP6_MESSAGE_TYPE_REPLY = 7;
public static final byte DHCP6_MESSAGE_TYPE_RELEASE = 8;
public static final byte DHCP6_MESSAGE_TYPE_DECLINE = 9;
public static final byte DHCP6_MESSAGE_TYPE_RECONFIGURE = 10;
public static final byte DHCP6_MESSAGE_TYPE_INFORMATION_REQUEST = 11;
public static final byte DHCP6_MESSAGE_TYPE_RELAY_FORW = 12;
public static final byte DHCP6_MESSAGE_TYPE_RELAY_REPL = 13;
/**
* DHCPv6 Optional Type: Client Identifier.
* DHCPv6 message from client must have this option.
*/
public static final byte DHCP6_CLIENT_IDENTIFIER = 1;
@NonNull
protected final byte[] mClientDuid;
/**
* DHCPv6 Optional Type: Server Identifier.
*/
public static final byte DHCP6_SERVER_IDENTIFIER = 2;
protected final byte[] mServerDuid;
/**
* DHCPv6 Optional Type: Option Request Option.
*/
public static final byte DHCP6_OPTION_REQUEST_OPTION = 6;
/**
* DHCPv6 Optional Type: Elapsed time.
* This time is expressed in hundredths of a second.
*/
public static final byte DHCP6_ELAPSED_TIME = 8;
protected final int mElapsedTime;
/**
* DHCPv6 Optional Type: Status Code.
*/
public static final byte DHCP6_STATUS_CODE = 13;
protected short mStatusCode;
protected String mStatusMsg;
public static final short STATUS_SUCCESS = 0;
public static final short STATUS_UNSPEC_FAIL = 1;
public static final short STATUS_NO_ADDR_AVAI = 2;
public static final short STATUS_NO_BINDING = 3;
public static final short STATUS_PREFIX_NOT_ONLINK = 4;
public static final short STATUS_USE_MULTICAST = 5;
public static final short STATUS_NO_PREFIX_AVAI = 6;
/**
* DHCPv6 zero-length Optional Type: Rapid Commit. Per RFC4039, both DHCPDISCOVER and DHCPACK
* packet may include this option.
*/
public static final byte DHCP6_RAPID_COMMIT = 14;
public boolean mRapidCommit;
/**
* DHCPv6 Optional Type: IA_PD.
*/
public static final byte DHCP6_IA_PD = 25;
@NonNull
protected final byte[] mIaPd;
@NonNull
protected PrefixDelegation mPrefixDelegation;
/**
* DHCPv6 Optional Type: IA Prefix Option.
*/
public static final byte DHCP6_IAPREFIX = 26;
/**
* DHCPv6 Optional Type: SOL_MAX_RT.
*/
public static final byte DHCP6_SOL_MAX_RT = 82;
private OptionalInt mSolMaxRt;
/**
* The transaction identifier used in this particular DHCPv6 negotiation
*/
protected final int mTransId;
/**
* The unique identifier for IA_NA, IA_TA, IA_PD used in this particular DHCPv6 negotiation
*/
protected int mIaId;
// Per rfc8415#section-12, the IAID MUST be consistent across restarts.
// Since currently only one IAID is supported, a well-known value can be used (0).
public static final int IAID = 0;
Dhcp6Packet(int transId, int elapsedTime, @NonNull final byte[] clientDuid,
final byte[] serverDuid, @NonNull final byte[] iapd) {
mTransId = transId;
mElapsedTime = elapsedTime;
mClientDuid = clientDuid;
mServerDuid = serverDuid;
mIaPd = iapd;
}
/**
* Returns the transaction ID.
*/
public int getTransactionId() {
return mTransId;
}
/**
* Returns decoded IA_PD options associated with IA_ID.
*/
@VisibleForTesting
public PrefixDelegation getPrefixDelegation() {
return mPrefixDelegation;
}
/**
* Returns IA_ID associated to IA_PD.
*/
public int getIaId() {
return mIaId;
}
/**
* Returns the client's DUID.
*/
@NonNull
public byte[] getClientDuid() {
return mClientDuid;
}
/**
* Returns the server's DUID.
*/
public byte[] getServerDuid() {
return mServerDuid;
}
/**
* Returns the SOL_MAX_RT option value in milliseconds.
*/
public OptionalInt getSolMaxRtMs() {
return mSolMaxRt;
}
/**
* 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 final int iaid;
public final int t1;
public final int t2;
@NonNull
public final List<IaPrefixOption> ipos;
PrefixDelegation(int iaid, int t1, int t2, @NonNull final List<IaPrefixOption> ipos) {
Objects.requireNonNull(ipos);
this.iaid = iaid;
this.t1 = t1;
this.t2 = t2;
this.ipos = ipos;
}
/**
* Check whether or not the delegated prefix in DHCPv6 packet is valid.
*
* TODO: ensure that the prefix has a reasonable lifetime, and the timers aren't too short.
*/
public boolean isValid() {
if (iaid != IAID) {
Log.w(TAG, "IA_ID doesn't match, expected: " + IAID + ", actual: " + iaid);
return false;
}
if (t1 < 0 || t2 < 0) {
Log.e(TAG, "IA_PD option with invalid T1 " + t1 + " or T2 " + t2);
return false;
}
// Generally, t1 must be smaller or equal to t2 (except when t2 is 0).
if (t2 != 0 && t1 > t2) {
Log.e(TAG, "IA_PD option with T1 " + t1 + " greater than T2 " + t2);
return false;
}
return true;
}
/**
* Decode IA Prefix options.
*/
public static PrefixDelegation decode(@NonNull final ByteBuffer buffer)
throws ParseException {
try {
final int iaid = buffer.getInt();
final int t1 = buffer.getInt();
final int t2 = buffer.getInt();
final List<IaPrefixOption> ipos = new ArrayList<IaPrefixOption>();
while (buffer.remaining() > 0) {
final int original = buffer.position();
final short optionType = buffer.getShort();
final int optionLen = buffer.getShort() & 0xFFFF;
switch (optionType) {
case DHCP6_IAPREFIX:
buffer.position(original);
final IaPrefixOption ipo = Struct.parse(IaPrefixOption.class, buffer);
Log.d(TAG, "IA Prefix Option: " + ipo);
ipos.add(ipo);
break;
// TODO: support DHCP6_STATUS_CODE option
default:
skipOption(buffer, optionLen);
}
}
return new PrefixDelegation(iaid, t1, t2, ipos);
} catch (BufferUnderflowException e) {
throw new ParseException(e.getMessage());
}
}
/**
* Return valid IA prefix options to be used and extended in the Reply message. It may
* return empty list if there isn't any valid IA prefix option in the Reply message.
*
* TODO: ensure that the prefix has a reasonable lifetime, and the timers aren't too short.
* and handle status code such as NoPrefixAvail.
*/
public List<IaPrefixOption> getValidIaPrefixes() {
final List<IaPrefixOption> validIpos = new ArrayList<IaPrefixOption>();
for (IaPrefixOption ipo : ipos) {
if (!ipo.isValid()) continue;
validIpos.add(ipo);
}
return validIpos;
}
@Override
public String toString() {
return "Prefix Delegation: iaid " + iaid + ", t1 " + t1 + ", t2 " + t2
+ ", IA prefix options: " + ipos;
}
/**
* Compare the preferred lifetime in the IA prefix optin list and return the minimum one.
* TODO: exclude 0 preferred lifetime.
*/
public long getMinimalPreferredLifetime() {
final IaPrefixOption ipo = Collections.min(ipos,
(IaPrefixOption lhs, IaPrefixOption rhs) -> Long.compare(lhs.preferred,
rhs.preferred));
return ipo.preferred;
}
/**
* Compare the valid lifetime in the IA prefix optin list and return the minimum one.
* TODO: exclude 0 valid lifetime.
*/
public long getMinimalValidLifetime() {
final IaPrefixOption ipo = Collections.min(ipos,
(IaPrefixOption lhs, IaPrefixOption rhs) -> Long.compare(lhs.valid, rhs.valid));
return ipo.valid;
}
}
/**
* DHCPv6 packet parsing exception.
*/
public static class ParseException extends Exception {
ParseException(String msg) {
super(msg);
}
}
private static void skipOption(@NonNull 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) .
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
private static Dhcp6Packet decode(@NonNull final ByteBuffer packet) throws ParseException {
int elapsedTime = 0;
byte[] iapd = null;
byte[] serverDuid = null;
byte[] clientDuid = null;
short statusCode = STATUS_SUCCESS;
String statusMsg = null;
boolean rapidCommit = false;
int solMaxRt = 0;
PrefixDelegation pd = null;
packet.order(ByteOrder.BIG_ENDIAN);
// DHCPv6 message contents.
final int msgTypeAndTransId = packet.getInt();
final byte messageType = (byte) (msgTypeAndTransId >> 24);
final int transId = msgTypeAndTransId & 0xffffff;
/**
* 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;
pd = PrefixDelegation.decode(ByteBuffer.wrap(iapd));
break;
case DHCP6_RAPID_COMMIT:
expectedLen = 0;
rapidCommit = true;
break;
case DHCP6_ELAPSED_TIME:
expectedLen = 2;
elapsedTime = (int) (packet.getShort() & 0xFFFF);
break;
case DHCP6_STATUS_CODE:
expectedLen = optionLen;
statusCode = packet.getShort();
statusMsg = readAsciiString(packet, expectedLen - 2, false /* isNullOk */);
break;
case DHCP6_SOL_MAX_RT:
expectedLen = 4;
solMaxRt = packet.getInt();
break;
default:
expectedLen = optionLen;
// BufferUnderflowException will be thrown if option is truncated.
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, elapsedTime, clientDuid, iapd,
rapidCommit);
break;
case DHCP6_MESSAGE_TYPE_ADVERTISE:
newPacket = new Dhcp6AdvertisePacket(transId, clientDuid, serverDuid, iapd);
break;
case DHCP6_MESSAGE_TYPE_REQUEST:
newPacket = new Dhcp6RequestPacket(transId, elapsedTime, clientDuid, serverDuid,
iapd);
break;
case DHCP6_MESSAGE_TYPE_REPLY:
newPacket = new Dhcp6ReplyPacket(transId, clientDuid, serverDuid, iapd,
rapidCommit);
break;
case DHCP6_MESSAGE_TYPE_RENEW:
newPacket = new Dhcp6RenewPacket(transId, elapsedTime, clientDuid, serverDuid,
iapd);
break;
case DHCP6_MESSAGE_TYPE_REBIND:
newPacket = new Dhcp6RebindPacket(transId, elapsedTime, clientDuid, iapd);
break;
default:
throw new ParseException("Unimplemented DHCP6 message type %d" + messageType);
}
if (pd != null) {
newPacket.mPrefixDelegation = pd;
newPacket.mIaId = pd.iaid;
}
newPacket.mStatusCode = statusCode;
newPacket.mStatusMsg = statusMsg;
newPacket.mRapidCommit = rapidCommit;
newPacket.mSolMaxRt =
(solMaxRt >= 60 && solMaxRt <= 86400)
? OptionalInt.of(solMaxRt * 1000)
: OptionalInt.empty();
return newPacket;
}
/**
* Parse a packet from an array of bytes, stopping at the given length.
*/
public static Dhcp6Packet decode(@NonNull final byte[] packet, int length)
throws ParseException {
final ByteBuffer buffer = ByteBuffer.wrap(packet, 0, length).order(ByteOrder.BIG_ENDIAN);
return decode(buffer);
}
/**
* Follow RFC8415 section 18.2.9 and 18.2.10 to check if the received DHCPv6 message is valid.
*/
public boolean isValid(int transId, @NonNull final byte[] clientDuid) {
if (mClientDuid == null) {
Log.e(TAG, "DHCPv6 message without Client DUID option");
return false;
}
if (!Arrays.equals(mClientDuid, clientDuid)) {
Log.e(TAG, "Unexpected client DUID " + HexDump.toHexString(mClientDuid)
+ ", expected " + HexDump.toHexString(clientDuid));
return false;
}
if (mTransId != transId) {
Log.e(TAG, "Unexpected transaction ID " + mTransId + ", expected " + transId);
return false;
}
if (mPrefixDelegation == null) {
Log.e(TAG, "DHCPv6 message without IA_PD option, ignoring");
return false;
}
if (!mPrefixDelegation.isValid()) {
Log.e(TAG, "DHCPv6 message takes invalid IA_PD option, ignoring");
return false;
}
//TODO: check if the status code is success or not.
return true;
}
/**
* Returns the client DUID, follows RFC 8415 and creates a client DUID
* based on the link-layer address(DUID-LL).
*
* TODO: use Struct to build and parse DUID.
*
* 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | DUID-Type (3) | hardware type (16 bits) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* . .
* . link-layer address (variable length) .
* . .
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public static byte[] createClientDuid(@NonNull final MacAddress macAddress) {
final byte[] duid = new byte[10];
// type: Link-layer address(3)
duid[0] = (byte) 0x00;
duid[1] = (byte) 0x03;
// hardware type: Ethernet(1)
duid[2] = (byte) 0x00;
duid[3] = (byte) 0x01;
System.arraycopy(macAddress.toByteArray() /* src */, 0 /* srcPos */, duid /* dest */,
4 /* destPos */, 6 /* length */);
return duid;
}
/**
* Adds an optional parameter containing an array of bytes.
*/
protected static void addTlv(ByteBuffer buf, short type, @NonNull byte[] payload) {
if (payload.length > DHCP_MAX_OPTION_LEN) {
throw new IllegalArgumentException("DHCP option too long: "
+ payload.length + " vs. " + DHCP_MAX_OPTION_LEN);
}
buf.putShort(type);
buf.putShort((short) payload.length);
buf.put(payload);
}
/**
* Adds an optional parameter containing a short integer.
*/
protected static void addTlv(ByteBuffer buf, short type, short value) {
buf.putShort(type);
buf.putShort((short) 2);
buf.putShort(value);
}
/**
* Adds an optional parameter containing zero-length value.
*/
protected static void addTlv(ByteBuffer buf, short type) {
buf.putShort(type);
buf.putShort((short) 0);
}
/**
* Build an IA_PD option from given specific parameters, including IA_PREFIX option.
*/
public static ByteBuffer buildIaPdOption(int iaid, int t1, int t2,
@NonNull final List<IaPrefixOption> ipos) {
final ByteBuffer iapd = ByteBuffer.allocate(IaPdOption.LENGTH
+ Struct.getSize(IaPrefixOption.class) * ipos.size());
iapd.putInt(iaid);
iapd.putInt(t1);
iapd.putInt(t2);
for (IaPrefixOption ipo : ipos) {
ipo.writeToByteBuffer(iapd);
}
iapd.flip();
return iapd;
}
/**
* Builds a DHCPv6 SOLICIT packet from the required specified parameters.
*/
public static ByteBuffer buildSolicitPacket(int transId, long millisecs,
@NonNull final byte[] iapd, @NonNull final byte[] clientDuid, boolean rapidCommit) {
final Dhcp6SolicitPacket pkt =
new Dhcp6SolicitPacket(transId, (int) (millisecs / 10) /* elapsed time */,
clientDuid, iapd, rapidCommit);
return pkt.buildPacket();
}
/**
* Builds a DHCPv6 ADVERTISE packet from the required specified parameters.
*/
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();
}
/**
* Builds a DHCPv6 REPLY packet from the required specified parameters.
*/
public static ByteBuffer buildReplyPacket(int transId, @NonNull final byte[] iapd,
@NonNull final byte[] clientDuid, @NonNull final byte[] serverDuid,
boolean rapidCommit) {
final Dhcp6ReplyPacket pkt =
new Dhcp6ReplyPacket(transId, clientDuid, serverDuid, iapd, rapidCommit);
return pkt.buildPacket();
}
/**
* Builds a DHCPv6 REQUEST packet from the required specified parameters.
*/
public static ByteBuffer buildRequestPacket(int transId, long millisecs,
@NonNull final byte[] iapd, @NonNull final byte[] clientDuid,
@NonNull final byte[] serverDuid) {
final Dhcp6RequestPacket pkt =
new Dhcp6RequestPacket(transId, (int) (millisecs / 10) /* elapsed time */,
clientDuid, serverDuid, iapd);
return pkt.buildPacket();
}
/**
* Builds a DHCPv6 RENEW packet from the required specified parameters.
*/
public static ByteBuffer buildRenewPacket(int transId, long millisecs,
@NonNull final byte[] iapd, @NonNull final byte[] clientDuid,
@NonNull final byte[] serverDuid) {
final Dhcp6RenewPacket pkt =
new Dhcp6RenewPacket(transId, (int) (millisecs / 10) /* elapsed time */, clientDuid,
serverDuid, iapd);
return pkt.buildPacket();
}
/**
* Builds a DHCPv6 REBIND packet from the required specified parameters.
*/
public static ByteBuffer buildRebindPacket(int transId, long millisecs,
@NonNull final byte[] iapd, @NonNull final byte[] clientDuid) {
final Dhcp6RebindPacket pkt = new Dhcp6RebindPacket(transId,
(int) (millisecs / 10) /* elapsed time */, clientDuid, iapd);
return pkt.buildPacket();
}
}