/* * 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.thread; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkState; import static com.android.net.module.util.HexDump.toHexString; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Objects.requireNonNull; import android.annotation.FlaggedApi; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Size; import android.annotation.SystemApi; import android.net.IpPrefix; import android.os.Parcel; import android.os.Parcelable; import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import java.io.ByteArrayOutputStream; import java.net.Inet6Address; import java.net.UnknownHostException; import java.util.Arrays; /** * Data interface for managing a Thread Active Operational Dataset. * *
An example usage of creating an Active Operational Dataset with randomized parameters: * *
{@code * ActiveOperationalDataset activeDataset = controller.createRandomizedDataset("MyNet"); * }* *
or randomized Dataset with customized channel: * *
{@code * ActiveOperationalDataset activeDataset = * new ActiveOperationalDataset.Builder(controller.createRandomizedDataset("MyNet")) * .setChannel(CHANNEL_PAGE_24_GHZ, 17) * .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now())) * .build(); * }* *
If the Active Operational Dataset is already known as Thread TLVs, you can simply use: * *
{@code * ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs); * }* * @hide */ @FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED) @SystemApi public final class ActiveOperationalDataset implements Parcelable { /** The maximum length of the Active Operational Dataset TLV array in bytes. */ public static final int LENGTH_MAX_DATASET_TLVS = 254; /** The length of Extended PAN ID in bytes. */ public static final int LENGTH_EXTENDED_PAN_ID = 8; /** The minimum length of Network Name as UTF-8 bytes. */ public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1; /** The maximum length of Network Name as UTF-8 bytes. */ public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16; /** The length of Network Key in bytes. */ public static final int LENGTH_NETWORK_KEY = 16; /** The length of Mesh-Local Prefix in bits. */ public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64; /** The length of PSKc in bytes. */ public static final int LENGTH_PSKC = 16; /** The 2.4 GHz channel page. */ public static final int CHANNEL_PAGE_24_GHZ = 0; /** The minimum 2.4GHz channel. */ public static final int CHANNEL_MIN_24_GHZ = 11; /** The maximum 2.4GHz channel. */ public static final int CHANNEL_MAX_24_GHZ = 26; /** @hide */ @VisibleForTesting public static final int TYPE_CHANNEL = 0; /** @hide */ @VisibleForTesting public static final int TYPE_PAN_ID = 1; /** @hide */ @VisibleForTesting public static final int TYPE_EXTENDED_PAN_ID = 2; /** @hide */ @VisibleForTesting public static final int TYPE_NETWORK_NAME = 3; /** @hide */ @VisibleForTesting public static final int TYPE_PSKC = 4; /** @hide */ @VisibleForTesting public static final int TYPE_NETWORK_KEY = 5; /** @hide */ @VisibleForTesting public static final int TYPE_MESH_LOCAL_PREFIX = 7; /** @hide */ @VisibleForTesting public static final int TYPE_SECURITY_POLICY = 12; /** @hide */ @VisibleForTesting public static final int TYPE_ACTIVE_TIMESTAMP = 14; /** @hide */ @VisibleForTesting public static final int TYPE_CHANNEL_MASK = 53; /** @hide */ public static final byte MESH_LOCAL_PREFIX_FIRST_BYTE = (byte) 0xfd; private static final int LENGTH_CHANNEL = 3; private static final int LENGTH_PAN_ID = 2; @NonNull public static final Creator
{@code tlvs} can be obtained from the value of a Thread Active Operational Dataset TLV
* (see the Thread
* specification for the definition) or the return value of {@link #toThreadTlvs}.
*
* @param tlvs a series of Thread TLVs which contain the Active Operational Dataset
* @return the decoded Active Operational Dataset
* @throws IllegalArgumentException if {@code tlvs} is malformed or the length is larger than
* {@link LENGTH_MAX_DATASET_TLVS}
*/
@NonNull
public static ActiveOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
requireNonNull(tlvs, "tlvs cannot be null");
if (tlvs.length > LENGTH_MAX_DATASET_TLVS) {
throw new IllegalArgumentException(
String.format(
"tlvs length exceeds max length %d (actual is %d)",
LENGTH_MAX_DATASET_TLVS, tlvs.length));
}
Builder builder = new Builder();
int i = 0;
while (i < tlvs.length) {
int type = tlvs[i++] & 0xff;
if (i >= tlvs.length) {
throw new IllegalArgumentException(
String.format(
"Found TLV type %d at end of operational dataset with length %d",
type, tlvs.length));
}
int length = tlvs[i++] & 0xff;
if (i + length > tlvs.length) {
throw new IllegalArgumentException(
String.format(
"Found TLV type %d with length %d which exceeds the remaining data"
+ " in the operational dataset with length %d",
type, length, tlvs.length));
}
initWithTlv(builder, type, Arrays.copyOfRange(tlvs, i, i + length));
i += length;
}
try {
return builder.build();
} catch (IllegalStateException e) {
throw new IllegalArgumentException(
"Failed to build the ActiveOperationalDataset object", e);
}
}
private static void initWithTlv(Builder builder, int type, byte[] value) {
// The max length of the dataset is 254 bytes, so the max length of a single TLV value is
// 252 (254 - 1 - 1)
if (value.length > LENGTH_MAX_DATASET_TLVS - 2) {
throw new IllegalArgumentException(
String.format(
"Length of TLV %d exceeds %d (actualLength = %d)",
(type & 0xff), LENGTH_MAX_DATASET_TLVS - 2, value.length));
}
switch (type) {
case TYPE_CHANNEL:
checkArgument(
value.length == LENGTH_CHANNEL,
"Invalid channel (length = %d, expectedLength = %d)",
value.length,
LENGTH_CHANNEL);
builder.setChannel((value[0] & 0xff), ((value[1] & 0xff) << 8) | (value[2] & 0xff));
break;
case TYPE_PAN_ID:
checkArgument(
value.length == LENGTH_PAN_ID,
"Invalid PAN ID (length = %d, expectedLength = %d)",
value.length,
LENGTH_PAN_ID);
builder.setPanId(((value[0] & 0xff) << 8) | (value[1] & 0xff));
break;
case TYPE_EXTENDED_PAN_ID:
builder.setExtendedPanId(value);
break;
case TYPE_NETWORK_NAME:
builder.setNetworkName(new String(value, UTF_8));
break;
case TYPE_PSKC:
builder.setPskc(value);
break;
case TYPE_NETWORK_KEY:
builder.setNetworkKey(value);
break;
case TYPE_MESH_LOCAL_PREFIX:
builder.setMeshLocalPrefix(value);
break;
case TYPE_SECURITY_POLICY:
builder.setSecurityPolicy(SecurityPolicy.fromTlvValue(value));
break;
case TYPE_ACTIVE_TIMESTAMP:
builder.setActiveTimestamp(OperationalDatasetTimestamp.fromTlvValue(value));
break;
case TYPE_CHANNEL_MASK:
builder.setChannelMask(decodeChannelMask(value));
break;
default:
builder.addUnknownTlv(type & 0xff, value);
break;
}
}
private static SparseArray See the Thread
* specification for the definition of the Thread TLV format.
*
* @return a series of Thread TLVs which contain this Active Operational Dataset
*/
@NonNull
public byte[] toThreadTlvs() {
ByteArrayOutputStream dataset = new ByteArrayOutputStream();
dataset.write(TYPE_ACTIVE_TIMESTAMP);
byte[] activeTimestampBytes = mActiveTimestamp.toTlvValue();
dataset.write(activeTimestampBytes.length);
dataset.write(activeTimestampBytes, 0, activeTimestampBytes.length);
dataset.write(TYPE_NETWORK_NAME);
byte[] networkNameBytes = mNetworkName.getBytes(UTF_8);
dataset.write(networkNameBytes.length);
dataset.write(networkNameBytes, 0, networkNameBytes.length);
dataset.write(TYPE_EXTENDED_PAN_ID);
dataset.write(mExtendedPanId.length);
dataset.write(mExtendedPanId, 0, mExtendedPanId.length);
dataset.write(TYPE_PAN_ID);
dataset.write(LENGTH_PAN_ID);
dataset.write(mPanId >> 8);
dataset.write(mPanId);
dataset.write(TYPE_CHANNEL);
dataset.write(LENGTH_CHANNEL);
dataset.write(mChannelPage);
dataset.write(mChannel >> 8);
dataset.write(mChannel);
encodeChannelMask(mChannelMask, dataset);
dataset.write(TYPE_PSKC);
dataset.write(mPskc.length);
dataset.write(mPskc, 0, mPskc.length);
dataset.write(TYPE_NETWORK_KEY);
dataset.write(mNetworkKey.length);
dataset.write(mNetworkKey, 0, mNetworkKey.length);
dataset.write(TYPE_MESH_LOCAL_PREFIX);
dataset.write(mMeshLocalPrefix.getPrefixLength() / 8);
dataset.write(mMeshLocalPrefix.getRawAddress(), 0, mMeshLocalPrefix.getPrefixLength() / 8);
dataset.write(TYPE_SECURITY_POLICY);
byte[] securityPolicyBytes = mSecurityPolicy.toTlvValue();
dataset.write(securityPolicyBytes.length);
dataset.write(securityPolicyBytes, 0, securityPolicyBytes.length);
for (int i = 0; i < mUnknownTlvs.size(); i++) {
byte[] value = mUnknownTlvs.valueAt(i);
dataset.write(mUnknownTlvs.keyAt(i));
dataset.write(value.length);
dataset.write(value, 0, value.length);
}
return dataset.toByteArray();
}
/** Returns the Active Timestamp. */
@NonNull
public OperationalDatasetTimestamp getActiveTimestamp() {
return mActiveTimestamp;
}
/** Returns the Network Name. */
@NonNull
@Size(min = LENGTH_MIN_NETWORK_NAME_BYTES, max = LENGTH_MAX_NETWORK_NAME_BYTES)
public String getNetworkName() {
return mNetworkName;
}
/** Returns the Extended PAN ID. */
@NonNull
@Size(LENGTH_EXTENDED_PAN_ID)
public byte[] getExtendedPanId() {
return mExtendedPanId.clone();
}
/** Returns the PAN ID. */
@IntRange(from = 0, to = 0xfffe)
public int getPanId() {
return mPanId;
}
/** Returns the Channel. */
@IntRange(from = 0, to = 65535)
public int getChannel() {
return mChannel;
}
/** Returns the Channel Page. */
@IntRange(from = 0, to = 255)
public int getChannelPage() {
return mChannelPage;
}
/**
* Returns the Channel masks. For the returned {@link SparseArray}, the key is the Channel Page
* and the value is the Channel Mask.
*/
@NonNull
@Size(min = 1)
public SparseArray An empty builder cannot build a new {@link ActiveOperationalDataset} object. The
* Active Operational Dataset parameters must be set with setters of this builder.
*/
public Builder() {
mChannelMask = new SparseArray<>();
mUnknownTlvs = new SparseArray<>();
}
/**
* Sets the Active Timestamp.
*
* @param activeTimestamp Active Timestamp of the Operational Dataset
*/
@NonNull
public Builder setActiveTimestamp(@NonNull OperationalDatasetTimestamp activeTimestamp) {
requireNonNull(activeTimestamp, "activeTimestamp cannot be null");
this.mActiveTimestamp = activeTimestamp;
return this;
}
/**
* Sets the Network Name.
*
* @param networkName the name of the Thread network
* @throws IllegalArgumentException if length of the UTF-8 representation of {@code
* networkName} isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
* #LENGTH_MAX_NETWORK_NAME_BYTES}]
*/
@NonNull
public Builder setNetworkName(
@NonNull
@Size(
min = LENGTH_MIN_NETWORK_NAME_BYTES,
max = LENGTH_MAX_NETWORK_NAME_BYTES)
String networkName) {
this.mNetworkName = checkNetworkName(networkName);
return this;
}
/**
* Sets the Extended PAN ID.
*
* Use with caution. A randomized Extended PAN ID should be used for real Thread
* networks. It's discouraged to call this method to override the default value created by
* {@link ThreadNetworkController#createRandomizedDataset} in production.
*
* @throws IllegalArgumentException if length of {@code extendedPanId} is not {@link
* #LENGTH_EXTENDED_PAN_ID}.
*/
@NonNull
public Builder setExtendedPanId(
@NonNull @Size(LENGTH_EXTENDED_PAN_ID) byte[] extendedPanId) {
requireNonNull(extendedPanId, "extendedPanId cannot be null");
checkArgument(
extendedPanId.length == LENGTH_EXTENDED_PAN_ID,
"Invalid extended PAN ID (length = %d, expectedLength = %d)",
extendedPanId.length,
LENGTH_EXTENDED_PAN_ID);
this.mExtendedPanId = extendedPanId.clone();
return this;
}
/**
* Sets the PAN ID.
*
* @throws IllegalArgumentException if {@code panId} is not in range of 0x0-0xfffe
*/
@NonNull
public Builder setPanId(@IntRange(from = 0, to = 0xfffe) int panId) {
checkArgument(
panId >= 0 && panId <= 0xfffe,
"PAN ID exceeds allowed range (panid = %d, allowedRange = [0x0, 0xffff])",
panId);
this.mPanId = panId;
return this;
}
/**
* Sets the Channel Page and Channel.
*
* Channel Pages other than {@link #CHANNEL_PAGE_24_GHZ} are undefined and may lead to
* unexpected behavior if it's applied to Thread devices.
*
* @throws IllegalArgumentException if invalid channel is specified for the {@code
* channelPage}
*/
@NonNull
public Builder setChannel(
@IntRange(from = 0, to = 255) int page,
@IntRange(from = 0, to = 65535) int channel) {
checkArgument(
page >= 0 && page <= 255,
"Invalid channel page (page = %d, allowedRange = [0, 255])",
page);
if (page == CHANNEL_PAGE_24_GHZ) {
checkArgument(
channel >= CHANNEL_MIN_24_GHZ && channel <= CHANNEL_MAX_24_GHZ,
"Invalid channel %d in page %d (allowedChannelRange = [%d, %d])",
channel,
page,
CHANNEL_MIN_24_GHZ,
CHANNEL_MAX_24_GHZ);
} else {
checkArgument(
channel >= 0 && channel <= 65535,
"Invalid channel %d in page %d "
+ "(channel = %d, allowedChannelRange = [0, 65535])",
channel,
page,
channel);
}
this.mChannelPage = page;
this.mChannel = channel;
return this;
}
/**
* Sets the Channel Mask.
*
* @throws IllegalArgumentException if {@code channelMask} is empty
*/
@NonNull
public Builder setChannelMask(@NonNull @Size(min = 1) SparseArray Use with caution. A randomly generated PSKc should be used for real Thread networks.
* It's discouraged to call this method to override the default value created by {@link
* ThreadNetworkController#createRandomizedDataset} in production.
*
* @param pskc the key stretched version of the Commissioning Credential for the network
* @throws IllegalArgumentException if length of {@code pskc} is not {@link #LENGTH_PSKC}
*/
@NonNull
public Builder setPskc(@NonNull @Size(LENGTH_PSKC) byte[] pskc) {
requireNonNull(pskc, "pskc cannot be null");
checkArgument(
pskc.length == LENGTH_PSKC,
"Invalid PSKc length (length = %d, expectedLength = %d)",
pskc.length,
LENGTH_PSKC);
this.mPskc = pskc.clone();
return this;
}
/**
* Sets the Network Key.
*
* Use with caution, randomly generated Network Key should be used for real Thread
* networks. It's discouraged to call this method to override the default value created by
* {@link ThreadNetworkController#createRandomizedDataset} in production.
*
* @param networkKey a 128-bit security key-derivation key for the Thread Network
* @throws IllegalArgumentException if length of {@code networkKey} is not {@link
* #LENGTH_NETWORK_KEY}
*/
@NonNull
public Builder setNetworkKey(@NonNull @Size(LENGTH_NETWORK_KEY) byte[] networkKey) {
requireNonNull(networkKey, "networkKey cannot be null");
checkArgument(
networkKey.length == LENGTH_NETWORK_KEY,
"Invalid network key length (length = %d, expectedLength = %d)",
networkKey.length,
LENGTH_NETWORK_KEY);
this.mNetworkKey = networkKey.clone();
return this;
}
/**
* Sets the Mesh-Local Prefix.
*
* @param meshLocalPrefix the prefix used for realm-local traffic within the mesh
* @throws IllegalArgumentException if prefix length of {@code meshLocalPrefix} isn't {@link
* #LENGTH_MESH_LOCAL_PREFIX_BITS} or {@code meshLocalPrefix} doesn't start with {@code
* 0xfd}
*/
@NonNull
public Builder setMeshLocalPrefix(@NonNull IpPrefix meshLocalPrefix) {
requireNonNull(meshLocalPrefix, "meshLocalPrefix cannot be null");
checkArgument(
meshLocalPrefix.getPrefixLength() == LENGTH_MESH_LOCAL_PREFIX_BITS,
"Invalid mesh-local prefix length (length = %d, expectedLength = %d)",
meshLocalPrefix.getPrefixLength(),
LENGTH_MESH_LOCAL_PREFIX_BITS);
checkArgument(
meshLocalPrefix.getRawAddress()[0] == MESH_LOCAL_PREFIX_FIRST_BYTE,
"Mesh-local prefix must start with 0xfd: " + meshLocalPrefix);
this.mMeshLocalPrefix = meshLocalPrefix;
return this;
}
/**
* Sets the Mesh-Local Prefix.
*
* @param meshLocalPrefix the prefix used for realm-local traffic within the mesh
* @throws IllegalArgumentException if {@code meshLocalPrefix} doesn't start with {@code
* 0xfd} or has length other than {@code LENGTH_MESH_LOCAL_PREFIX_BITS / 8}
* @hide
*/
@NonNull
public Builder setMeshLocalPrefix(byte[] meshLocalPrefix) {
final int prefixLength = meshLocalPrefix.length * 8;
checkArgument(
prefixLength == LENGTH_MESH_LOCAL_PREFIX_BITS,
"Invalid mesh-local prefix length (length = %d, expectedLength = %d)",
prefixLength,
LENGTH_MESH_LOCAL_PREFIX_BITS);
byte[] ip6RawAddress = new byte[16];
System.arraycopy(meshLocalPrefix, 0, ip6RawAddress, 0, meshLocalPrefix.length);
try {
return setMeshLocalPrefix(
new IpPrefix(Inet6Address.getByAddress(ip6RawAddress), prefixLength));
} catch (UnknownHostException e) {
// Can't happen because numeric address is provided
throw new AssertionError(e);
}
}
/** Sets the Security Policy. */
@NonNull
public Builder setSecurityPolicy(@NonNull SecurityPolicy securityPolicy) {
requireNonNull(securityPolicy, "securityPolicy cannot be null");
this.mSecurityPolicy = securityPolicy;
return this;
}
/**
* Sets additional unknown TLVs.
*
* @hide
*/
@NonNull
public Builder setUnknownTlvs(@NonNull SparseArray