Merge "Fill ConnectivityService in testDumpDoesNotCrash()" into main
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 246e5bc..f275d49 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -33,6 +33,7 @@
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
@@ -56,7 +57,6 @@
import android.net.dhcp.IDhcpServer;
import android.net.ip.RouterAdvertisementDaemon.RaParams;
import android.os.Handler;
-import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
@@ -66,9 +66,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.MessageUtils;
import com.android.internal.util.State;
-import com.android.internal.util.StateMachine;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetdUtils;
@@ -85,12 +85,15 @@
import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
+import com.android.networkstack.tethering.util.StateMachineShim;
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -104,7 +107,7 @@
*
* @hide
*/
-public class IpServer extends StateMachine {
+public class IpServer extends StateMachineShim {
public static final int STATE_UNAVAILABLE = 0;
public static final int STATE_AVAILABLE = 1;
public static final int STATE_TETHERED = 2;
@@ -236,6 +239,7 @@
public static final int CMD_NEW_PREFIX_REQUEST = BASE_IPSERVER + 12;
// request from PrivateAddressCoordinator to restart tethering.
public static final int CMD_NOTIFY_PREFIX_CONFLICT = BASE_IPSERVER + 13;
+ public static final int CMD_SERVICE_FAILED_TO_START = BASE_IPSERVER + 14;
private final State mInitialState;
private final State mLocalHotspotState;
@@ -297,6 +301,8 @@
private int mLastIPv6UpstreamIfindex = 0;
private boolean mUpstreamSupportsBpf = false;
+ @NonNull
+ private Set<IpPrefix> mLastIPv6UpstreamPrefixes = Collections.emptySet();
private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer {
public void accept(NeighborEvent e) {
@@ -309,16 +315,18 @@
private LinkAddress mIpv4Address;
private final TetheringMetrics mTetheringMetrics;
+ private final Handler mHandler;
// TODO: Add a dependency object to pass the data members or variables from the tethering
// object. It helps to reduce the arguments of the constructor.
public IpServer(
- String ifaceName, Looper looper, int interfaceType, SharedLog log,
+ String ifaceName, Handler handler, int interfaceType, SharedLog log,
INetd netd, @NonNull BpfCoordinator bpfCoordinator,
@Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
TetheringMetrics tetheringMetrics, Dependencies deps) {
- super(ifaceName, looper);
+ super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
+ mHandler = handler;
mLog = log.forSubComponent(ifaceName);
mNetd = netd;
mBpfCoordinator = bpfCoordinator;
@@ -351,13 +359,22 @@
mTetheredState = new TetheredState();
mUnavailableState = new UnavailableState();
mWaitingForRestartState = new WaitingForRestartState();
- addState(mInitialState);
- addState(mLocalHotspotState);
- addState(mTetheredState);
- addState(mWaitingForRestartState, mTetheredState);
- addState(mUnavailableState);
+ final ArrayList allStates = new ArrayList<StateInfo>();
+ allStates.add(new StateInfo(mInitialState, null));
+ allStates.add(new StateInfo(mLocalHotspotState, null));
+ allStates.add(new StateInfo(mTetheredState, null));
+ allStates.add(new StateInfo(mWaitingForRestartState, mTetheredState));
+ allStates.add(new StateInfo(mUnavailableState, null));
+ addAllStates(allStates);
+ }
- setInitialState(mInitialState);
+ private Handler getHandler() {
+ return mHandler;
+ }
+
+ /** Start IpServer state machine. */
+ public void start() {
+ start(mInitialState);
}
/** Interface name which IpServer served.*/
@@ -498,7 +515,12 @@
private void handleError() {
mLastError = TETHER_ERROR_DHCPSERVER_ERROR;
- transitionTo(mInitialState);
+ if (USE_SYNC_SM) {
+ sendMessage(CMD_SERVICE_FAILED_TO_START, TETHER_ERROR_DHCPSERVER_ERROR);
+ } else {
+ sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START,
+ TETHER_ERROR_DHCPSERVER_ERROR);
+ }
}
}
@@ -770,13 +792,8 @@
if (params.hasDefaultRoute) params.hopLimit = getHopLimit(upstreamIface, ttlAdjustment);
- for (LinkAddress linkAddr : v6only.getLinkAddresses()) {
- if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
-
- final IpPrefix prefix = new IpPrefix(
- linkAddr.getAddress(), linkAddr.getPrefixLength());
- params.prefixes.add(prefix);
-
+ params.prefixes = getTetherableIpv6Prefixes(v6only);
+ for (IpPrefix prefix : params.prefixes) {
final Inet6Address dnsServer = getLocalDnsIpFor(prefix);
if (dnsServer != null) {
params.dnses.add(dnsServer);
@@ -796,9 +813,12 @@
// Not support BPF on virtual upstream interface
final boolean upstreamSupportsBpf = upstreamIface != null && !isVcnInterface(upstreamIface);
- updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfIndex, upstreamSupportsBpf);
+ final Set<IpPrefix> upstreamPrefixes = params != null ? params.prefixes : Set.of();
+ updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamPrefixes,
+ upstreamIfIndex, upstreamPrefixes, upstreamSupportsBpf);
mLastIPv6LinkProperties = v6only;
mLastIPv6UpstreamIfindex = upstreamIfIndex;
+ mLastIPv6UpstreamPrefixes = upstreamPrefixes;
mUpstreamSupportsBpf = upstreamSupportsBpf;
if (mDadProxy != null) {
mDadProxy.setUpstreamIface(upstreamIfaceParams);
@@ -916,14 +936,17 @@
return supportsBpf ? ifindex : NO_UPSTREAM;
}
- // Handles updates to IPv6 forwarding rules if the upstream changes.
- private void updateIpv6ForwardingRules(int prevUpstreamIfindex, int upstreamIfindex,
- boolean upstreamSupportsBpf) {
+ // Handles updates to IPv6 forwarding rules if the upstream or its prefixes change.
+ private void updateIpv6ForwardingRules(int prevUpstreamIfindex,
+ @NonNull Set<IpPrefix> prevUpstreamPrefixes, int upstreamIfindex,
+ @NonNull Set<IpPrefix> upstreamPrefixes, boolean upstreamSupportsBpf) {
// If the upstream interface has changed, remove all rules and re-add them with the new
// upstream interface. If upstream is a virtual network, treated as no upstream.
- if (prevUpstreamIfindex != upstreamIfindex) {
+ if (prevUpstreamIfindex != upstreamIfindex
+ || !prevUpstreamPrefixes.equals(upstreamPrefixes)) {
mBpfCoordinator.updateAllIpv6Rules(this, this.mInterfaceParams,
- getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf));
+ getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf),
+ upstreamPrefixes);
}
}
@@ -1108,7 +1131,19 @@
startServingInterface();
if (mLastError != TETHER_ERROR_NO_ERROR) {
- transitionTo(mInitialState);
+ // This will transition to InitialState right away, regardless of whether any
+ // message is already waiting in the StateMachine queue (including maybe some
+ // message to go to InitialState). InitialState will then process any pending
+ // message (and generally ignores them). It is difficult to know for sure whether
+ // this is correct in all cases, but this is equivalent to what IpServer was doing
+ // in previous versions of the mainline module.
+ // TODO : remove sendMessageAtFrontOfQueueToAsyncSM after migrating to the Sync
+ // StateMachine.
+ if (USE_SYNC_SM) {
+ sendSelfMessageToSyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
+ } else {
+ sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
+ }
}
if (DBG) Log.d(TAG, getStateString(mDesiredInterfaceState) + " serve " + mIfaceName);
@@ -1198,6 +1233,9 @@
mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
transitionTo(mWaitingForRestartState);
break;
+ case CMD_SERVICE_FAILED_TO_START:
+ mLog.e("start serving fail, error: " + message.arg1);
+ transitionTo(mInitialState);
default:
return false;
}
@@ -1328,7 +1366,7 @@
for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
mUpstreamIfaceSet = null;
mBpfCoordinator.updateAllIpv6Rules(
- IpServer.this, IpServer.this.mInterfaceParams, NO_UPSTREAM);
+ IpServer.this, IpServer.this.mInterfaceParams, NO_UPSTREAM, Set.of());
}
private void cleanupUpstreamInterface(String upstreamIface) {
@@ -1515,4 +1553,21 @@
}
return random;
}
+
+ /** Get IPv6 prefixes from LinkProperties */
+ @NonNull
+ @VisibleForTesting
+ static HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull Collection<LinkAddress> addrs) {
+ final HashSet<IpPrefix> prefixes = new HashSet<>();
+ for (LinkAddress linkAddr : addrs) {
+ if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
+ prefixes.add(new IpPrefix(linkAddr.getAddress(), RFC7421_PREFIX_LENGTH));
+ }
+ return prefixes;
+ }
+
+ @NonNull
+ private HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull LinkProperties lp) {
+ return getTetherableIpv6Prefixes(lp.getLinkAddresses());
+ }
}
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 46c815f..2b14a42 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -28,6 +28,7 @@
import static android.system.OsConstants.ETH_P_IPV6;
import static com.android.net.module.util.NetworkStackConstants.IPV4_MIN_MTU;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
import static com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
@@ -90,7 +91,6 @@
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -123,7 +123,6 @@
private static final int DUMP_TIMEOUT_MS = 10_000;
private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString(
"00:00:00:00:00:00");
- private static final IpPrefix IPV6_ZERO_PREFIX64 = new IpPrefix("::/64");
private static final String TETHER_DOWNSTREAM4_MAP_PATH = makeMapPath(DOWNSTREAM, 4);
private static final String TETHER_UPSTREAM4_MAP_PATH = makeMapPath(UPSTREAM, 4);
private static final String TETHER_DOWNSTREAM6_FS_PATH = makeMapPath(DOWNSTREAM, 6);
@@ -768,7 +767,8 @@
* Note that this can be only called on handler thread.
*/
public void updateAllIpv6Rules(@NonNull final IpServer ipServer,
- final InterfaceParams interfaceParams, int newUpstreamIfindex) {
+ final InterfaceParams interfaceParams, int newUpstreamIfindex,
+ @NonNull final Set<IpPrefix> newUpstreamPrefixes) {
if (!isUsingBpf()) return;
// Remove IPv6 downstream rules. Remove the old ones before adding the new rules, otherwise
@@ -791,9 +791,11 @@
// Add new upstream rules.
if (newUpstreamIfindex != 0 && interfaceParams != null && interfaceParams.macAddr != null) {
- addIpv6UpstreamRule(ipServer, new Ipv6UpstreamRule(
- newUpstreamIfindex, interfaceParams.index, IPV6_ZERO_PREFIX64,
- interfaceParams.macAddr, NULL_MAC_ADDRESS, NULL_MAC_ADDRESS));
+ for (final IpPrefix ipPrefix : newUpstreamPrefixes) {
+ addIpv6UpstreamRule(ipServer, new Ipv6UpstreamRule(
+ newUpstreamIfindex, interfaceParams.index, ipPrefix,
+ interfaceParams.macAddr, NULL_MAC_ADDRESS, NULL_MAC_ADDRESS));
+ }
}
// Add updated downstream rules.
@@ -1256,10 +1258,24 @@
pw.decreaseIndent();
}
+ private IpPrefix longToPrefix(long ip64) {
+ final ByteBuffer prefixBuffer = ByteBuffer.allocate(IPV6_ADDR_LEN);
+ prefixBuffer.putLong(ip64);
+ IpPrefix sourcePrefix;
+ try {
+ sourcePrefix = new IpPrefix(InetAddress.getByAddress(prefixBuffer.array()), 64);
+ } catch (UnknownHostException e) {
+ // Cannot happen. InetAddress.getByAddress can only throw an exception if the byte array
+ // is the wrong length, but we allocate it with fixed length IPV6_ADDR_LEN.
+ throw new IllegalArgumentException("Invalid IPv6 address");
+ }
+ return sourcePrefix;
+ }
+
private String ipv6UpstreamRuleToString(TetherUpstream6Key key, Tether6Value value) {
- return String.format("%d(%s) [%s] -> %d(%s) %04x [%s] [%s]",
- key.iif, getIfName(key.iif), key.dstMac, value.oif, getIfName(value.oif),
- value.ethProto, value.ethSrcMac, value.ethDstMac);
+ return String.format("%d(%s) [%s] [%s] -> %d(%s) %04x [%s] [%s]",
+ key.iif, getIfName(key.iif), key.dstMac, longToPrefix(key.src64), value.oif,
+ getIfName(value.oif), value.ethProto, value.ethSrcMac, value.ethDstMac);
}
private void dumpIpv6UpstreamRules(IndentingPrintWriter pw) {
@@ -1309,8 +1325,8 @@
// TODO: use dump utils with headerline and lambda which prints key and value to reduce
// duplicate bpf map dump code.
private void dumpBpfForwardingRulesIpv6(IndentingPrintWriter pw) {
- pw.println("IPv6 Upstream: iif(iface) [inDstMac] -> oif(iface) etherType [outSrcMac] "
- + "[outDstMac]");
+ pw.println("IPv6 Upstream: iif(iface) [inDstMac] [sourcePrefix] -> oif(iface) etherType "
+ + "[outSrcMac] [outDstMac]");
pw.increaseIndent();
dumpIpv6UpstreamRules(pw);
pw.decreaseIndent();
@@ -1554,8 +1570,7 @@
*/
@NonNull
public TetherUpstream6Key makeTetherUpstream6Key() {
- byte[] prefixBytes = Arrays.copyOf(sourcePrefix.getRawAddress(), 8);
- long prefix64 = ByteBuffer.wrap(prefixBytes).order(ByteOrder.BIG_ENDIAN).getLong();
+ long prefix64 = ByteBuffer.wrap(sourcePrefix.getRawAddress()).getLong();
return new TetherUpstream6Key(downstreamIfindex, inDstMac, prefix64);
}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index f52bed9..996ee11 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -1687,6 +1687,8 @@
static final int EVENT_IFACE_UPDATE_LINKPROPERTIES = BASE_MAIN_SM + 7;
// Events from EntitlementManager to choose upstream again.
static final int EVENT_UPSTREAM_PERMISSION_CHANGED = BASE_MAIN_SM + 8;
+ // Internal request from IpServer to enable or disable downstream.
+ static final int EVENT_REQUEST_CHANGE_DOWNSTREAM = BASE_MAIN_SM + 9;
private final State mInitialState;
private final State mTetherModeAliveState;
@@ -2186,6 +2188,12 @@
}
break;
}
+ case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
+ final int tetheringType = message.arg1;
+ final Boolean enabled = (Boolean) message.obj;
+ enableTetheringInternal(tetheringType, enabled, null);
+ break;
+ }
default:
retValue = false;
break;
@@ -2743,7 +2751,8 @@
@Override
public void requestEnableTethering(int tetheringType, boolean enabled) {
- enableTetheringInternal(tetheringType, enabled, null);
+ mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
+ tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
}
};
}
@@ -2841,7 +2850,7 @@
mLog.i("adding IpServer for: " + iface);
final TetherState tetherState = new TetherState(
- new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
+ new IpServer(iface, mHandler, interfaceType, mLog, mNetd, mBpfCoordinator,
mRoutingCoordinator, makeControlCallback(), mConfig,
mPrivateAddressCoordinator, mTetheringMetrics,
mDeps.getIpServerDependencies()), isNcm);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 747cc20..502fee8 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -136,6 +136,9 @@
*/
public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
+ /** A flag for using synchronous or asynchronous state machine. */
+ public static final boolean USE_SYNC_SM = false;
+
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWigigRegexs;
diff --git a/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
new file mode 100644
index 0000000..078a35f
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
@@ -0,0 +1,196 @@
+/*
+ * 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 com.android.networkstack.tethering.util;
+
+import android.annotation.Nullable;
+import android.os.Looper;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
+
+import java.util.List;
+
+/** A wrapper to decide whether use synchronous state machine for tethering. */
+public class StateMachineShim {
+ // Exactly one of mAsyncSM or mSyncSM is non-null.
+ private final AsyncStateMachine mAsyncSM;
+ private final SyncStateMachine mSyncSM;
+
+ /**
+ * The Looper parameter is only needed for AsyncSM, so if looper is null, the shim will be
+ * created for SyncSM.
+ */
+ public StateMachineShim(final String name, @Nullable final Looper looper) {
+ this(name, looper, new Dependencies());
+ }
+
+ @VisibleForTesting
+ public StateMachineShim(final String name, @Nullable final Looper looper,
+ final Dependencies deps) {
+ if (looper == null) {
+ mAsyncSM = null;
+ mSyncSM = deps.makeSyncStateMachine(name, Thread.currentThread());
+ } else {
+ mAsyncSM = deps.makeAsyncStateMachine(name, looper);
+ mSyncSM = null;
+ }
+ }
+
+ /** A dependencies class which used for testing injection. */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Create SyncSM instance, for injection. */
+ public SyncStateMachine makeSyncStateMachine(final String name, final Thread thread) {
+ return new SyncStateMachine(name, thread);
+ }
+
+ /** Create AsyncSM instance, for injection. */
+ public AsyncStateMachine makeAsyncStateMachine(final String name, final Looper looper) {
+ return new AsyncStateMachine(name, looper);
+ }
+ }
+
+ /** Start the state machine */
+ public void start(final State initialState) {
+ if (mSyncSM != null) {
+ mSyncSM.start(initialState);
+ } else {
+ mAsyncSM.setInitialState(initialState);
+ mAsyncSM.start();
+ }
+ }
+
+ /** Add states to state machine. */
+ public void addAllStates(final List<StateInfo> stateInfos) {
+ if (mSyncSM != null) {
+ mSyncSM.addAllStates(stateInfos);
+ } else {
+ for (final StateInfo info : stateInfos) {
+ mAsyncSM.addState(info.state, info.parent);
+ }
+ }
+ }
+
+ /**
+ * Transition to given state.
+ *
+ * SyncSM doesn't allow this be called during state transition (#enter() or #exit() methods),
+ * or multiple times while processing a single message.
+ */
+ public void transitionTo(final State state) {
+ if (mSyncSM != null) {
+ mSyncSM.transitionTo(state);
+ } else {
+ mAsyncSM.transitionTo(state);
+ }
+ }
+
+ /** Send message to state machine. */
+ public void sendMessage(int what) {
+ sendMessage(what, 0, 0, null);
+ }
+
+ /** Send message to state machine. */
+ public void sendMessage(int what, Object obj) {
+ sendMessage(what, 0, 0, obj);
+ }
+
+ /** Send message to state machine. */
+ public void sendMessage(int what, int arg1) {
+ sendMessage(what, arg1, 0, null);
+ }
+
+ /**
+ * Send message to state machine.
+ *
+ * If using asynchronous state machine, putting the message into looper's message queue.
+ * Tethering runs on single looper thread that ipServers and mainSM all share with same message
+ * queue. The enqueued message will be processed by asynchronous state machine when all the
+ * messages before such enqueued message are processed.
+ * If using synchronous state machine, the message is processed right away without putting into
+ * looper's message queue.
+ */
+ public void sendMessage(int what, int arg1, int arg2, Object obj) {
+ if (mSyncSM != null) {
+ mSyncSM.processMessage(what, arg1, arg2, obj);
+ } else {
+ mAsyncSM.sendMessage(what, arg1, arg2, obj);
+ }
+ }
+
+ /**
+ * Send message after delayMillis millisecond.
+ *
+ * This can only be used with async state machine, so this will throw if using sync state
+ * machine.
+ */
+ public void sendMessageDelayedToAsyncSM(final int what, final long delayMillis) {
+ if (mSyncSM != null) {
+ throw new IllegalStateException("sendMessageDelayed can only be used with async SM");
+ }
+
+ mAsyncSM.sendMessageDelayed(what, delayMillis);
+ }
+
+ /**
+ * Enqueue a message to the front of the queue.
+ * Protected, may only be called by instances of async state machine.
+ *
+ * Message is ignored if state machine has quit.
+ */
+ protected void sendMessageAtFrontOfQueueToAsyncSM(int what, int arg1) {
+ if (mSyncSM != null) {
+ throw new IllegalStateException("sendMessageAtFrontOfQueue can only be used with"
+ + " async SM");
+ }
+
+ mAsyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1);
+ }
+
+ /**
+ * Send self message.
+ * This can only be used with sync state machine, so this will throw if using async state
+ * machine.
+ */
+ public void sendSelfMessageToSyncSM(final int what, final Object obj) {
+ if (mSyncSM == null) {
+ throw new IllegalStateException("sendSelfMessage can only be used with sync SM");
+ }
+
+ mSyncSM.sendSelfMessage(what, 0, 0, obj);
+ }
+
+ /**
+ * An alias StateMahchine class with public construtor.
+ *
+ * Since StateMachine.java only provides protected construtor, adding a child class so that this
+ * shim could create StateMachine instance.
+ */
+ @VisibleForTesting
+ public static class AsyncStateMachine extends StateMachine {
+ public AsyncStateMachine(final String name, final Looper looper) {
+ super(name, looper);
+ }
+
+ /** Enqueue a message to the front of the queue for this state machine. */
+ public void sendMessageAtFrontOfQueueToAsyncSM(int what, int arg1) {
+ sendMessageAtFrontOfQueue(what, arg1);
+ }
+ }
+}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index fc9928d..08fca4a 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -34,6 +34,7 @@
import static android.net.ip.IpServer.STATE_LOCAL_ONLY;
import static android.net.ip.IpServer.STATE_TETHERED;
import static android.net.ip.IpServer.STATE_UNAVAILABLE;
+import static android.net.ip.IpServer.getTetherableIpv6Prefixes;
import static android.system.OsConstants.ETH_P_IPV6;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
@@ -95,6 +96,8 @@
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -145,12 +148,15 @@
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.verification.VerificationMode;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
+import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -188,6 +194,18 @@
private final LinkAddress mTestAddress = new LinkAddress("192.168.42.5/24");
private final IpPrefix mBluetoothPrefix = new IpPrefix("192.168.44.0/24");
+ private static final Set<LinkAddress> NO_ADDRESSES = Set.of();
+ private static final Set<IpPrefix> NO_PREFIXES = Set.of();
+ private static final Set<LinkAddress> UPSTREAM_ADDRESSES =
+ Set.of(new LinkAddress("2001:db8:0:1234::168/64"));
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES =
+ Set.of(new IpPrefix("2001:db8:0:1234::/64"));
+ private static final Set<LinkAddress> UPSTREAM_ADDRESSES2 = Set.of(
+ new LinkAddress("2001:db8:0:1234::168/64"),
+ new LinkAddress("2001:db8:0:abcd::168/64"));
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES2 = Set.of(
+ new IpPrefix("2001:db8:0:1234::/64"), new IpPrefix("2001:db8:0:abcd::/64"));
+
@Mock private INetd mNetd;
@Mock private IpServer.Callback mCallback;
@Mock private SharedLog mSharedLog;
@@ -214,7 +232,8 @@
@Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
- private final TestLooper mLooper = new TestLooper();
+ private TestLooper mLooper;
+ private Handler mHandler;
private final ArgumentCaptor<LinkProperties> mLinkPropertiesCaptor =
ArgumentCaptor.forClass(LinkProperties.class);
private IpServer mIpServer;
@@ -251,12 +270,7 @@
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
- // Recreate mBpfCoordinator again here because mTetherConfig has changed
- mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
- mIpServer = new IpServer(
- IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
- mRoutingCoordinatorManager, mCallback, mTetherConfig, mAddressCoordinator,
- mTetheringMetrics, mDependencies);
+ mIpServer = createIpServer(interfaceType);
mIpServer.start();
mNeighborEventConsumer = neighborCaptor.getValue();
@@ -270,24 +284,27 @@
private void initTetheredStateMachine(int interfaceType, String upstreamIface)
throws Exception {
- initTetheredStateMachine(interfaceType, upstreamIface, false,
+ initTetheredStateMachine(interfaceType, upstreamIface, NO_ADDRESSES, false,
DEFAULT_USING_BPF_OFFLOAD);
}
private void initTetheredStateMachine(int interfaceType, String upstreamIface,
- boolean usingLegacyDhcp, boolean usingBpfOffload) throws Exception {
+ Set<LinkAddress> upstreamAddresses, boolean usingLegacyDhcp, boolean usingBpfOffload)
+ throws Exception {
initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
if (upstreamIface != null) {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(upstreamIface);
+ lp.setLinkAddresses(upstreamAddresses);
dispatchTetherConnectionChanged(upstreamIface, lp, 0);
- if (usingBpfOffload) {
+ if (usingBpfOffload && !lp.getLinkAddresses().isEmpty()) {
+ Set<IpPrefix> upstreamPrefixes = getTetherableIpv6Prefixes(lp.getLinkAddresses());
InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
assertNotNull("missing upstream interface: " + upstreamIface, interfaceParams);
verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, interfaceParams.index);
- verifyStartUpstreamIpv6Forwarding(null, interfaceParams.index);
+ mIpServer, TEST_IFACE_PARAMS, interfaceParams.index, upstreamPrefixes);
+ verifyStartUpstreamIpv6Forwarding(null, interfaceParams.index, upstreamPrefixes);
} else {
verifyNoUpstreamIpv6ForwardingChange(null);
}
@@ -321,10 +338,17 @@
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
+ setUpDhcpServer();
+ }
+
+ // In order to interact with syncSM from the test, IpServer must be created in test thread.
+ private IpServer createIpServer(final int interfaceType) {
+ mLooper = new TestLooper();
+ mHandler = new Handler(mLooper.getLooper());
mBpfDeps = new BpfCoordinator.Dependencies() {
@NonNull
public Handler getHandler() {
- return new Handler(mLooper.getLooper());
+ return mHandler;
}
@NonNull
@@ -393,18 +417,19 @@
return mBpfErrorMap;
}
};
- mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
- setUpDhcpServer();
+ mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
+ return new IpServer(IFACE_NAME, mHandler, interfaceType, mSharedLog, mNetd, mBpfCoordinator,
+ mRoutingCoordinatorManager, mCallback, mTetherConfig, mAddressCoordinator,
+ mTetheringMetrics, mDependencies);
+
}
@Test
- public void startsOutAvailable() {
+ public void startsOutAvailable() throws Exception {
when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
.thenReturn(mIpNeighborMonitor);
- mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
- mNetd, mBpfCoordinator, mRoutingCoordinatorManager, mCallback, mTetherConfig,
- mAddressCoordinator, mTetheringMetrics, mDependencies);
+ mIpServer = createIpServer(TETHERING_BLUETOOTH);
mIpServer.start();
mLooper.dispatchAll();
verify(mCallback).updateInterfaceState(
@@ -646,10 +671,7 @@
inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM);
- if (!mBpfDeps.isAtLeastS()) {
- inOrder.verify(mNetd).tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX);
- }
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
// When tethering stops, upstream interface is set to zero and thus clearing all upstream
// rules. Downstream rules are needed to be cleared explicitly by calling
// BpfCoordinator#clearAllIpv6Rules in TetheredState#exit.
@@ -839,8 +861,8 @@
@Test
public void doesNotStartDhcpServerIfDisabled() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, true /* usingLegacyDhcp */,
- DEFAULT_USING_BPF_OFFLOAD);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, NO_ADDRESSES,
+ true /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
verify(mDependencies, never()).makeDhcpServer(any(), any(), any());
@@ -945,11 +967,19 @@
TEST_IFACE_PARAMS.macAddr, ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
}
+ private static long prefixToLong(IpPrefix prefix) {
+ return ByteBuffer.wrap(prefix.getRawAddress()).getLong();
+ }
+
private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ return verifyWithOrder(inOrder, t, times(1));
+ }
+
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t, VerificationMode mode) {
if (inOrder != null) {
- return inOrder.verify(t);
+ return inOrder.verify(t, mode);
} else {
- return verify(t);
+ return verify(t, mode);
}
}
@@ -1009,23 +1039,49 @@
}
}
- private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex)
- throws Exception {
+ private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex,
+ @NonNull Set<IpPrefix> upstreamPrefixes) throws Exception {
if (!mBpfDeps.isAtLeastS()) return;
- final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
- TEST_IFACE_PARAMS.macAddr, 0);
- final Tether6Value value = new Tether6Value(upstreamIfindex,
- MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
- ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
- verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
+ ArrayMap<TetherUpstream6Key, Tether6Value> expected = new ArrayMap<>();
+ for (IpPrefix upstreamPrefix : upstreamPrefixes) {
+ long prefix64 = prefixToLong(upstreamPrefix);
+ final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
+ TEST_IFACE_PARAMS.macAddr, prefix64);
+ final Tether6Value value = new Tether6Value(upstreamIfindex,
+ MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
+ ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
+ expected.put(key, value);
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ ArgumentCaptor<Tether6Value> valueCaptor =
+ ArgumentCaptor.forClass(Tether6Value.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).insertEntry(
+ keyCaptor.capture(), valueCaptor.capture());
+ List<TetherUpstream6Key> keys = keyCaptor.getAllValues();
+ List<Tether6Value> values = valueCaptor.getAllValues();
+ ArrayMap<TetherUpstream6Key, Tether6Value> captured = new ArrayMap<>();
+ for (int i = 0; i < keys.size(); i++) {
+ captured.put(keys.get(i), values.get(i));
+ }
+ assertEquals(expected, captured);
}
- private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder)
- throws Exception {
+ private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder,
+ @NonNull Set<IpPrefix> upstreamPrefixes) throws Exception {
if (!mBpfDeps.isAtLeastS()) return;
- final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
- TEST_IFACE_PARAMS.macAddr, 0);
- verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
+ Set<TetherUpstream6Key> expected = new ArraySet<>();
+ for (IpPrefix upstreamPrefix : upstreamPrefixes) {
+ long prefix64 = prefixToLong(upstreamPrefix);
+ final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
+ TEST_IFACE_PARAMS.macAddr, prefix64);
+ expected.add(key);
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).deleteEntry(
+ keyCaptor.capture());
+ assertEquals(expected, new ArraySet(keyCaptor.getAllValues()));
}
private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
@@ -1066,8 +1122,8 @@
@Test
public void addRemoveipv6ForwardingRules() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- DEFAULT_USING_BPF_OFFLOAD);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
final int myIfindex = TEST_IFACE_PARAMS.index;
final int notMyIfindex = myIfindex - 1;
@@ -1128,7 +1184,7 @@
UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macNull);
resetNetdBpfMapAndCoordinator();
- // Upstream changes result in updating the rules.
+ // Upstream interface changes result in updating the rules.
recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
resetNetdBpfMapAndCoordinator();
@@ -1136,14 +1192,36 @@
InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(UPSTREAM_IFACE2);
+ lp.setLinkAddresses(UPSTREAM_ADDRESSES);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
- verify(mBpfCoordinator).updateAllIpv6Rules(mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
verifyTetherOffloadRuleRemove(inOrder,
UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
verifyTetherOffloadRuleRemove(inOrder,
UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
- verifyStopUpstreamIpv6Forwarding(inOrder);
- verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2);
+ verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES);
+ verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
+ verifyTetherOffloadRuleAdd(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
+ verifyTetherOffloadRuleAdd(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
+ verifyNoUpstreamIpv6ForwardingChange(inOrder);
+ resetNetdBpfMapAndCoordinator();
+
+ // Upstream link addresses change result in updating the rules.
+ LinkProperties lp2 = new LinkProperties();
+ lp2.setInterfaceName(UPSTREAM_IFACE2);
+ lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, -1);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+ verifyTetherOffloadRuleRemove(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
+ verifyTetherOffloadRuleRemove(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
+ verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES);
+ verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
verifyTetherOffloadRuleAdd(inOrder,
UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
verifyTetherOffloadRuleAdd(inOrder,
@@ -1157,8 +1235,8 @@
// - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost.
// See dispatchTetherConnectionChanged.
verify(mBpfCoordinator, times(2)).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM);
- verifyStopUpstreamIpv6Forwarding(inOrder);
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES2);
verifyTetherOffloadRuleRemove(null,
UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
verifyTetherOffloadRuleRemove(null,
@@ -1188,7 +1266,7 @@
// with an upstream of NO_UPSTREAM are reapplied.
lp.setInterfaceName(UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verify(mBpfCoordinator).addIpv6DownstreamRule(
mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
verifyTetherOffloadRuleAdd(null,
@@ -1202,16 +1280,17 @@
// If upstream IPv6 connectivity is lost, rules are removed.
resetNetdBpfMapAndCoordinator();
dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
- verify(mBpfCoordinator).updateAllIpv6Rules(mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
verifyTetherOffloadRuleRemove(null,
UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
- verifyStopUpstreamIpv6Forwarding(null);
+ verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
// When upstream IPv6 connectivity comes back, upstream rules are added and downstream rules
// are reapplied.
lp.setInterfaceName(UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verify(mBpfCoordinator).addIpv6DownstreamRule(
mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
verifyTetherOffloadRuleAdd(null,
@@ -1226,7 +1305,7 @@
mIpServer.stop();
mLooper.dispatchAll();
verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
- verifyStopUpstreamIpv6Forwarding(null);
+ verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
verifyTetherOffloadRuleRemove(null,
UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
verifyTetherOffloadRuleRemove(null,
@@ -1251,8 +1330,8 @@
// [1] Enable BPF offload.
// A neighbor that is added or deleted causes the rule to be added or removed.
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- true /* usingBpfOffload */);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, true /* usingBpfOffload */);
resetNetdBpfMapAndCoordinator();
recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
@@ -1272,15 +1351,17 @@
// Upstream IPv6 connectivity change causes upstream rules change.
LinkProperties lp2 = new LinkProperties();
lp2.setInterfaceName(UPSTREAM_IFACE2);
+ lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, 0);
- verify(mBpfCoordinator).updateAllIpv6Rules(mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX2);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
resetNetdBpfMapAndCoordinator();
// [2] Disable BPF offload.
// A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- false /* usingBpfOffload */);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, false /* usingBpfOffload */);
resetNetdBpfMapAndCoordinator();
recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
@@ -1300,8 +1381,8 @@
@Test
public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- false /* usingBpfOffload */);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, false /* usingBpfOffload */);
// IP neighbor monitor doesn't start if BPF offload is disabled.
verify(mIpNeighborMonitor, never()).start();
@@ -1583,8 +1664,8 @@
// TODO: move to BpfCoordinatorTest once IpNeighborMonitor is migrated to BpfCoordinator.
@Test
public void addRemoveTetherClient() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- DEFAULT_USING_BPF_OFFLOAD);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
final int myIfindex = TEST_IFACE_PARAMS.index;
final int notMyIfindex = myIfindex - 1;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 601f587..7fbb670 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -55,6 +55,7 @@
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.testutils.MiscAsserts.assertSameElements;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -73,6 +74,7 @@
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -92,6 +94,7 @@
import android.os.Build;
import android.os.Handler;
import android.os.test.TestLooper;
+import android.util.ArrayMap;
import android.util.SparseArray;
import androidx.annotation.NonNull;
@@ -136,16 +139,20 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
+import org.mockito.verification.VerificationMode;
import java.io.StringWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -159,7 +166,7 @@
private static final int TEST_NET_ID = 24;
private static final int TEST_NET_ID2 = 25;
- private static final int INVALID_IFINDEX = 0;
+ private static final int NO_UPSTREAM = 0;
private static final int UPSTREAM_IFINDEX = 1001;
private static final int UPSTREAM_XLAT_IFINDEX = 1002;
private static final int UPSTREAM_IFINDEX2 = 1003;
@@ -178,8 +185,17 @@
private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a");
private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
- private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
- private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2");
+ private static final IpPrefix UPSTREAM_PREFIX = new IpPrefix("2001:db8:0:1234::/64");
+ private static final IpPrefix UPSTREAM_PREFIX2 = new IpPrefix("2001:db8:0:abcd::/64");
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES = Set.of(UPSTREAM_PREFIX);
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES2 =
+ Set.of(UPSTREAM_PREFIX, UPSTREAM_PREFIX2);
+ private static final Set<IpPrefix> NO_PREFIXES = Set.of();
+
+ private static final InetAddress NEIGH_A =
+ InetAddresses.parseNumericAddress("2001:db8:0:1234::1");
+ private static final InetAddress NEIGH_B =
+ InetAddresses.parseNumericAddress("2001:db8:0:1234::2");
private static final Inet4Address REMOTE_ADDR =
(Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
@@ -195,7 +211,6 @@
private static final Inet4Address XLAT_LOCAL_IPV4ADDR =
(Inet4Address) InetAddresses.parseNumericAddress("192.0.0.46");
private static final IpPrefix NAT64_IP_PREFIX = new IpPrefix("64:ff9b::/96");
- private static final IpPrefix IPV6_ZERO_PREFIX = new IpPrefix("::/64");
// Generally, public port and private port are the same in the NAT conntrack message.
// TODO: consider using different private port and public port for testing.
@@ -624,10 +639,14 @@
}
private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ return verifyWithOrder(inOrder, t, times(1));
+ }
+
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t, VerificationMode mode) {
if (inOrder != null) {
- return inOrder.verify(t);
+ return inOrder.verify(t, mode);
} else {
- return verify(t);
+ return verify(t, mode);
}
}
@@ -667,6 +686,28 @@
rule.makeTetherUpstream6Key(), rule.makeTether6Value());
}
+ private void verifyAddUpstreamRules(@Nullable InOrder inOrder,
+ @NonNull Set<Ipv6UpstreamRule> rules) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ ArrayMap<TetherUpstream6Key, Tether6Value> expected = new ArrayMap<>();
+ for (Ipv6UpstreamRule rule : rules) {
+ expected.put(rule.makeTetherUpstream6Key(), rule.makeTether6Value());
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ ArgumentCaptor<Tether6Value> valueCaptor =
+ ArgumentCaptor.forClass(Tether6Value.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).insertEntry(
+ keyCaptor.capture(), valueCaptor.capture());
+ List<TetherUpstream6Key> keys = keyCaptor.getAllValues();
+ List<Tether6Value> values = valueCaptor.getAllValues();
+ ArrayMap<TetherUpstream6Key, Tether6Value> captured = new ArrayMap<>();
+ for (int i = 0; i < keys.size(); i++) {
+ captured.put(keys.get(i), values.get(i));
+ }
+ assertEquals(expected, captured);
+ }
+
private void verifyAddDownstreamRule(@Nullable InOrder inOrder,
@NonNull Ipv6DownstreamRule rule) throws Exception {
if (mDeps.isAtLeastS()) {
@@ -697,6 +738,20 @@
rule.makeTetherUpstream6Key());
}
+ private void verifyRemoveUpstreamRules(@Nullable InOrder inOrder,
+ @NonNull Set<Ipv6UpstreamRule> rules) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ List<TetherUpstream6Key> expected = new ArrayList<>();
+ for (Ipv6UpstreamRule rule : rules) {
+ expected.add(rule.makeTetherUpstream6Key());
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).deleteEntry(
+ keyCaptor.capture());
+ assertSameElements(expected, keyCaptor.getAllValues());
+ }
+
private void verifyRemoveDownstreamRule(@Nullable InOrder inOrder,
@NonNull final Ipv6DownstreamRule rule) throws Exception {
if (mDeps.isAtLeastS()) {
@@ -785,10 +840,11 @@
final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfDownstream6Map, mBpfLimitMap,
mBpfStatsMap);
final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(
- mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(
mobileIfIndex, NEIGH_A, MAC_A);
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
verifyAddUpstreamRule(inOrder, upstreamRule);
@@ -798,7 +854,8 @@
// Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
updateStatsEntryForTetherOffloadGetAndClearStats(
buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, 0);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
verifyRemoveDownstreamRule(inOrder, downstreamRule);
verifyRemoveUpstreamRule(inOrder, upstreamRule);
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
@@ -998,11 +1055,10 @@
}
@NonNull
- private static Ipv6UpstreamRule buildTestUpstreamRule(
- int upstreamIfindex, int downstreamIfindex, @NonNull MacAddress inDstMac) {
- return new Ipv6UpstreamRule(upstreamIfindex, downstreamIfindex,
- IPV6_ZERO_PREFIX, inDstMac, MacAddress.ALL_ZEROS_ADDRESS,
- MacAddress.ALL_ZEROS_ADDRESS);
+ private static Ipv6UpstreamRule buildTestUpstreamRule(int upstreamIfindex,
+ int downstreamIfindex, @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac) {
+ return new Ipv6UpstreamRule(upstreamIfindex, downstreamIfindex, sourcePrefix, inDstMac,
+ MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS);
}
@NonNull
@@ -1054,9 +1110,10 @@
// Set the unlimited quota as default if the service has never applied a data limit for a
// given upstream. Note that the data limit only be applied on an upstream which has rules.
final Ipv6UpstreamRule rule = buildTestUpstreamRule(
- mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
verifyAddUpstreamRule(inOrder, rule);
@@ -1104,28 +1161,32 @@
// Adding the first rule on current upstream immediately sends the quota to BPF.
final Ipv6UpstreamRule ruleA = buildTestUpstreamRule(
- mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
verifyAddUpstreamRule(inOrder, ruleA);
inOrder.verifyNoMoreInteractions();
// Adding the second rule on current upstream does not send the quota to BPF.
final Ipv6UpstreamRule ruleB = buildTestUpstreamRule(
- mobileIfIndex, DOWNSTREAM_IFINDEX2, DOWNSTREAM_MAC2);
- coordinator.updateAllIpv6Rules(mIpServer2, DOWNSTREAM_IFACE_PARAMS2, mobileIfIndex);
+ mobileIfIndex, DOWNSTREAM_IFINDEX2, UPSTREAM_PREFIX, DOWNSTREAM_MAC2);
+ coordinator.updateAllIpv6Rules(
+ mIpServer2, DOWNSTREAM_IFACE_PARAMS2, mobileIfIndex, UPSTREAM_PREFIXES);
verifyAddUpstreamRule(inOrder, ruleB);
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
// Removing the second rule on current upstream does not send the quota to BPF.
- coordinator.updateAllIpv6Rules(mIpServer2, DOWNSTREAM_IFACE_PARAMS2, 0);
+ coordinator.updateAllIpv6Rules(
+ mIpServer2, DOWNSTREAM_IFACE_PARAMS2, NO_UPSTREAM, NO_PREFIXES);
verifyRemoveUpstreamRule(inOrder, ruleB);
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
// Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
updateStatsEntryForTetherOffloadGetAndClearStats(
buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, 0);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
verifyRemoveUpstreamRule(inOrder, ruleA);
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
inOrder.verifyNoMoreInteractions();
@@ -1157,13 +1218,14 @@
// [1] Adding rules on the upstream Ethernet.
// Note that the default data limit is applied after the first rule is added.
final Ipv6UpstreamRule ethernetUpstreamRule = buildTestUpstreamRule(
- ethIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ ethIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
final Ipv6DownstreamRule ethernetRuleA = buildTestDownstreamRule(
ethIfIndex, NEIGH_A, MAC_A);
final Ipv6DownstreamRule ethernetRuleB = buildTestDownstreamRule(
ethIfIndex, NEIGH_B, MAC_B);
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, ethIfIndex);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, ethIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
verifyAddUpstreamRule(inOrder, ethernetUpstreamRule);
@@ -1174,7 +1236,9 @@
// [2] Update the existing rules from Ethernet to cellular.
final Ipv6UpstreamRule mobileUpstreamRule = buildTestUpstreamRule(
- mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ final Ipv6UpstreamRule mobileUpstreamRule2 = buildTestUpstreamRule(
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX2, DOWNSTREAM_MAC);
final Ipv6DownstreamRule mobileRuleA = buildTestDownstreamRule(
mobileIfIndex, NEIGH_A, MAC_A);
final Ipv6DownstreamRule mobileRuleB = buildTestDownstreamRule(
@@ -1183,15 +1247,16 @@
buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40));
// Update the existing rules for upstream changes. The rules are removed and re-added one
- // by one for updating upstream interface index by #tetherOffloadRuleUpdate.
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
+ // by one for updating upstream interface index and prefixes by #tetherOffloadRuleUpdate.
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES2);
verifyRemoveDownstreamRule(inOrder, ethernetRuleA);
verifyRemoveDownstreamRule(inOrder, ethernetRuleB);
verifyRemoveUpstreamRule(inOrder, ethernetUpstreamRule);
verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
- verifyAddUpstreamRule(inOrder, mobileUpstreamRule);
+ verifyAddUpstreamRules(inOrder, Set.of(mobileUpstreamRule, mobileUpstreamRule2));
verifyAddDownstreamRule(inOrder, mobileRuleA);
verifyAddDownstreamRule(inOrder, mobileRuleB);
@@ -1201,7 +1266,7 @@
coordinator.clearAllIpv6Rules(mIpServer);
verifyRemoveDownstreamRule(inOrder, mobileRuleA);
verifyRemoveDownstreamRule(inOrder, mobileRuleB);
- verifyRemoveUpstreamRule(inOrder, mobileUpstreamRule);
+ verifyRemoveUpstreamRules(inOrder, Set.of(mobileUpstreamRule, mobileUpstreamRule2));
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
// [4] Force pushing stats update to verify that the last diff of stats is reported on all
@@ -1264,8 +1329,8 @@
assertEquals(1, rules.size());
// The rule can't be updated.
- coordinator.updateAllIpv6Rules(
- mIpServer, DOWNSTREAM_IFACE_PARAMS, rule.upstreamIfindex + 1 /* new */);
+ coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS,
+ rule.upstreamIfindex + 1 /* new */, UPSTREAM_PREFIXES);
verifyNeverRemoveDownstreamRule();
verifyNeverAddDownstreamRule();
rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
@@ -1561,12 +1626,12 @@
//
// @param coordinator BpfCoordinator instance.
// @param upstreamIfindex upstream interface index. can be the following values.
- // INVALID_IFINDEX: no upstream interface
+ // NO_UPSTREAM: no upstream interface
// UPSTREAM_IFINDEX: CELLULAR (raw ip interface)
// UPSTREAM_IFINDEX2: WIFI (ethernet interface)
private void setUpstreamInformationTo(final BpfCoordinator coordinator,
@Nullable Integer upstreamIfindex) {
- if (upstreamIfindex == INVALID_IFINDEX) {
+ if (upstreamIfindex == NO_UPSTREAM) {
coordinator.updateUpstreamNetworkState(null);
return;
}
@@ -1706,7 +1771,8 @@
final BpfCoordinator coordinator = makeBpfCoordinator();
coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, UPSTREAM_IFINDEX);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
eq(new TetherDevValue(UPSTREAM_IFINDEX)));
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
@@ -1715,7 +1781,8 @@
// Adding the second downstream, only the second downstream ifindex is added to DevMap,
// the existing upstream ifindex won't be added again.
- coordinator.updateAllIpv6Rules(mIpServer2, DOWNSTREAM_IFACE_PARAMS2, UPSTREAM_IFINDEX);
+ coordinator.updateAllIpv6Rules(
+ mIpServer2, DOWNSTREAM_IFACE_PARAMS2, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX2)),
eq(new TetherDevValue(DOWNSTREAM_IFINDEX2)));
verify(mBpfDevMap, never()).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
@@ -1996,6 +2063,11 @@
100 /* nonzero, CT_NEW */);
}
+ private static long prefixToLong(IpPrefix prefix) {
+ byte[] prefixBytes = Arrays.copyOf(prefix.getRawAddress(), 8);
+ return ByteBuffer.wrap(prefixBytes).getLong();
+ }
+
void checkRule4ExistInUpstreamDownstreamMap() throws Exception {
assertEquals(UPSTREAM4_RULE_VALUE_A, mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_A));
assertEquals(DOWNSTREAM4_RULE_VALUE_A, mBpfDownstream4Map.getValue(
@@ -2113,7 +2185,7 @@
// [3] Switch upstream from the first upstream (rawip, bpf supported) to no upstream. Clear
// all rules.
- setUpstreamInformationTo(coordinator, INVALID_IFINDEX);
+ setUpstreamInformationTo(coordinator, NO_UPSTREAM);
checkRule4NotExistInUpstreamDownstreamMap();
// Client information should be not deleted.
@@ -2180,14 +2252,15 @@
public void testIpv6ForwardingRuleToString() throws Exception {
final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A,
MAC_A);
- assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, address: 2001:db8::1, "
+ assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, address: 2001:db8:0:1234::1, "
+ "srcMac: 12:34:56:78:90:ab, dstMac: 00:00:00:00:00:0a",
downstreamRule.toString());
final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(
- UPSTREAM_IFINDEX, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
- assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, sourcePrefix: ::/64, "
- + "inDstMac: 12:34:56:78:90:ab, outSrcMac: 00:00:00:00:00:00, "
- + "outDstMac: 00:00:00:00:00:00", upstreamRule.toString());
+ UPSTREAM_IFINDEX, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, "
+ + "sourcePrefix: 2001:db8:0:1234::/64, inDstMac: 12:34:56:78:90:ab, "
+ + "outSrcMac: 00:00:00:00:00:00, outDstMac: 00:00:00:00:00:00",
+ upstreamRule.toString());
}
private void verifyDump(@NonNull final BpfCoordinator coordinator) {
@@ -2237,8 +2310,9 @@
final Ipv6DownstreamRule rule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
mBpfDownstream6Map.insertEntry(rule.makeTetherDownstream6Key(), rule.makeTether6Value());
+ final long prefix64 = prefixToLong(UPSTREAM_PREFIX);
final TetherUpstream6Key upstream6Key = new TetherUpstream6Key(DOWNSTREAM_IFINDEX,
- DOWNSTREAM_MAC, 0);
+ DOWNSTREAM_MAC, prefix64);
final Tether6Value upstream6Value = new Tether6Value(UPSTREAM_IFINDEX,
MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index ba39f22..6eba590 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -302,7 +302,7 @@
// Like so many Android system APIs, these cannot be mocked because it is marked final.
// We have to use the real versions.
private final PersistableBundle mCarrierConfig = new PersistableBundle();
- private final TestLooper mLooper = new TestLooper();
+ private TestLooper mLooper;
private Vector<Intent> mIntents;
private BroadcastInterceptingContext mServiceContext;
@@ -680,7 +680,14 @@
mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class)));
- mTethering = makeTethering();
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
+ }
+
+ // In order to interact with syncSM from the test, tethering must be created in test thread.
+ private void initTetheringOnTestThread() throws Exception {
+ mLooper = new TestLooper();
+ mTethering = new Tethering(mTetheringDependencies);
verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any());
verify(mNetd).registerUnsolicitedEventListener(any());
verifyDefaultNetworkRequestFiled();
@@ -704,9 +711,6 @@
localOnlyCallbackCaptor.capture());
mLocalOnlyHotspotCallback = localOnlyCallbackCaptor.getValue();
}
-
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
}
private void setTetheringSupported(final boolean supported) {
@@ -738,10 +742,6 @@
doReturn(upstreamState).when(mUpstreamNetworkMonitor).selectPreferredUpstreamType(any());
}
- private Tethering makeTethering() {
- return new Tethering(mTetheringDependencies);
- }
-
private TetheringRequestParcel createTetheringRequestParcel(final int type) {
return createTetheringRequestParcel(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
}
@@ -885,6 +885,7 @@
public void failingLocalOnlyHotspotLegacyApBroadcast(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
// Emulate externally-visible WifiManager effects, causing the
// per-interface state machine to start up, and telling us that
// hotspot mode is to be started.
@@ -936,6 +937,7 @@
@Test
public void testUsbConfiguredBroadcastStartsTethering() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
initTetheringUpstream(upstreamState);
prepareUsbTethering();
@@ -1012,6 +1014,7 @@
public void workingLocalOnlyHotspotEnrichedApBroadcast(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
// Emulate externally-visible WifiManager effects, causing the
// per-interface state machine to start up, and telling us that
// hotspot mode is to be started.
@@ -1075,6 +1078,7 @@
@Test
public void workingMobileUsbTethering_IPv4() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
runUsbTethering(upstreamState);
@@ -1089,7 +1093,8 @@
}
@Test
- public void workingMobileUsbTethering_IPv4LegacyDhcp() {
+ public void workingMobileUsbTethering_IPv4LegacyDhcp() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
true);
sendConfigurationChanged();
@@ -1102,6 +1107,7 @@
@Test
public void workingMobileUsbTethering_IPv6() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
runUsbTethering(upstreamState);
@@ -1117,6 +1123,7 @@
@Test
public void workingMobileUsbTethering_DualStack() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
runUsbTethering(upstreamState);
@@ -1134,6 +1141,7 @@
@Test
public void workingMobileUsbTethering_MultipleUpstreams() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobile464xlatUpstreamState();
runUsbTethering(upstreamState);
@@ -1153,6 +1161,7 @@
@Test
public void workingMobileUsbTethering_v6Then464xlat() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
when(mResources.getStringArray(R.array.config_tether_usb_regexs))
@@ -1194,6 +1203,7 @@
@Test
public void configTetherUpstreamAutomaticIgnoresConfigTetherUpstreamTypes() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true);
sendConfigurationChanged();
@@ -1242,6 +1252,7 @@
}
private void verifyAutomaticUpstreamSelection(boolean configAutomatic) throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1341,6 +1352,7 @@
@Test
@IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
public void testLegacyUpstreamSelection() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1491,6 +1503,7 @@
// +-------+-------+-------+-------+-------+
//
private void verifyChooseDunUpstreamByAutomaticMode(boolean configAutomatic) throws Exception {
+ initTetheringOnTestThread();
// Enable automatic upstream selection.
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
@@ -1551,6 +1564,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_defaultNetworkWifi() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
@@ -1602,6 +1616,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_loseDefaultNetworkWifi() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1643,6 +1658,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_defaultNetworkCell() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1687,6 +1703,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_loseAndRegainDun() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
setupDunUpstreamTest(true /* configAutomatic */, inOrder);
@@ -1728,6 +1745,7 @@
@Test
public void testChooseDunUpstreamByAutomaticMode_switchDefaultFromWifiToCell()
throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
@@ -1765,6 +1783,7 @@
@Test
@IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
public void testChooseDunUpstreamByLegacyMode() throws Exception {
+ initTetheringOnTestThread();
// Enable Legacy upstream selection.
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
@@ -1857,6 +1876,7 @@
@Test
public void workingNcmTethering() throws Exception {
+ initTetheringOnTestThread();
runNcmTethering();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
@@ -1864,7 +1884,8 @@
}
@Test
- public void workingNcmTethering_LegacyDhcp() {
+ public void workingNcmTethering_LegacyDhcp() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
true);
sendConfigurationChanged();
@@ -1886,6 +1907,7 @@
// TODO: Test with and without interfaceStatusChanged().
@Test
public void failingWifiTetheringLegacyApBroadcast() throws Exception {
+ initTetheringOnTestThread();
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
@@ -1914,6 +1936,7 @@
// TODO: Test with and without interfaceStatusChanged().
@Test
public void workingWifiTetheringEnrichedApBroadcast() throws Exception {
+ initTetheringOnTestThread();
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
@@ -1962,6 +1985,7 @@
// TODO: Test with and without interfaceStatusChanged().
@Test
public void failureEnablingIpForwarding() throws Exception {
+ initTetheringOnTestThread();
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
@@ -2109,7 +2133,8 @@
}
@Test
- public void testUntetherUsbWhenRestrictionIsOn() {
+ public void testUntetherUsbWhenRestrictionIsOn() throws Exception {
+ initTetheringOnTestThread();
// Start usb tethering and check that usb interface is tethered.
final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
runUsbTethering(upstreamState);
@@ -2286,6 +2311,7 @@
@Test
public void testRegisterTetheringEventCallback() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
final TetheringInterface wifiIface = new TetheringInterface(
@@ -2350,6 +2376,7 @@
@Test
public void testReportFailCallbackIfOffloadNotSupported() throws Exception {
+ initTetheringOnTestThread();
final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
mTethering.registerTetheringEventCallback(callback);
@@ -2389,6 +2416,7 @@
@Test
public void testMultiSimAware() throws Exception {
+ initTetheringOnTestThread();
final TetheringConfiguration initailConfig = mTethering.getTetheringConfiguration();
assertEquals(INVALID_SUBSCRIPTION_ID, initailConfig.activeDataSubId);
@@ -2401,6 +2429,7 @@
@Test
public void testNoDuplicatedEthernetRequest() throws Exception {
+ initTetheringOnTestThread();
final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
@@ -2421,6 +2450,7 @@
private void workingWifiP2pGroupOwner(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
if (emulateInterfaceStatusChanged) {
mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
}
@@ -2460,6 +2490,7 @@
private void workingWifiP2pGroupClient(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
if (emulateInterfaceStatusChanged) {
mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
}
@@ -2500,6 +2531,7 @@
private void workingWifiP2pGroupOwnerLegacyMode(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
// change to legacy mode and update tethering information by chaning SIM
when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
.thenReturn(new String[]{});
@@ -2549,7 +2581,8 @@
}
@Test
- public void testDataSaverChanged() {
+ public void testDataSaverChanged() throws Exception {
+ initTetheringOnTestThread();
// Start Tethering.
final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
runUsbTethering(upstreamState);
@@ -2604,6 +2637,7 @@
@Test
public void testMultipleStartTethering() throws Exception {
+ initTetheringOnTestThread();
final LinkAddress serverLinkAddr = new LinkAddress("192.168.20.1/24");
final LinkAddress clientLinkAddr = new LinkAddress("192.168.20.42/24");
final String serverAddr = "192.168.20.1";
@@ -2647,6 +2681,7 @@
@Test
public void testRequestStaticIp() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
when(mResources.getStringArray(R.array.config_tether_usb_regexs))
@@ -2676,7 +2711,8 @@
}
@Test
- public void testUpstreamNetworkChanged() {
+ public void testUpstreamNetworkChanged() throws Exception {
+ initTetheringOnTestThread();
final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
mTetheringDependencies.mUpstreamNetworkMonitorSM;
final InOrder inOrder = inOrder(mNotificationUpdater);
@@ -2718,7 +2754,8 @@
}
@Test
- public void testUpstreamCapabilitiesChanged() {
+ public void testUpstreamCapabilitiesChanged() throws Exception {
+ initTetheringOnTestThread();
final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
mTetheringDependencies.mUpstreamNetworkMonitorSM;
final InOrder inOrder = inOrder(mNotificationUpdater);
@@ -2753,6 +2790,7 @@
@Test
public void testUpstreamCapabilitiesChanged_startStopTethering() throws Exception {
+ initTetheringOnTestThread();
final TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
// Start USB tethering with no current upstream.
@@ -2774,6 +2812,7 @@
@Test
public void testDumpTetheringLog() throws Exception {
+ initTetheringOnTestThread();
final FileDescriptor mockFd = mock(FileDescriptor.class);
final PrintWriter mockPw = mock(PrintWriter.class);
runUsbTethering(null);
@@ -2787,6 +2826,7 @@
@Test
public void testExemptFromEntitlementCheck() throws Exception {
+ initTetheringOnTestThread();
setupForRequiredProvisioning();
final TetheringRequestParcel wifiNotExemptRequest =
createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
@@ -2877,31 +2917,42 @@
@Test
public void testHandleIpConflict() throws Exception {
+ initTetheringOnTestThread();
final Network wifiNetwork = new Network(200);
final Network[] allNetworks = { wifiNetwork };
doReturn(allNetworks).when(mCm).getAllNetworks();
+ InOrder inOrder = inOrder(mUsbManager, mNetd);
runUsbTethering(null);
+
+ inOrder.verify(mNetd).tetherInterfaceAdd(TEST_RNDIS_IFNAME);
+
final ArgumentCaptor<InterfaceConfigurationParcel> ifaceConfigCaptor =
ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
verify(mNetd).interfaceSetCfg(ifaceConfigCaptor.capture());
final String ipv4Address = ifaceConfigCaptor.getValue().ipv4Addr;
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
- reset(mUsbManager);
// Cause a prefix conflict by assigning a /30 out of the downstream's /24 to the upstream.
updateV4Upstream(new LinkAddress(InetAddresses.parseNumericAddress(ipv4Address), 30),
wifiNetwork, TEST_WIFI_IFNAME, TRANSPORT_WIFI);
// verify turn off usb tethering
- verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ inOrder.verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
sendUsbBroadcast(true, true, -1 /* function */);
mLooper.dispatchAll();
+ inOrder.verify(mNetd).tetherInterfaceRemove(TEST_RNDIS_IFNAME);
+
// verify restart usb tethering
- verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+ inOrder.verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+
+ sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
+ mLooper.dispatchAll();
+ inOrder.verify(mNetd).tetherInterfaceAdd(TEST_RNDIS_IFNAME);
}
@Test
public void testNoAddressAvailable() throws Exception {
+ initTetheringOnTestThread();
final Network wifiNetwork = new Network(200);
final Network btNetwork = new Network(201);
final Network mobileNetwork = new Network(202);
@@ -2963,6 +3014,7 @@
@Test
public void testProvisioningNeededButUnavailable() throws Exception {
+ initTetheringOnTestThread();
assertTrue(mTethering.isTetheringSupported());
verify(mPackageManager, never()).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES);
@@ -2980,6 +3032,7 @@
@Test
public void testUpdateConnectedClients() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
runAsShell(NETWORK_SETTINGS, () -> {
mTethering.registerTetheringEventCallback(callback);
@@ -3029,6 +3082,7 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testUpdateConnectedClientsForLocalOnlyHotspot() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
runAsShell(NETWORK_SETTINGS, () -> {
mTethering.registerTetheringEventCallback(callback);
@@ -3061,6 +3115,7 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testConnectedClientsForSapAndLohsConcurrency() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
runAsShell(NETWORK_SETTINGS, () -> {
mTethering.registerTetheringEventCallback(callback);
@@ -3186,6 +3241,7 @@
@Test
public void testBluetoothTethering() throws Exception {
+ initTetheringOnTestThread();
// Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP.
assumeTrue(isAtLeastT());
@@ -3222,6 +3278,7 @@
@Test
public void testBluetoothTetheringBeforeT() throws Exception {
+ initTetheringOnTestThread();
// Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP.
assumeFalse(isAtLeastT());
@@ -3269,6 +3326,7 @@
@Test
public void testBluetoothServiceDisconnects() throws Exception {
+ initTetheringOnTestThread();
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
@@ -3423,6 +3481,7 @@
@Test
public void testUsbFunctionConfigurationChange() throws Exception {
+ initTetheringOnTestThread();
// Run TETHERING_NCM.
runNcmTethering();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
@@ -3481,6 +3540,7 @@
@Test
public void testTetheringSupported() throws Exception {
+ initTetheringOnTestThread();
final ArraySet<Integer> expectedTypes = getAllSupportedTetheringTypes();
// Check tethering is supported after initialization.
TestTetheringEventCallback callback = new TestTetheringEventCallback();
@@ -3553,6 +3613,7 @@
@Test
public void testIpv4AddressForSapAndLohsConcurrency() throws Exception {
+ initTetheringOnTestThread();
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
new file mode 100644
index 0000000..f8e98e3
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
@@ -0,0 +1,135 @@
+/**
+ * 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 com.android.networkstack.tethering.util
+
+import android.os.Looper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.util.State
+import com.android.networkstack.tethering.util.StateMachineShim.AsyncStateMachine
+import com.android.networkstack.tethering.util.StateMachineShim.Dependencies
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class StateMachineShimTest {
+ private val mSyncSM = mock(SyncStateMachine::class.java)
+ private val mAsyncSM = mock(AsyncStateMachine::class.java)
+ private val mState1 = mock(State::class.java)
+ private val mState2 = mock(State::class.java)
+
+ inner class MyDependencies() : Dependencies() {
+
+ override fun makeSyncStateMachine(name: String, thread: Thread) = mSyncSM
+
+ override fun makeAsyncStateMachine(name: String, looper: Looper) = mAsyncSM
+ }
+
+ @Test
+ fun testUsingSyncStateMachine() {
+ val inOrder = inOrder(mSyncSM, mAsyncSM)
+ val shimUsingSyncSM = StateMachineShim("ShimTest", null, MyDependencies())
+ shimUsingSyncSM.start(mState1)
+ inOrder.verify(mSyncSM).start(mState1)
+
+ val allStates = ArrayList<StateInfo>()
+ allStates.add(StateInfo(mState1, null))
+ allStates.add(StateInfo(mState2, mState1))
+ shimUsingSyncSM.addAllStates(allStates)
+ inOrder.verify(mSyncSM).addAllStates(allStates)
+
+ shimUsingSyncSM.transitionTo(mState1)
+ inOrder.verify(mSyncSM).transitionTo(mState1)
+
+ val what = 10
+ shimUsingSyncSM.sendMessage(what)
+ inOrder.verify(mSyncSM).processMessage(what, 0, 0, null)
+ val obj = Object()
+ shimUsingSyncSM.sendMessage(what, obj)
+ inOrder.verify(mSyncSM).processMessage(what, 0, 0, obj)
+ val arg1 = 11
+ shimUsingSyncSM.sendMessage(what, arg1)
+ inOrder.verify(mSyncSM).processMessage(what, arg1, 0, null)
+ val arg2 = 12
+ shimUsingSyncSM.sendMessage(what, arg1, arg2, obj)
+ inOrder.verify(mSyncSM).processMessage(what, arg1, arg2, obj)
+
+ assertFailsWith(IllegalStateException::class) {
+ shimUsingSyncSM.sendMessageDelayedToAsyncSM(what, 1000 /* delayMillis */)
+ }
+
+ assertFailsWith(IllegalStateException::class) {
+ shimUsingSyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+ }
+
+ shimUsingSyncSM.sendSelfMessageToSyncSM(what, obj)
+ inOrder.verify(mSyncSM).sendSelfMessage(what, 0, 0, obj)
+
+ verifyNoMoreInteractions(mSyncSM, mAsyncSM)
+ }
+
+ @Test
+ fun testUsingAsyncStateMachine() {
+ val inOrder = inOrder(mSyncSM, mAsyncSM)
+ val shimUsingAsyncSM = StateMachineShim("ShimTest", mock(Looper::class.java),
+ MyDependencies())
+ shimUsingAsyncSM.start(mState1)
+ inOrder.verify(mAsyncSM).setInitialState(mState1)
+ inOrder.verify(mAsyncSM).start()
+
+ val allStates = ArrayList<StateInfo>()
+ allStates.add(StateInfo(mState1, null))
+ allStates.add(StateInfo(mState2, mState1))
+ shimUsingAsyncSM.addAllStates(allStates)
+ inOrder.verify(mAsyncSM).addState(mState1, null)
+ inOrder.verify(mAsyncSM).addState(mState2, mState1)
+
+ shimUsingAsyncSM.transitionTo(mState1)
+ inOrder.verify(mAsyncSM).transitionTo(mState1)
+
+ val what = 10
+ shimUsingAsyncSM.sendMessage(what)
+ inOrder.verify(mAsyncSM).sendMessage(what, 0, 0, null)
+ val obj = Object()
+ shimUsingAsyncSM.sendMessage(what, obj)
+ inOrder.verify(mAsyncSM).sendMessage(what, 0, 0, obj)
+ val arg1 = 11
+ shimUsingAsyncSM.sendMessage(what, arg1)
+ inOrder.verify(mAsyncSM).sendMessage(what, arg1, 0, null)
+ val arg2 = 12
+ shimUsingAsyncSM.sendMessage(what, arg1, arg2, obj)
+ inOrder.verify(mAsyncSM).sendMessage(what, arg1, arg2, obj)
+
+ shimUsingAsyncSM.sendMessageDelayedToAsyncSM(what, 1000 /* delayMillis */)
+ inOrder.verify(mAsyncSM).sendMessageDelayed(what, 1000)
+
+ shimUsingAsyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+ inOrder.verify(mAsyncSM).sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+
+ assertFailsWith(IllegalStateException::class) {
+ shimUsingAsyncSM.sendSelfMessageToSyncSM(what, obj)
+ }
+
+ verifyNoMoreInteractions(mSyncSM, mAsyncSM)
+ }
+}
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 9017976..b77a8d1 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -142,12 +142,6 @@
BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
"fs_bpf_net_shared", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
-static __always_inline int is_system_uid(uint32_t uid) {
- // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
- // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
- return (uid < AID_APP_START);
-}
-
/*
* Note: this blindly assumes an MTU of 1500, and that packets > MTU are always TCP,
* and that TCP is using the Linux default settings with TCP timestamp option enabled
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index 4958040..2b28f06 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -16,6 +16,7 @@
#pragma once
+#include <cutils/android_filesystem_config.h>
#include <linux/if.h>
#include <linux/if_ether.h>
#include <linux/in.h>
@@ -249,3 +250,9 @@
static inline bool isBlockedByUidRules(BpfConfig enabledRules, uint32_t uidRules) {
return enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET);
}
+
+static inline bool is_system_uid(uint32_t uid) {
+ // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
+ // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
+ return (uid < AID_APP_START);
+}
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 35b8eea..90f96a1 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -198,7 +198,8 @@
TetherUpstream6Key ku = {
.iif = skb->ifindex,
- .src64 = 0,
+ // Retrieve the first 64 bits of the source IPv6 address in network order
+ .src64 = *(uint64_t*)&(ip6->saddr.s6_addr32[0]),
};
if (is_ethernet) __builtin_memcpy(stream.down ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
diff --git a/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java b/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
new file mode 100644
index 0000000..eabcf3c
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.android.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+
+import java.net.Inet6Address;
+
+/** Key type for ingress discard map */
+public class IngressDiscardKey extends Struct {
+ // The destination ip of the incoming packet. IPv4 uses IPv4-mapped IPv6 address.
+ @Field(order = 0, type = Type.Ipv6Address)
+ public final Inet6Address dstAddr;
+
+ public IngressDiscardKey(final Inet6Address dstAddr) {
+ this.dstAddr = dstAddr;
+ }
+}
diff --git a/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java b/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java
new file mode 100644
index 0000000..7df3620
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.android.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+
+/** Value type for ingress discard map */
+public class IngressDiscardValue extends Struct {
+ // Allowed interface indexes.
+ // Use the same value for iif1 and iif2 if there is only a single allowed interface index.
+ @Field(order = 0, type = Type.S32)
+ public final int iif1;
+ @Field(order = 1, type = Type.S32)
+ public final int iif2;
+
+ public IngressDiscardValue(final int iif1, final int iif2) {
+ this.iif1 = iif1;
+ this.iif2 = iif2;
+ }
+}
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 06d3238..23510e1 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -417,12 +417,87 @@
package android.net.thread {
- @FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkController {
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ActiveOperationalDataset implements android.os.Parcelable {
+ method @NonNull public static android.net.thread.ActiveOperationalDataset createRandomDataset();
+ method public int describeContents();
+ method @NonNull public static android.net.thread.ActiveOperationalDataset fromThreadTlvs(@NonNull byte[]);
+ method @NonNull public android.net.thread.OperationalDatasetTimestamp getActiveTimestamp();
+ method @IntRange(from=0, to=65535) public int getChannel();
+ method @NonNull @Size(min=1) public android.util.SparseArray<byte[]> getChannelMask();
+ method @IntRange(from=0, to=255) public int getChannelPage();
+ method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) public byte[] getExtendedPanId();
+ method @NonNull public android.net.IpPrefix getMeshLocalPrefix();
+ method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) public byte[] getNetworkKey();
+ method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) public String getNetworkName();
+ method @IntRange(from=0, to=65534) public int getPanId();
+ method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) public byte[] getPskc();
+ method @NonNull public android.net.thread.ActiveOperationalDataset.SecurityPolicy getSecurityPolicy();
+ method @NonNull public byte[] toThreadTlvs();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int CHANNEL_MAX_24_GHZ = 26; // 0x1a
+ field public static final int CHANNEL_MIN_24_GHZ = 11; // 0xb
+ field public static final int CHANNEL_PAGE_24_GHZ = 0; // 0x0
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ActiveOperationalDataset> CREATOR;
+ field public static final int LENGTH_EXTENDED_PAN_ID = 8; // 0x8
+ field public static final int LENGTH_MAX_DATASET_TLVS = 254; // 0xfe
+ field public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16; // 0x10
+ field public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64; // 0x40
+ field public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1; // 0x1
+ field public static final int LENGTH_NETWORK_KEY = 16; // 0x10
+ field public static final int LENGTH_PSKC = 16; // 0x10
+ }
+
+ public static final class ActiveOperationalDataset.Builder {
+ ctor public ActiveOperationalDataset.Builder(@NonNull android.net.thread.ActiveOperationalDataset);
+ ctor public ActiveOperationalDataset.Builder();
+ method @NonNull public android.net.thread.ActiveOperationalDataset build();
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setActiveTimestamp(@NonNull android.net.thread.OperationalDatasetTimestamp);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannel(@IntRange(from=0, to=255) int, @IntRange(from=0, to=65535) int);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannelMask(@NonNull @Size(min=1) android.util.SparseArray<byte[]>);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setExtendedPanId(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setMeshLocalPrefix(@NonNull android.net.IpPrefix);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkKey(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkName(@NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) String);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPanId(@IntRange(from=0, to=65534) int);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPskc(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setSecurityPolicy(@NonNull android.net.thread.ActiveOperationalDataset.SecurityPolicy);
+ }
+
+ public static final class ActiveOperationalDataset.SecurityPolicy {
+ ctor public ActiveOperationalDataset.SecurityPolicy(@IntRange(from=1, to=65535) int, @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[]);
+ method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) public byte[] getFlags();
+ method @IntRange(from=1, to=65535) public int getRotationTimeHours();
+ field public static final int DEFAULT_ROTATION_TIME_HOURS = 672; // 0x2a0
+ field public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1; // 0x1
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class OperationalDatasetTimestamp {
+ ctor public OperationalDatasetTimestamp(@IntRange(from=0, to=281474976710655L) long, @IntRange(from=0, to=32767) int, boolean);
+ method @NonNull public static android.net.thread.OperationalDatasetTimestamp fromInstant(@NonNull java.time.Instant);
+ method @IntRange(from=0, to=281474976710655L) public long getSeconds();
+ method @IntRange(from=0, to=32767) public int getTicks();
+ method public boolean isAuthoritativeSource();
+ method @NonNull public java.time.Instant toInstant();
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class PendingOperationalDataset implements android.os.Parcelable {
+ ctor public PendingOperationalDataset(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull android.net.thread.OperationalDatasetTimestamp, @NonNull java.time.Duration);
+ method public int describeContents();
+ method @NonNull public static android.net.thread.PendingOperationalDataset fromThreadTlvs(@NonNull byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset getActiveOperationalDataset();
+ method @NonNull public java.time.Duration getDelayTimer();
+ method @NonNull public android.net.thread.OperationalDatasetTimestamp getPendingTimestamp();
+ method @NonNull public byte[] toThreadTlvs();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> CREATOR;
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
method public int getThreadVersion();
field public static final int THREAD_VERSION_1_3 = 4; // 0x4
}
- @FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkManager {
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkManager {
method @NonNull public java.util.List<android.net.thread.ThreadNetworkController> getAllThreadNetworkControllers();
}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index d150373..6152287 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -38,6 +38,7 @@
#include <sys/stat.h>
#include <sys/types.h>
+#include <android/api-level.h>
#include <android-base/logging.h>
#include <android-base/macros.h>
#include <android-base/properties.h>
@@ -172,6 +173,9 @@
(void)argc;
android::base::InitLogging(argv, &android::base::KernelLogger);
+ const int device_api_level = android_get_device_api_level();
+ const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+
if (!android::bpf::isAtLeastKernelVersion(4, 19, 0)) {
ALOGE("Android U QPR2 requires kernel 4.19.");
return 1;
@@ -208,24 +212,27 @@
return 1;
}
- // Linux 5.16-rc1 changed the default to 2 (disabled but changeable), but we need 0 (enabled)
- // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on pre-5.13,
- // on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
- if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
- android::bpf::isAtLeastKernelVersion(5, 13, 0)) return 1;
+ if (isAtLeastU) {
+ // Linux 5.16-rc1 changed the default to 2 (disabled but changeable),
+ // but we need 0 (enabled)
+ // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on
+ // pre-5.13, on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
+ if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
+ android::bpf::isAtLeastKernelVersion(5, 13, 0)) return 1;
- // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
- // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
- // (Note: this (open) will fail with ENOENT 'No such file or directory' if
- // kernel does not have CONFIG_BPF_JIT=y)
- // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
- // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
- if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
+ // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
+ // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
+ // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+ // kernel does not have CONFIG_BPF_JIT=y)
+ // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
+ // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
+ if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
- // Enable JIT kallsyms export for privileged users only
- // (Note: this (open) will fail with ENOENT 'No such file or directory' if
- // kernel does not have CONFIG_HAVE_EBPF_JIT=y)
- if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
+ // Enable JIT kallsyms export for privileged users only
+ // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+ // kernel does not have CONFIG_HAVE_EBPF_JIT=y)
+ if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
+ }
// Create all the pin subdirectories
// (this must be done first to allow selinux_context and pin_subdir functionality,
diff --git a/service-t/src/com/android/server/net/NetworkStatsEventLogger.java b/service-t/src/com/android/server/net/NetworkStatsEventLogger.java
new file mode 100644
index 0000000..679837a
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsEventLogger.java
@@ -0,0 +1,152 @@
+/*
+ * 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 com.android.server.net;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper class for NetworkStatsService to log events.
+ *
+ * @hide
+ */
+public class NetworkStatsEventLogger {
+ static final int POLL_REASON_DUMPSYS = 0;
+ static final int POLL_REASON_FORCE_UPDATE = 1;
+ static final int POLL_REASON_GLOBAL_ALERT = 2;
+ static final int POLL_REASON_NETWORK_STATUS_CHANGED = 3;
+ static final int POLL_REASON_OPEN_SESSION = 4;
+ static final int POLL_REASON_PERIODIC = 5;
+ static final int POLL_REASON_RAT_CHANGED = 6;
+ static final int POLL_REASON_REG_CALLBACK = 7;
+ static final int POLL_REASON_REMOVE_UIDS = 8;
+ static final int POLL_REASON_UPSTREAM_CHANGED = 9;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "POLL_REASON_" }, value = {
+ POLL_REASON_DUMPSYS,
+ POLL_REASON_FORCE_UPDATE,
+ POLL_REASON_GLOBAL_ALERT,
+ POLL_REASON_NETWORK_STATUS_CHANGED,
+ POLL_REASON_OPEN_SESSION,
+ POLL_REASON_PERIODIC,
+ POLL_REASON_RAT_CHANGED,
+ POLL_REASON_REMOVE_UIDS,
+ POLL_REASON_REG_CALLBACK,
+ POLL_REASON_UPSTREAM_CHANGED
+ })
+ public @interface PollReason {
+ }
+ static final int MAX_POLL_REASON = POLL_REASON_UPSTREAM_CHANGED;
+
+ @VisibleForTesting(visibility = PRIVATE)
+ public static final int MAX_EVENTS_LOGS = 50;
+ private final LocalLog mEventChanges = new LocalLog(MAX_EVENTS_LOGS);
+ private final int[] mPollEventCounts = new int[MAX_POLL_REASON + 1];
+
+ /**
+ * Log a poll event.
+ *
+ * @param flags Flags used when polling. See NetworkStatsService#FLAG_PERSIST_*.
+ * @param event The event of polling to be logged.
+ */
+ public void logPollEvent(int flags, @NonNull PollEvent event) {
+ mEventChanges.log("Poll(flags=" + flags + ", " + event + ")");
+ mPollEventCounts[event.reason]++;
+ }
+
+ /**
+ * Print poll counts per reason into the given stream.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public void dumpPollCountsPerReason(@NonNull IndentingPrintWriter pw) {
+ pw.println("Poll counts per reason:");
+ pw.increaseIndent();
+ for (int i = 0; i <= MAX_POLL_REASON; i++) {
+ pw.println(PollEvent.pollReasonNameOf(i) + ": " + mPollEventCounts[i]);
+ }
+ pw.decreaseIndent();
+ pw.println();
+ }
+
+ /**
+ * Print recent poll events into the given stream.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public void dumpRecentPollEvents(@NonNull IndentingPrintWriter pw) {
+ pw.println("Recent poll events:");
+ pw.increaseIndent();
+ mEventChanges.reverseDump(pw);
+ pw.decreaseIndent();
+ pw.println();
+ }
+
+ /**
+ * Print the object's state into the given stream.
+ */
+ public void dump(@NonNull IndentingPrintWriter pw) {
+ dumpPollCountsPerReason(pw);
+ dumpRecentPollEvents(pw);
+ }
+
+ public static class PollEvent {
+ public final int reason;
+
+ public PollEvent(@PollReason int reason) {
+ if (reason < 0 || reason > MAX_POLL_REASON) {
+ throw new IllegalArgumentException("Unsupported poll reason: " + reason);
+ }
+ this.reason = reason;
+ }
+
+ @Override
+ public String toString() {
+ return "PollEvent{" + "reason=" + pollReasonNameOf(reason) + "}";
+ }
+
+ /**
+ * Get the name of the given reason.
+ *
+ * If the reason does not have a String representation, returns its integer representation.
+ */
+ @NonNull
+ public static String pollReasonNameOf(@PollReason int reason) {
+ switch (reason) {
+ case POLL_REASON_DUMPSYS: return "DUMPSYS";
+ case POLL_REASON_FORCE_UPDATE: return "FORCE_UPDATE";
+ case POLL_REASON_GLOBAL_ALERT: return "GLOBAL_ALERT";
+ case POLL_REASON_NETWORK_STATUS_CHANGED: return "NETWORK_STATUS_CHANGED";
+ case POLL_REASON_OPEN_SESSION: return "OPEN_SESSION";
+ case POLL_REASON_PERIODIC: return "PERIODIC";
+ case POLL_REASON_RAT_CHANGED: return "RAT_CHANGED";
+ case POLL_REASON_REMOVE_UIDS: return "REMOVE_UIDS";
+ case POLL_REASON_REG_CALLBACK: return "REG_CALLBACK";
+ case POLL_REASON_UPSTREAM_CHANGED: return "UPSTREAM_CHANGED";
+ default: return Integer.toString(reason);
+ }
+ }
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index cc67550..46afd31 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -66,6 +66,17 @@
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REMOVE_UIDS;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_UPSTREAM_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.PollEvent;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -281,6 +292,8 @@
static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts";
static final String NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME = "import.successes";
static final String NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME = "import.fallbacks";
+ static final String CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER =
+ "enable_network_stats_event_logger";
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
@@ -441,6 +454,7 @@
* Map from key {@code OpenSessionKey} to count of opened sessions. This is for recording
* the caller of open session and it is only for debugging.
*/
+ // TODO: Move to NetworkStatsEventLogger to centralize event logging.
@GuardedBy("mOpenSessionCallsLock")
private final HashMap<OpenSessionKey, Integer> mOpenSessionCallsPerCaller = new HashMap<>();
@@ -513,20 +527,21 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_PERFORM_POLL: {
- performPoll(FLAG_PERSIST_ALL);
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent((int) msg.obj));
break;
}
case MSG_NOTIFY_NETWORK_STATUS: {
synchronized (mStatsLock) {
// If no cached states, ignore.
if (mLastNetworkStateSnapshots == null) break;
- handleNotifyNetworkStatus(
- mDefaultNetworks, mLastNetworkStateSnapshots, mActiveIface);
+ handleNotifyNetworkStatus(mDefaultNetworks, mLastNetworkStateSnapshots,
+ mActiveIface, maybeCreatePollEvent((int) msg.obj));
}
break;
}
case MSG_PERFORM_POLL_REGISTER_ALERT: {
- performPoll(FLAG_PERSIST_NETWORK);
+ performPoll(FLAG_PERSIST_NETWORK,
+ maybeCreatePollEvent(POLL_REASON_GLOBAL_ALERT));
registerGlobalAlert();
break;
}
@@ -612,6 +627,13 @@
mStatsMapB = mDeps.getStatsMapB();
mAppUidStatsMap = mDeps.getAppUidStatsMap();
mIfaceStatsMap = mDeps.getIfaceStatsMap();
+ // To prevent any possible races, the flag is not allowed to change until rebooting.
+ mSupportEventLogger = mDeps.supportEventLogger(mContext);
+ if (mSupportEventLogger) {
+ mEventLogger = new NetworkStatsEventLogger();
+ } else {
+ mEventLogger = null;
+ }
// TODO: Remove bpfNetMaps creation and always start SkDestroyListener
// Following code is for the experiment to verify the SkDestroyListener refactoring. Based
@@ -840,6 +862,14 @@
IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
return new SkDestroyListener(cookieTagMap, handler, new SharedLog(TAG));
}
+
+ /**
+ * Get whether event logger feature is supported.
+ */
+ public boolean supportEventLogger(Context ctx) {
+ return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+ ctx, CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER);
+ }
}
/**
@@ -1432,7 +1462,7 @@
| NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
final long ident = Binder.clearCallingIdentity();
try {
- performPoll(FLAG_PERSIST_ALL);
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_OPEN_SESSION));
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -1828,7 +1858,8 @@
final long token = Binder.clearCallingIdentity();
try {
- handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface);
+ handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface,
+ maybeCreatePollEvent(POLL_REASON_NETWORK_STATUS_CHANGED));
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1845,7 +1876,8 @@
final long token = Binder.clearCallingIdentity();
try {
- performPoll(FLAG_PERSIST_ALL);
+ // TODO: Log callstack for system server callers.
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_FORCE_UPDATE));
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1902,7 +1934,8 @@
}
// Create baseline stats
- mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL));
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL,
+ POLL_REASON_REG_CALLBACK));
return normalizedRequest;
}
@@ -1999,7 +2032,8 @@
new TetheringManager.TetheringEventCallback() {
@Override
public void onUpstreamChanged(@Nullable Network network) {
- performPoll(FLAG_PERSIST_NETWORK);
+ performPoll(FLAG_PERSIST_NETWORK,
+ maybeCreatePollEvent(POLL_REASON_UPSTREAM_CHANGED));
}
};
@@ -2008,7 +2042,7 @@
public void onReceive(Context context, Intent intent) {
// on background handler thread, and verified UPDATE_DEVICE_STATS
// permission above.
- performPoll(FLAG_PERSIST_ALL);
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_PERIODIC));
// verify that we're watching global alert
registerGlobalAlert();
@@ -2072,19 +2106,20 @@
public void handleOnCollapsedRatTypeChanged() {
// Protect service from frequently updating. Remove pending messages if any.
mHandler.removeMessages(MSG_NOTIFY_NETWORK_STATUS);
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS), mSettings.getPollDelay());
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS,
+ POLL_REASON_RAT_CHANGED), mSettings.getPollDelay());
}
private void handleNotifyNetworkStatus(
Network[] defaultNetworks,
NetworkStateSnapshot[] snapshots,
- String activeIface) {
+ String activeIface,
+ @Nullable PollEvent event) {
synchronized (mStatsLock) {
mWakeLock.acquire();
try {
mActiveIface = activeIface;
- handleNotifyNetworkStatusLocked(defaultNetworks, snapshots);
+ handleNotifyNetworkStatusLocked(defaultNetworks, snapshots, event);
} finally {
mWakeLock.release();
}
@@ -2098,7 +2133,7 @@
*/
@GuardedBy("mStatsLock")
private void handleNotifyNetworkStatusLocked(@NonNull Network[] defaultNetworks,
- @NonNull NetworkStateSnapshot[] snapshots) {
+ @NonNull NetworkStateSnapshot[] snapshots, @Nullable PollEvent event) {
if (!mSystemReady) return;
if (LOGV) Log.v(TAG, "handleNotifyNetworkStatusLocked()");
@@ -2108,7 +2143,7 @@
// poll, but only persist network stats to keep codepath fast. UID stats
// will be persisted during next alarm poll event.
- performPollLocked(FLAG_PERSIST_NETWORK);
+ performPollLocked(FLAG_PERSIST_NETWORK, event);
// Rebuild active interfaces based on connected networks
mActiveIfaces.clear();
@@ -2325,12 +2360,12 @@
}
}
- private void performPoll(int flags) {
+ private void performPoll(int flags, @Nullable PollEvent event) {
synchronized (mStatsLock) {
mWakeLock.acquire();
try {
- performPollLocked(flags);
+ performPollLocked(flags, event);
} finally {
mWakeLock.release();
}
@@ -2342,11 +2377,15 @@
* {@link NetworkStatsHistory}.
*/
@GuardedBy("mStatsLock")
- private void performPollLocked(int flags) {
+ private void performPollLocked(int flags, @Nullable PollEvent event) {
if (!mSystemReady) return;
if (LOGV) Log.v(TAG, "performPollLocked(flags=0x" + Integer.toHexString(flags) + ")");
Trace.traceBegin(TRACE_TAG_NETWORK, "performPollLocked");
+ if (mSupportEventLogger) {
+ mEventLogger.logPollEvent(flags, event);
+ }
+
final boolean persistNetwork = (flags & FLAG_PERSIST_NETWORK) != 0;
final boolean persistUid = (flags & FLAG_PERSIST_UID) != 0;
final boolean persistForce = (flags & FLAG_PERSIST_FORCE) != 0;
@@ -2546,7 +2585,7 @@
if (LOGV) Log.v(TAG, "removeUidsLocked() for UIDs " + Arrays.toString(uids));
// Perform one last poll before removing
- performPollLocked(FLAG_PERSIST_ALL);
+ performPollLocked(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_REMOVE_UIDS));
mUidRecorder.removeUidsLocked(uids);
mUidTagRecorder.removeUidsLocked(uids);
@@ -2629,7 +2668,8 @@
}
if (poll) {
- performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE);
+ performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE,
+ maybeCreatePollEvent(POLL_REASON_DUMPSYS));
pw.println("Forced poll");
return;
}
@@ -2689,6 +2729,7 @@
pw.println("(failed to dump platform legacy stats import counters)");
}
}
+ pw.println(CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER + ": " + mSupportEventLogger);
pw.decreaseIndent();
@@ -2746,6 +2787,10 @@
pw.decreaseIndent();
pw.println();
+ if (mSupportEventLogger) {
+ mEventLogger.dump(pw);
+ }
+
pw.println("Stats Providers:");
pw.increaseIndent();
invokeForAllStatsProviderCallbacks((cb) -> {
@@ -3215,6 +3260,22 @@
}
}
+ private final boolean mSupportEventLogger;
+ @GuardedBy("mStatsLock")
+ @Nullable
+ private final NetworkStatsEventLogger mEventLogger;
+
+ /**
+ * Create a PollEvent instance if the feature is enabled.
+ */
+ @Nullable
+ public PollEvent maybeCreatePollEvent(@NetworkStatsEventLogger.PollReason int reason) {
+ if (mSupportEventLogger) {
+ return new PollEvent(reason);
+ }
+ return null;
+ }
+
private class DropBoxNonMonotonicObserver implements NonMonotonicObserver<String> {
@Override
public void foundNonMonotonic(NetworkStats left, int leftIndex, NetworkStats right,
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index c559fa3..5ed50ce 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -1538,9 +1538,9 @@
/**
* Get BPF program Id from CGROUP. See {@link BpfUtils#getProgramId}.
*/
- public int getBpfProgramId(final int attachType, @NonNull final String cgroupPath)
+ public int getBpfProgramId(final int attachType)
throws IOException {
- return BpfUtils.getProgramId(attachType, cgroupPath);
+ return BpfUtils.getProgramId(attachType);
}
/**
@@ -3274,15 +3274,15 @@
pw.increaseIndent();
try {
pw.print("CGROUP_INET_INGRESS: ");
- pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_INGRESS, BpfUtils.CGROUP_PATH));
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_INGRESS));
pw.print("CGROUP_INET_EGRESS: ");
- pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_EGRESS, BpfUtils.CGROUP_PATH));
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_EGRESS));
pw.print("CGROUP_INET_SOCK_CREATE: ");
- pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_SOCK_CREATE, BpfUtils.CGROUP_PATH));
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_SOCK_CREATE));
pw.print("CGROUP_INET4_BIND: ");
- pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET4_BIND, BpfUtils.CGROUP_PATH));
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET4_BIND));
pw.print("CGROUP_INET6_BIND: ");
- pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET6_BIND, BpfUtils.CGROUP_PATH));
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET6_BIND));
} catch (IOException e) {
pw.println(" IOException");
}
@@ -5074,8 +5074,8 @@
}
mNetd.networkCreate(config);
mDnsResolver.createNetworkCache(nai.network.getNetId());
- mDnsManager.updateTransportsForNetwork(nai.network.getNetId(),
- nai.networkCapabilities.getTransportTypes());
+ mDnsManager.updateCapabilitiesForNetwork(nai.network.getNetId(),
+ nai.networkCapabilities);
return true;
} catch (RemoteException | ServiceSpecificException e) {
loge("Error creating network " + nai.toShortString() + ": " + e.getMessage());
@@ -8899,8 +8899,7 @@
propagateUnderlyingNetworkCapabilities(nai.network);
if (!newNc.equalsTransportTypes(prevNc)) {
- mDnsManager.updateTransportsForNetwork(
- nai.network.getNetId(), newNc.getTransportTypes());
+ mDnsManager.updateCapabilitiesForNetwork(nai.network.getNetId(), newNc);
}
maybeSendProxyBroadcast(nai, prevNc, newNc);
@@ -10816,7 +10815,7 @@
// If type can't be parsed, this throws NumberFormatException, which
// is passed back to adb who prints it.
final int type = Integer.parseInt(getNextArg());
- final int ret = BpfUtils.getProgramId(type, BpfUtils.CGROUP_PATH);
+ final int ret = BpfUtils.getProgramId(type);
pw.println(ret);
return 0;
}
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index 1493cae..81b2289 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -38,6 +38,7 @@
import android.net.InetAddresses;
import android.net.LinkProperties;
import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.ResolverParamsParcel;
import android.net.Uri;
import android.net.shared.PrivateDnsConfig;
@@ -251,7 +252,7 @@
// TODO: Replace the Map with SparseArrays.
private final Map<Integer, PrivateDnsValidationStatuses> mPrivateDnsValidationMap;
private final Map<Integer, LinkProperties> mLinkPropertiesMap;
- private final Map<Integer, int[]> mTransportsMap;
+ private final Map<Integer, NetworkCapabilities> mNetworkCapabilitiesMap;
private int mSampleValidity;
private int mSuccessThreshold;
@@ -265,7 +266,7 @@
mPrivateDnsMap = new ConcurrentHashMap<>();
mPrivateDnsValidationMap = new HashMap<>();
mLinkPropertiesMap = new HashMap<>();
- mTransportsMap = new HashMap<>();
+ mNetworkCapabilitiesMap = new HashMap<>();
// TODO: Create and register ContentObservers to track every setting
// used herein, posting messages to respond to changes.
@@ -278,7 +279,7 @@
public void removeNetwork(Network network) {
mPrivateDnsMap.remove(network.getNetId());
mPrivateDnsValidationMap.remove(network.getNetId());
- mTransportsMap.remove(network.getNetId());
+ mNetworkCapabilitiesMap.remove(network.getNetId());
mLinkPropertiesMap.remove(network.getNetId());
}
@@ -326,12 +327,12 @@
/**
* When creating a new network or transport types are changed in a specific network,
- * transport types are always saved to a hashMap before update dns config.
+ * capabilities are always saved to a hashMap before update dns config.
* When destroying network, the specific network will be removed from the hashMap.
* The hashMap is always accessed on the same thread.
*/
- public void updateTransportsForNetwork(int netId, @NonNull int[] transportTypes) {
- mTransportsMap.put(netId, transportTypes);
+ public void updateCapabilitiesForNetwork(int netId, @NonNull final NetworkCapabilities nc) {
+ mNetworkCapabilitiesMap.put(netId, nc);
sendDnsConfigurationForNetwork(netId);
}
@@ -351,8 +352,8 @@
*/
public void sendDnsConfigurationForNetwork(int netId) {
final LinkProperties lp = mLinkPropertiesMap.get(netId);
- final int[] transportTypes = mTransportsMap.get(netId);
- if (lp == null || transportTypes == null) return;
+ final NetworkCapabilities nc = mNetworkCapabilitiesMap.get(netId);
+ if (lp == null || nc == null) return;
updateParametersSettings();
final ResolverParamsParcel paramsParcel = new ResolverParamsParcel();
@@ -383,7 +384,7 @@
.collect(Collectors.toList()))
: useTls ? paramsParcel.servers // Opportunistic
: new String[0]; // Off
- paramsParcel.transportTypes = transportTypes;
+ paramsParcel.transportTypes = nc.getTransportTypes();
// Prepare to track the validation status of the DNS servers in the
// resolver config when private DNS is in opportunistic or strict mode.
if (useTls) {
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index d99eedc..1d8b4eb 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -258,7 +258,7 @@
}
/** Add or remove |route|. */
- public static void modifyRoute(final INetd netd, final ModifyOperation op, final int netId,
+ private static void modifyRoute(final INetd netd, final ModifyOperation op, final int netId,
final RouteInfo route) {
final String ifName = route.getInterface();
final String dst = route.getDestination().toString();
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index d45cace..595ac74 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -187,12 +187,6 @@
return nativeDeleteMapEntry(mMapFd.getFd(), key.writeToBytes());
}
- /** Returns {@code true} if this map contains no elements. */
- @Override
- public boolean isEmpty() throws ErrnoException {
- return getFirstKey() == null;
- }
-
private K getNextKeyInternal(@Nullable K key) throws ErrnoException {
byte[] rawKey = new byte[mKeySize];
@@ -245,49 +239,6 @@
return Struct.parse(mValueClass, buffer);
}
- /**
- * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
- * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
- * other structural modifications to the map, such as adding entries or deleting other entries.
- * Otherwise, iteration will result in undefined behaviour.
- */
- @Override
- public void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException {
- @Nullable K nextKey = getFirstKey();
-
- while (nextKey != null) {
- @NonNull final K curKey = nextKey;
- @NonNull final V value = getValue(curKey);
-
- nextKey = getNextKey(curKey);
- action.accept(curKey, value);
- }
- }
-
- /* Empty implementation to implement AutoCloseable, so we can use BpfMaps
- * with try with resources, but due to persistent FD cache, there is no actual
- * need to close anything. File descriptors will actually be closed when we
- * unlock the BpfMap class and destroy the ParcelFileDescriptor objects.
- */
- @Override
- public void close() throws IOException {
- }
-
- /**
- * Clears the map. The map may already be empty.
- *
- * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
- * or if a non-ENOENT error occurred when deleting a key.
- */
- @Override
- public void clear() throws ErrnoException {
- K key = getFirstKey();
- while (key != null) {
- deleteEntry(key); // ignores ENOENT.
- key = getFirstKey();
- }
- }
-
private static native int nativeBpfFdGet(String path, int mode, int keySize, int valueSize)
throws ErrnoException, NullPointerException;
diff --git a/staticlibs/device/com/android/net/module/util/BpfUtils.java b/staticlibs/device/com/android/net/module/util/BpfUtils.java
index 6116a5f..10a8f60 100644
--- a/staticlibs/device/com/android/net/module/util/BpfUtils.java
+++ b/staticlibs/device/com/android/net/module/util/BpfUtils.java
@@ -41,49 +41,18 @@
public static final String CGROUP_PATH = "/sys/fs/cgroup/";
/**
- * Attach BPF program to CGROUP
- */
- public static void attachProgram(int type, @NonNull String programPath,
- @NonNull String cgroupPath, int flags) throws IOException {
- native_attachProgramToCgroup(type, programPath, cgroupPath, flags);
- }
-
- /**
- * Detach BPF program from CGROUP
- */
- public static void detachProgram(int type, @NonNull String cgroupPath)
- throws IOException {
- native_detachProgramFromCgroup(type, cgroupPath);
- }
-
- /**
* Get BPF program Id from CGROUP.
*
* Note: This requires a 4.19 kernel which is only guaranteed on V+.
*
* @param attachType Bpf attach type. See bpf_attach_type in include/uapi/linux/bpf.h.
- * @param cgroupPath Path of cgroup.
* @return Positive integer for a Program Id. 0 if no program is attached.
* @throws IOException if failed to open the cgroup directory or query bpf program.
*/
- public static int getProgramId(int attachType, @NonNull String cgroupPath) throws IOException {
- return native_getProgramIdFromCgroup(attachType, cgroupPath);
+ public static int getProgramId(int attachType) throws IOException {
+ return native_getProgramIdFromCgroup(attachType, CGROUP_PATH);
}
- /**
- * Detach single BPF program from CGROUP
- */
- public static void detachSingleProgram(int type, @NonNull String programPath,
- @NonNull String cgroupPath) throws IOException {
- native_detachSingleProgramFromCgroup(type, programPath, cgroupPath);
- }
-
- private static native boolean native_attachProgramToCgroup(int type, String programPath,
- String cgroupPath, int flags) throws IOException;
- private static native boolean native_detachProgramFromCgroup(int type, String cgroupPath)
- throws IOException;
- private static native boolean native_detachSingleProgramFromCgroup(int type,
- String programPath, String cgroupPath) throws IOException;
private static native int native_getProgramIdFromCgroup(int type, String cgroupPath)
throws IOException;
}
diff --git a/staticlibs/device/com/android/net/module/util/IBpfMap.java b/staticlibs/device/com/android/net/module/util/IBpfMap.java
index 83ff875..ca56830 100644
--- a/staticlibs/device/com/android/net/module/util/IBpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/IBpfMap.java
@@ -18,6 +18,7 @@
import android.system.ErrnoException;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.io.IOException;
import java.util.NoSuchElementException;
@@ -49,15 +50,17 @@
/** Remove existing key from eBpf map. Return true if something was deleted. */
boolean deleteEntry(K key) throws ErrnoException;
- /** Returns {@code true} if this map contains no elements. */
- boolean isEmpty() throws ErrnoException;
-
/** Get the key after the passed-in key. */
K getNextKey(@NonNull K key) throws ErrnoException;
/** Get the first key of the eBpf map. */
K getFirstKey() throws ErrnoException;
+ /** Returns {@code true} if this map contains no elements. */
+ default boolean isEmpty() throws ErrnoException {
+ return getFirstKey() == null;
+ }
+
/** Check whether a key exists in the map. */
boolean containsKey(@NonNull K key) throws ErrnoException;
@@ -70,13 +73,38 @@
/**
* Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+ * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
+ * other structural modifications to the map, such as adding entries or deleting other entries.
+ * Otherwise, iteration will result in undefined behaviour.
*/
- void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException;
+ default public void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException {
+ @Nullable K nextKey = getFirstKey();
- /** Clears the map. */
- void clear() throws ErrnoException;
+ while (nextKey != null) {
+ @NonNull final K curKey = nextKey;
+ @NonNull final V value = getValue(curKey);
+
+ nextKey = getNextKey(curKey);
+ action.accept(curKey, value);
+ }
+ }
+
+ /**
+ * Clears the map. The map may already be empty.
+ *
+ * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
+ * or if a non-ENOENT error occurred when deleting a key.
+ */
+ default public void clear() throws ErrnoException {
+ K key = getFirstKey();
+ while (key != null) {
+ deleteEntry(key); // ignores ENOENT.
+ key = getFirstKey();
+ }
+ }
/** Close for AutoCloseable. */
@Override
- void close() throws IOException;
+ default void close() throws IOException {
+ };
}
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
index ecdd440..b0f19e2 100644
--- a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
@@ -129,6 +129,10 @@
return true;
}
+ public IpPrefix getIpPrefix() {
+ return mIpPrefix;
+ }
+
/**
* Check whether or not IA Prefix option has 0 preferred and valid lifetimes.
*/
diff --git a/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp b/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
index cf09379..bcc3ded 100644
--- a/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
+++ b/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
@@ -30,86 +30,6 @@
using base::unique_fd;
-// If attach fails throw error and return false.
-static jboolean com_android_net_module_util_BpfUtil_attachProgramToCgroup(JNIEnv *env,
- jclass clazz, jint type, jstring bpfProgPath, jstring cgroupPath, jint flags) {
-
- ScopedUtfChars dirPath(env, cgroupPath);
- unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (cg_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to open the cgroup directory %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
-
- ScopedUtfChars bpfProg(env, bpfProgPath);
- unique_fd bpf_fd(bpf::retrieveProgram(bpfProg.c_str()));
- if (bpf_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to retrieve bpf program from %s: %s",
- bpfProg.c_str(), strerror(errno));
- return false;
- }
- if (bpf::attachProgram((bpf_attach_type) type, bpf_fd, cg_fd, flags)) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to attach bpf program %s to %s: %s",
- bpfProg.c_str(), dirPath.c_str(), strerror(errno));
- return false;
- }
- return true;
-}
-
-// If detach fails throw error and return false.
-static jboolean com_android_net_module_util_BpfUtil_detachProgramFromCgroup(JNIEnv *env,
- jclass clazz, jint type, jstring cgroupPath) {
-
- ScopedUtfChars dirPath(env, cgroupPath);
- unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (cg_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to open the cgroup directory %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
-
- if (bpf::detachProgram((bpf_attach_type) type, cg_fd)) {
- jniThrowExceptionFmt(env, "Failed to detach bpf program from %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
- return true;
-}
-
-// If detach single program fails throw error and return false.
-static jboolean com_android_net_module_util_BpfUtil_detachSingleProgramFromCgroup(JNIEnv *env,
- jclass clazz, jint type, jstring bpfProgPath, jstring cgroupPath) {
-
- ScopedUtfChars dirPath(env, cgroupPath);
- unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (cg_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to open the cgroup directory %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
-
- ScopedUtfChars bpfProg(env, bpfProgPath);
- unique_fd bpf_fd(bpf::retrieveProgram(bpfProg.c_str()));
- if (bpf_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to retrieve bpf program from %s: %s",
- bpfProg.c_str(), strerror(errno));
- return false;
- }
- if (bpf::detachSingleProgram((bpf_attach_type) type, bpf_fd, cg_fd)) {
- jniThrowExceptionFmt(env, "Failed to detach bpf program %s from %s: %s",
- bpfProg.c_str(), dirPath.c_str(), strerror(errno));
- return false;
- }
- return true;
-}
-
static jint com_android_net_module_util_BpfUtil_getProgramIdFromCgroup(JNIEnv *env,
jclass clazz, jint type, jstring cgroupPath) {
@@ -138,12 +58,6 @@
*/
static const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
- { "native_attachProgramToCgroup", "(ILjava/lang/String;Ljava/lang/String;I)Z",
- (void*) com_android_net_module_util_BpfUtil_attachProgramToCgroup },
- { "native_detachProgramFromCgroup", "(ILjava/lang/String;)Z",
- (void*) com_android_net_module_util_BpfUtil_detachProgramFromCgroup },
- { "native_detachSingleProgramFromCgroup", "(ILjava/lang/String;Ljava/lang/String;)Z",
- (void*) com_android_net_module_util_BpfUtil_detachSingleProgramFromCgroup },
{ "native_getProgramIdFromCgroup", "(ILjava/lang/String;)I",
(void*) com_android_net_module_util_BpfUtil_getProgramIdFromCgroup },
};
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index cc58367..82fb651 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2299,7 +2299,7 @@
}
@Override
- public int getBpfProgramId(final int attachType, @NonNull final String cgroupPath) {
+ public int getBpfProgramId(final int attachType) {
return 0;
}
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index 24aecdb..9a6fd38 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -169,10 +169,12 @@
lp.addDnsServer(InetAddress.getByName("4.4.4.4"));
// Send a validation event that is tracked on the alternate netId
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
- mDnsManager.updateTransportsForNetwork(TEST_NETID_ALTERNATE, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID_ALTERNATE, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID_ALTERNATE, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -205,7 +207,7 @@
InetAddress.parseNumericAddress("6.6.6.6"),
InetAddress.parseNumericAddress("2001:db8:66:66::1")
}));
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
fixedLp = new LinkProperties(lp);
@@ -242,7 +244,9 @@
// be tracked.
LinkProperties lp = new LinkProperties();
lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -256,7 +260,7 @@
// Validation event has untracked netId
mDnsManager.updatePrivateDns(new Network(TEST_NETID),
mDnsManager.getPrivateDnsConfig());
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -307,7 +311,7 @@
ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_OFF);
mDnsManager.updatePrivateDns(new Network(TEST_NETID),
mDnsManager.getPrivateDnsConfig());
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -352,7 +356,9 @@
lp.setInterfaceName(TEST_IFACENAME);
lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
lp.addDnsServer(InetAddress.getByName("4.4.4.4"));
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt b/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt
new file mode 100644
index 0000000..9f2d4d3
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt
@@ -0,0 +1,173 @@
+/*
+ * 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 com.android.server.net
+
+import android.util.IndentingPrintWriter
+import com.android.server.net.NetworkStatsEventLogger.MAX_POLL_REASON
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK
+import com.android.server.net.NetworkStatsEventLogger.PollEvent
+import com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf
+import com.android.testutils.DevSdkIgnoreRunner
+import java.io.StringWriter
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+const val TEST_PERSIST_FLAG = 0x101
+
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkStatsEventLoggerTest {
+ val logger = NetworkStatsEventLogger()
+ val stringWriter = TestStringWriter()
+ val pw = IndentingPrintWriter(stringWriter)
+
+ @Test
+ fun testDump_invalid() {
+ // Verify it won't crash.
+ logger.dump(pw)
+ // Clear output buffer.
+ stringWriter.getOutputAndClear()
+
+ // Verify log invalid event throws. And nothing output in the dump.
+ val invalidReasons = listOf(-1, MAX_POLL_REASON + 1)
+ invalidReasons.forEach {
+ assertFailsWith<IllegalArgumentException> {
+ logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(it))
+ }
+ logger.dumpRecentPollEvents(pw)
+ val output = stringWriter.getOutputAndClear()
+ assertStringNotContains(output, pollReasonNameOf(it))
+ }
+ }
+
+ @Test
+ fun testDump_valid() {
+ // Choose arbitrary set of reasons for testing.
+ val loggedReasons = listOf(
+ POLL_REASON_GLOBAL_ALERT,
+ POLL_REASON_FORCE_UPDATE,
+ POLL_REASON_DUMPSYS,
+ POLL_REASON_PERIODIC,
+ POLL_REASON_RAT_CHANGED
+ )
+ val nonLoggedReasons = listOf(
+ POLL_REASON_NETWORK_STATUS_CHANGED,
+ POLL_REASON_OPEN_SESSION,
+ POLL_REASON_REG_CALLBACK)
+
+ // Add some valid records.
+ loggedReasons.forEach {
+ logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(it))
+ }
+
+ // Collect dumps.
+ logger.dumpRecentPollEvents(pw)
+ val outputRecentEvents = stringWriter.getOutputAndClear()
+ logger.dumpPollCountsPerReason(pw)
+ val outputCountsPerReason = stringWriter.getOutputAndClear()
+
+ // Verify the output contains at least necessary information.
+ loggedReasons.forEach {
+ // Verify all events are shown in the recent event dump.
+ val eventString = PollEvent(it).toString()
+ assertStringContains(outputRecentEvents, TEST_PERSIST_FLAG.toString())
+ assertStringContains(eventString, pollReasonNameOf(it))
+ assertStringContains(outputRecentEvents, eventString)
+ // Verify counts are 1 for each reason.
+ assertCountForReason(outputCountsPerReason, it, 1)
+ }
+
+ // Verify the output remains untouched for other reasons.
+ nonLoggedReasons.forEach {
+ assertStringNotContains(outputRecentEvents, PollEvent(it).toString())
+ assertCountForReason(outputCountsPerReason, it, 0)
+ }
+ }
+
+ @Test
+ fun testDump_maxEventLogs() {
+ // Choose arbitrary reason.
+ val reasonToBeTested = POLL_REASON_PERIODIC
+ val repeatCount = NetworkStatsEventLogger.MAX_EVENTS_LOGS * 2
+
+ // Collect baseline.
+ logger.dumpRecentPollEvents(pw)
+ val lineCountBaseLine = getLineCount(stringWriter.getOutputAndClear())
+
+ repeat(repeatCount) {
+ logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(reasonToBeTested))
+ }
+
+ // Collect dump.
+ logger.dumpRecentPollEvents(pw)
+ val lineCountAfterTest = getLineCount(stringWriter.getOutputAndClear())
+
+ // Verify line count increment is limited.
+ assertEquals(
+ NetworkStatsEventLogger.MAX_EVENTS_LOGS,
+ lineCountAfterTest - lineCountBaseLine
+ )
+
+ // Verify count per reason increased for the testing reason.
+ logger.dumpPollCountsPerReason(pw)
+ val outputCountsPerReason = stringWriter.getOutputAndClear()
+ for (reason in 0..MAX_POLL_REASON) {
+ assertCountForReason(
+ outputCountsPerReason,
+ reason,
+ if (reason == reasonToBeTested) repeatCount else 0
+ )
+ }
+ }
+
+ private fun getLineCount(multilineString: String) = multilineString.lines().size
+
+ private fun assertStringContains(got: String, want: String) {
+ assertTrue(got.contains(want), "Wanted: $want, but got: $got")
+ }
+
+ private fun assertStringNotContains(got: String, unwant: String) {
+ assertFalse(got.contains(unwant), "Unwanted: $unwant, but got: $got")
+ }
+
+ /**
+ * Assert the reason and the expected count are at the same line.
+ */
+ private fun assertCountForReason(dump: String, reason: Int, expectedCount: Int) {
+ // Matches strings like "GLOBAL_ALERT: 50" but not too strict since the format might change.
+ val regex = Regex(pollReasonNameOf(reason) + "[^0-9]+" + expectedCount)
+ assertEquals(
+ 1,
+ regex.findAll(dump).count(),
+ "Unexpected output: $dump " + " for reason: " + pollReasonNameOf(reason)
+ )
+ }
+
+ class TestStringWriter : StringWriter() {
+ fun getOutputAndClear() = toString().also { buffer.setLength(0) }
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 9453617..e8d5c66 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -64,6 +64,8 @@
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
@@ -525,6 +527,11 @@
IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
return mSkDestroyListener;
}
+
+ @Override
+ public boolean supportEventLogger(@NonNull Context cts) {
+ return true;
+ }
};
}
@@ -2674,4 +2681,14 @@
doReturn(null).when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
doTestDumpIfaceStatsMap("unknown");
}
+
+ // Basic test to ensure event logger dump is called.
+ // Note that tests to ensure detailed correctness is done in the dedicated tests.
+ // See NetworkStatsEventLoggerTest.
+ @Test
+ public void testDumpEventLogger() {
+ setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
+ final String dump = getDump();
+ assertDumpContains(dump, pollReasonNameOf(POLL_REASON_RAT_CHANGED));
+ }
}
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 17a74f6..3eaebfa 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -1,7 +1,5 @@
{
- // TODO (b/297729075): graduate this test to presubmit once it meets the SLO requirements.
- // See go/test-mapping-slo-guide
- "postsubmit": [
+ "presubmit": [
{
"name": "CtsThreadNetworkTestCases"
}
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl b/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl
new file mode 100644
index 0000000..8bf12a4
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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;
+
+parcelable ActiveOperationalDataset;
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
new file mode 100644
index 0000000..c9b047a
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -0,0 +1,1165 @@
+/*
+ * 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 android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkState;
+import static com.android.net.module.util.HexDump.dumpHexString;
+
+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.security.SecureRandom;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Data interface for managing a Thread Active Operational Dataset.
+ *
+ * <p>An example usage of creating an Active Operational Dataset with random parameters:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset = ActiveOperationalDataset.createRandomDataset();
+ * }</pre>
+ *
+ * <p>or random Dataset with customized Network Name:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset =
+ * new ActiveOperationalDataset.Builder(ActiveOperationalDataset.createRandomDataset())
+ * .setNetworkName("MyThreadNet").build();
+ * }</pre>
+ *
+ * <p>If the Active Operational Dataset is already known as <a
+ * href="https://www.threadgroup.org">Thread TLVs</a>, you can simply use:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
+ * }</pre>
+ *
+ * @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;
+
+ private 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<ActiveOperationalDataset> CREATOR =
+ new Creator<>() {
+ @Override
+ public ActiveOperationalDataset createFromParcel(Parcel in) {
+ return ActiveOperationalDataset.fromThreadTlvs(in.createByteArray());
+ }
+
+ @Override
+ public ActiveOperationalDataset[] newArray(int size) {
+ return new ActiveOperationalDataset[size];
+ }
+ };
+
+ private final OperationalDatasetTimestamp mActiveTimestamp;
+ private final String mNetworkName;
+ private final byte[] mExtendedPanId;
+ private final int mPanId;
+ private final int mChannel;
+ private final int mChannelPage;
+ private final SparseArray<byte[]> mChannelMask;
+ private final byte[] mPskc;
+ private final byte[] mNetworkKey;
+ private final IpPrefix mMeshLocalPrefix;
+ private final SecurityPolicy mSecurityPolicy;
+ private final SparseArray<byte[]> mUnknownTlvs;
+
+ private ActiveOperationalDataset(Builder builder) {
+ this(
+ requireNonNull(builder.mActiveTimestamp),
+ requireNonNull(builder.mNetworkName),
+ requireNonNull(builder.mExtendedPanId),
+ requireNonNull(builder.mPanId),
+ requireNonNull(builder.mChannelPage),
+ requireNonNull(builder.mChannel),
+ requireNonNull(builder.mChannelMask),
+ requireNonNull(builder.mPskc),
+ requireNonNull(builder.mNetworkKey),
+ requireNonNull(builder.mMeshLocalPrefix),
+ requireNonNull(builder.mSecurityPolicy),
+ requireNonNull(builder.mUnknownTlvs));
+ }
+
+ private ActiveOperationalDataset(
+ OperationalDatasetTimestamp activeTimestamp,
+ String networkName,
+ byte[] extendedPanId,
+ int panId,
+ int channelPage,
+ int channel,
+ SparseArray<byte[]> channelMask,
+ byte[] pskc,
+ byte[] networkKey,
+ IpPrefix meshLocalPrefix,
+ SecurityPolicy securityPolicy,
+ SparseArray<byte[]> unknownTlvs) {
+ this.mActiveTimestamp = activeTimestamp;
+ this.mNetworkName = networkName;
+ this.mExtendedPanId = extendedPanId.clone();
+ this.mPanId = panId;
+ this.mChannel = channel;
+ this.mChannelPage = channelPage;
+ this.mChannelMask = deepCloneSparseArray(channelMask);
+ this.mPskc = pskc.clone();
+ this.mNetworkKey = networkKey.clone();
+ this.mMeshLocalPrefix = meshLocalPrefix;
+ this.mSecurityPolicy = securityPolicy;
+ this.mUnknownTlvs = deepCloneSparseArray(unknownTlvs);
+ }
+
+ /**
+ * Creates a new {@link ActiveOperationalDataset} object from a series of Thread TLVs.
+ *
+ * <p>{@code tlvs} can be obtained from the value of a Thread Active Operational Dataset TLV
+ * (see the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> 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<byte[]> decodeChannelMask(byte[] tlvValue) {
+ SparseArray<byte[]> channelMask = new SparseArray<>();
+ int i = 0;
+ while (i < tlvValue.length) {
+ int channelPage = tlvValue[i++] & 0xff;
+ if (i >= tlvValue.length) {
+ throw new IllegalArgumentException(
+ "Invalid channel mask - channel mask length is missing");
+ }
+
+ int maskLength = tlvValue[i++] & 0xff;
+ if (i + maskLength > tlvValue.length) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Invalid channel mask - channel mask is incomplete "
+ + "(offset = %d, length = %d, totalLength = %d)",
+ i, maskLength, tlvValue.length));
+ }
+
+ channelMask.put(channelPage, Arrays.copyOfRange(tlvValue, i, i + maskLength));
+ i += maskLength;
+ }
+ return channelMask;
+ }
+
+ private static void encodeChannelMask(
+ SparseArray<byte[]> channelMask, ByteArrayOutputStream outputStream) {
+ ByteArrayOutputStream entryStream = new ByteArrayOutputStream();
+
+ for (int i = 0; i < channelMask.size(); i++) {
+ int key = channelMask.keyAt(i);
+ byte[] value = channelMask.get(key);
+ entryStream.write(key);
+ entryStream.write(value.length);
+ entryStream.write(value, 0, value.length);
+ }
+
+ byte[] entries = entryStream.toByteArray();
+
+ outputStream.write(TYPE_CHANNEL_MASK);
+ outputStream.write(entries.length);
+ outputStream.write(entries, 0, entries.length);
+ }
+
+ /**
+ * Creates a new {@link ActiveOperationalDataset} object with randomized or default parameters.
+ *
+ * <p>The randomized (or default) value for each parameter:
+ *
+ * <ul>
+ * <li>{@code Active Timestamp} defaults to {@code new OperationalDatasetTimestamp(1, 0,
+ * false)}
+ * <li>{@code Network Name} defaults to "THREAD-PAN-<PAN ID decimal>", for example
+ * "THREAD-PAN-12345"
+ * <li>{@code Extended PAN ID} filled with randomly generated bytes
+ * <li>{@code PAN ID} randomly generated integer in range of [0, 0xfffe]
+ * <li>{@code Channel Page} defaults to {@link #CHANNEL_PAGE_24_GHZ}
+ * <li>{@code Channel} randomly selected channel in range of [{@link #CHANNEL_MIN_24_GHZ},
+ * {@link #CHANNEL_MAX_24_GHZ}]
+ * <li>{@code Channel Mask} all bits from {@link #CHANNEL_MIN_24_GHZ} to {@link
+ * #CHANNEL_MAX_24_GHZ} are set to {@code true}
+ * <li>{@code PSKc} filled with bytes generated by secure random generator
+ * <li>{@code Network Key} filled with bytes generated by secure random generator
+ * <li>{@code Mesh-local Prefix} filled with randomly generated bytes except that the first
+ * byte is always set to {@code 0xfd}
+ * <li>{@code Security Policy} defaults to {@code new SecurityPolicy(
+ * DEFAULT_ROTATION_TIME_HOURS, new byte[]{(byte)0xff, (byte)0xf8})}. This is the default
+ * values required by the Thread 1.2 specification
+ * </ul>
+ *
+ * <p>This method is the recommended way to create a randomized operational dataset for a new
+ * Thread network. It may be desired to change one or more of the generated value(s). For
+ * example, to use a more meaningful Network Name. To do that, create a new {@link Builder}
+ * object from this dataset with {@link Builder#Builder(ActiveOperationalDataset)} and override
+ * the value with the setters of {@link Builder}.
+ *
+ * <p>Note that it's highly discouraged to change the randomly generated Extended PAN ID,
+ * Network Key or PSKc, as it will compromise the security of a Thread network.
+ */
+ @NonNull
+ public static ActiveOperationalDataset createRandomDataset() {
+ return createRandomDataset(new Random(Instant.now().toEpochMilli()), new SecureRandom());
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public static ActiveOperationalDataset createRandomDataset(
+ Random random, SecureRandom secureRandom) {
+ int panId = random.nextInt(/* bound= */ 0xffff);
+ byte[] meshLocalPrefix = newRandomBytes(random, LENGTH_MESH_LOCAL_PREFIX_BITS / 8);
+ meshLocalPrefix[0] = MESH_LOCAL_PREFIX_FIRST_BYTE;
+
+ SparseArray<byte[]> channelMask = new SparseArray<>(1);
+ channelMask.put(CHANNEL_PAGE_24_GHZ, new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+
+ return new Builder()
+ .setActiveTimestamp(
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 1,
+ /* ticks= */ 0,
+ /* isAuthoritativeSource= */ false))
+ .setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
+ .setPanId(panId)
+ .setNetworkName("THREAD-PAN-" + panId)
+ .setChannel(
+ CHANNEL_PAGE_24_GHZ,
+ random.nextInt(CHANNEL_MAX_24_GHZ - CHANNEL_MIN_24_GHZ + 1)
+ + CHANNEL_MIN_24_GHZ)
+ .setChannelMask(channelMask)
+ .setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
+ .setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
+ .setMeshLocalPrefix(meshLocalPrefix)
+ .setSecurityPolicy(
+ new SecurityPolicy(
+ DEFAULT_ROTATION_TIME_HOURS, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .build();
+ }
+
+ private static byte[] newRandomBytes(Random random, int length) {
+ byte[] result = new byte[length];
+ random.nextBytes(result);
+ return result;
+ }
+
+ private static boolean areByteSparseArraysEqual(
+ @NonNull SparseArray<byte[]> first, @NonNull SparseArray<byte[]> second) {
+ if (first == second) {
+ return true;
+ } else if (first == null || second == null) {
+ return false;
+ } else if (first.size() != second.size()) {
+ return false;
+ } else {
+ for (int i = 0; i < first.size(); i++) {
+ int firstKey = first.keyAt(i);
+ int secondKey = second.keyAt(i);
+ if (firstKey != secondKey) {
+ return false;
+ }
+
+ byte[] firstValue = first.valueAt(i);
+ byte[] secondValue = second.valueAt(i);
+ if (!Arrays.equals(firstValue, secondValue)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ /** An easy-to-use wrapper of {@link Arrays#deepHashCode}. */
+ private static int deepHashCode(Object... values) {
+ return Arrays.deepHashCode(values);
+ }
+
+ /**
+ * Converts this {@link ActiveOperationalDataset} object to a series of Thread TLVs.
+ *
+ * <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> 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<byte[]> getChannelMask() {
+ return deepCloneSparseArray(mChannelMask);
+ }
+
+ private static SparseArray<byte[]> deepCloneSparseArray(SparseArray<byte[]> src) {
+ SparseArray<byte[]> dst = new SparseArray<>(src.size());
+ for (int i = 0; i < src.size(); i++) {
+ dst.put(src.keyAt(i), src.valueAt(i).clone());
+ }
+ return dst;
+ }
+
+ /** Returns the PSKc. */
+ @NonNull
+ @Size(LENGTH_PSKC)
+ public byte[] getPskc() {
+ return mPskc.clone();
+ }
+
+ /** Returns the Network Key. */
+ @NonNull
+ @Size(LENGTH_NETWORK_KEY)
+ public byte[] getNetworkKey() {
+ return mNetworkKey.clone();
+ }
+
+ /**
+ * Returns the Mesh-local Prefix. The length of the returned prefix is always {@link
+ * #LENGTH_MESH_LOCAL_PREFIX_BITS}.
+ */
+ @NonNull
+ public IpPrefix getMeshLocalPrefix() {
+ return mMeshLocalPrefix;
+ }
+
+ /** Returns the Security Policy. */
+ @NonNull
+ public SecurityPolicy getSecurityPolicy() {
+ return mSecurityPolicy;
+ }
+
+ /**
+ * Returns Thread TLVs which are not recognized by this device. The returned {@link SparseArray}
+ * associates TLV values to their keys.
+ *
+ * @hide
+ */
+ @NonNull
+ public SparseArray<byte[]> getUnknownTlvs() {
+ return deepCloneSparseArray(mUnknownTlvs);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeByteArray(toThreadTlvs());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (!(other instanceof ActiveOperationalDataset)) {
+ return false;
+ } else {
+ ActiveOperationalDataset otherDataset = (ActiveOperationalDataset) other;
+ return mActiveTimestamp.equals(otherDataset.mActiveTimestamp)
+ && mNetworkName.equals(otherDataset.mNetworkName)
+ && Arrays.equals(mExtendedPanId, otherDataset.mExtendedPanId)
+ && mPanId == otherDataset.mPanId
+ && mChannelPage == otherDataset.mChannelPage
+ && mChannel == otherDataset.mChannel
+ && areByteSparseArraysEqual(mChannelMask, otherDataset.mChannelMask)
+ && Arrays.equals(mPskc, otherDataset.mPskc)
+ && Arrays.equals(mNetworkKey, otherDataset.mNetworkKey)
+ && mMeshLocalPrefix.equals(otherDataset.mMeshLocalPrefix)
+ && mSecurityPolicy.equals(otherDataset.mSecurityPolicy)
+ && areByteSparseArraysEqual(mUnknownTlvs, otherDataset.mUnknownTlvs);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return deepHashCode(
+ mActiveTimestamp,
+ mNetworkName,
+ mExtendedPanId,
+ mPanId,
+ mChannel,
+ mChannelPage,
+ mChannelMask,
+ mPskc,
+ mNetworkKey,
+ mMeshLocalPrefix,
+ mSecurityPolicy);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{networkName=")
+ .append(getNetworkName())
+ .append(", extendedPanId=")
+ .append(dumpHexString(getExtendedPanId()))
+ .append(", panId=")
+ .append(getPanId())
+ .append(", channel=")
+ .append(getChannel())
+ .append(", activeTimestamp=")
+ .append(getActiveTimestamp())
+ .append("}");
+ return sb.toString();
+ }
+
+ /** The builder for creating {@link ActiveOperationalDataset} objects. */
+ public static final class Builder {
+ private OperationalDatasetTimestamp mActiveTimestamp;
+ private String mNetworkName;
+ private byte[] mExtendedPanId;
+ private Integer mPanId;
+ private Integer mChannel;
+ private Integer mChannelPage;
+ private SparseArray<byte[]> mChannelMask;
+ private byte[] mPskc;
+ private byte[] mNetworkKey;
+ private IpPrefix mMeshLocalPrefix;
+ private SecurityPolicy mSecurityPolicy;
+ private SparseArray<byte[]> mUnknownTlvs;
+
+ /**
+ * Creates a {@link Builder} object with values from an {@link ActiveOperationalDataset}
+ * object.
+ */
+ public Builder(@NonNull ActiveOperationalDataset activeOpDataset) {
+ requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
+
+ this.mActiveTimestamp = activeOpDataset.mActiveTimestamp;
+ this.mNetworkName = activeOpDataset.mNetworkName;
+ this.mExtendedPanId = activeOpDataset.mExtendedPanId.clone();
+ this.mPanId = activeOpDataset.mPanId;
+ this.mChannel = activeOpDataset.mChannel;
+ this.mChannelPage = activeOpDataset.mChannelPage;
+ this.mChannelMask = deepCloneSparseArray(activeOpDataset.mChannelMask);
+ this.mPskc = activeOpDataset.mPskc.clone();
+ this.mNetworkKey = activeOpDataset.mNetworkKey.clone();
+ this.mMeshLocalPrefix = activeOpDataset.mMeshLocalPrefix;
+ this.mSecurityPolicy = activeOpDataset.mSecurityPolicy;
+ this.mUnknownTlvs = deepCloneSparseArray(activeOpDataset.mUnknownTlvs);
+ }
+
+ /**
+ * Creates an empty {@link Builder} object.
+ *
+ * <p>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) {
+ requireNonNull(networkName, "networkName cannot be null");
+
+ int nameLength = networkName.getBytes(UTF_8).length;
+ checkArgument(
+ nameLength >= LENGTH_MIN_NETWORK_NAME_BYTES
+ && nameLength <= LENGTH_MAX_NETWORK_NAME_BYTES,
+ "Invalid network name (length = %d, expectedLengthRange = [%d, %d])",
+ nameLength,
+ LENGTH_MIN_NETWORK_NAME_BYTES,
+ LENGTH_MAX_NETWORK_NAME_BYTES);
+ this.mNetworkName = networkName;
+ return this;
+ }
+
+ /**
+ * Sets the Extended PAN ID.
+ *
+ * <p>Use with caution. A randomly generated 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 ActiveOperationalDataset#createRandomDataset} 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.
+ *
+ * <p>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<byte[]> channelMask) {
+ requireNonNull(channelMask, "channelMask cannot be null");
+ checkArgument(channelMask.size() > 0, "channelMask is empty");
+ this.mChannelMask = deepCloneSparseArray(channelMask);
+ return this;
+ }
+
+ /**
+ * Sets the PSKc.
+ *
+ * <p>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
+ * ActiveOperationalDataset#createRandomDataset} 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.
+ *
+ * <p>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 ActiveOperationalDataset#createRandomDataset} 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;
+ }
+
+ @NonNull
+ private 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<byte[]> unknownTlvs) {
+ requireNonNull(unknownTlvs, "unknownTlvs cannot be null");
+ mUnknownTlvs = deepCloneSparseArray(unknownTlvs);
+ return this;
+ }
+
+ /** Adds one more unknown TLV. @hide */
+ @VisibleForTesting
+ @NonNull
+ public Builder addUnknownTlv(int type, byte[] value) {
+ mUnknownTlvs.put(type, value);
+ return this;
+ }
+
+ /**
+ * Creates a new {@link ActiveOperationalDataset} object.
+ *
+ * @throws IllegalStateException if any of the fields isn't set or the total length exceeds
+ * {@link #LENGTH_MAX_DATASET_TLVS} bytes
+ */
+ @NonNull
+ public ActiveOperationalDataset build() {
+ checkState(mActiveTimestamp != null, "Active Timestamp is missing");
+ checkState(mNetworkName != null, "Network Name is missing");
+ checkState(mExtendedPanId != null, "Extended PAN ID is missing");
+ checkState(mPanId != null, "PAN ID is missing");
+ checkState(mChannel != null, "Channel is missing");
+ checkState(mChannelPage != null, "Channel Page is missing");
+ checkState(mChannelMask.size() != 0, "Channel Mask is missing");
+ checkState(mPskc != null, "PSKc is missing");
+ checkState(mNetworkKey != null, "Network Key is missing");
+ checkState(mMeshLocalPrefix != null, "Mesh Local Prefix is missing");
+ checkState(mSecurityPolicy != null, "Security Policy is missing");
+
+ int length = getTotalDatasetLength();
+ if (length > LENGTH_MAX_DATASET_TLVS) {
+ throw new IllegalStateException(
+ String.format(
+ "Total dataset length exceeds max length %d (actual is %d)",
+ LENGTH_MAX_DATASET_TLVS, length));
+ }
+
+ return new ActiveOperationalDataset(this);
+ }
+
+ private int getTotalDatasetLength() {
+ int length =
+ 2 * 9 // 9 fields with 1 byte of type and 1 byte of length
+ + OperationalDatasetTimestamp.LENGTH_TIMESTAMP
+ + mNetworkName.getBytes(UTF_8).length
+ + LENGTH_EXTENDED_PAN_ID
+ + LENGTH_PAN_ID
+ + LENGTH_CHANNEL
+ + LENGTH_PSKC
+ + LENGTH_NETWORK_KEY
+ + LENGTH_MESH_LOCAL_PREFIX_BITS / 8
+ + mSecurityPolicy.toTlvValue().length;
+
+ for (int i = 0; i < mChannelMask.size(); i++) {
+ length += 2 + mChannelMask.valueAt(i).length;
+ }
+
+ // For the type and length bytes of the Channel Mask TLV because the masks are encoded
+ // as TLVs in TLV.
+ length += 2;
+
+ for (int i = 0; i < mUnknownTlvs.size(); i++) {
+ length += 2 + mUnknownTlvs.valueAt(i).length;
+ }
+
+ return length;
+ }
+ }
+
+ /**
+ * The Security Policy of Thread Operational Dataset which provides an administrator with a way
+ * to enable or disable certain security related behaviors.
+ */
+ public static final class SecurityPolicy {
+ /** The default Rotation Time in hours. */
+ public static final int DEFAULT_ROTATION_TIME_HOURS = 672;
+ /** The minimum length of Security Policy flags in bytes. */
+ public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1;
+ /** The length of Rotation Time TLV value in bytes. */
+ private static final int LENGTH_SECURITY_POLICY_ROTATION_TIME = 2;
+
+ private final int mRotationTimeHours;
+ private final byte[] mFlags;
+
+ /**
+ * Creates a new {@link SecurityPolicy} object.
+ *
+ * @param rotationTimeHours the value for Thread key rotation in hours. Must be in range of
+ * 0x1-0xffff.
+ * @param flags security policy flags with length of either 1 byte for Thread 1.1 or 2 bytes
+ * for Thread 1.2 or higher.
+ * @throws IllegalArgumentException if {@code rotationTimeHours} is not in range of
+ * 0x1-0xffff or length of {@code flags} is smaller than {@link
+ * #LENGTH_MIN_SECURITY_POLICY_FLAGS}.
+ */
+ public SecurityPolicy(
+ @IntRange(from = 0x1, to = 0xffff) int rotationTimeHours,
+ @NonNull @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[] flags) {
+ requireNonNull(flags, "flags cannot be null");
+ checkArgument(
+ rotationTimeHours >= 1 && rotationTimeHours <= 0xffff,
+ "Rotation time exceeds allowed range (rotationTimeHours = %d, allowedRange ="
+ + " [0x1, 0xffff])",
+ rotationTimeHours);
+ checkArgument(
+ flags.length >= LENGTH_MIN_SECURITY_POLICY_FLAGS,
+ "Invalid security policy flags length (length = %d, minimumLength = %d)",
+ flags.length,
+ LENGTH_MIN_SECURITY_POLICY_FLAGS);
+ this.mRotationTimeHours = rotationTimeHours;
+ this.mFlags = flags.clone();
+ }
+
+ /**
+ * Creates a new {@link SecurityPolicy} object from the Security Policy TLV value.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ @NonNull
+ public static SecurityPolicy fromTlvValue(byte[] encodedSecurityPolicy) {
+ checkArgument(
+ encodedSecurityPolicy.length
+ >= LENGTH_SECURITY_POLICY_ROTATION_TIME
+ + LENGTH_MIN_SECURITY_POLICY_FLAGS,
+ "Invalid Security Policy TLV length (length = %d, minimumLength = %d)",
+ encodedSecurityPolicy.length,
+ LENGTH_SECURITY_POLICY_ROTATION_TIME + LENGTH_MIN_SECURITY_POLICY_FLAGS);
+
+ return new SecurityPolicy(
+ ((encodedSecurityPolicy[0] & 0xff) << 8) | (encodedSecurityPolicy[1] & 0xff),
+ Arrays.copyOfRange(
+ encodedSecurityPolicy,
+ LENGTH_SECURITY_POLICY_ROTATION_TIME,
+ encodedSecurityPolicy.length));
+ }
+
+ /**
+ * Converts this {@link SecurityPolicy} object to Security Policy TLV value.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ @NonNull
+ public byte[] toTlvValue() {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ result.write(mRotationTimeHours >> 8);
+ result.write(mRotationTimeHours);
+ result.write(mFlags, 0, mFlags.length);
+ return result.toByteArray();
+ }
+
+ /** Returns the Security Policy Rotation Time in hours. */
+ @IntRange(from = 0x1, to = 0xffff)
+ public int getRotationTimeHours() {
+ return mRotationTimeHours;
+ }
+
+ /** Returns 1 byte flags for Thread 1.1 or 2 bytes flags for Thread 1.2. */
+ @NonNull
+ @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS)
+ public byte[] getFlags() {
+ return mFlags.clone();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof SecurityPolicy)) {
+ return false;
+ } else {
+ SecurityPolicy otherSecurityPolicy = (SecurityPolicy) other;
+ return mRotationTimeHours == otherSecurityPolicy.mRotationTimeHours
+ && Arrays.equals(mFlags, otherSecurityPolicy.mFlags);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return deepHashCode(mRotationTimeHours, mFlags);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{rotation=")
+ .append(mRotationTimeHours)
+ .append(", flags=")
+ .append(dumpHexString(mFlags))
+ .append("}");
+ return sb.toString();
+ }
+ }
+}
diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
new file mode 100644
index 0000000..bda9373
--- /dev/null
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -0,0 +1,220 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * The timestamp of Thread Operational Dataset.
+ *
+ * @see ActiveOperationalDataset
+ * @see PendingOperationalDataset
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class OperationalDatasetTimestamp {
+ /** @hide */
+ public static final int LENGTH_TIMESTAMP = Long.BYTES;
+
+ private static final long TICKS_UPPER_BOUND = 0x8000;
+
+ private final Instant mInstant;
+ private final boolean mIsAuthoritativeSource;
+
+ /**
+ * Creates a new {@link OperationalDatasetTimestamp} object from an {@link Instant}.
+ *
+ * <p>The {@code seconds} is set to {@code instant.getEpochSecond()}, {@code ticks} is set to
+ * {@link instant#getNano()} based on frequency of 32768 Hz, and {@code isAuthoritativeSource}
+ * is set to {@code true}.
+ *
+ * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
+ * 0xffffffffffffL}
+ */
+ @NonNull
+ public static OperationalDatasetTimestamp fromInstant(@NonNull Instant instant) {
+ return new OperationalDatasetTimestamp(instant, /* isAuthoritativeSource= */ true);
+ }
+
+ /** Converts this {@link OperationalDatasetTimestamp} object to an {@link Instant}. */
+ @NonNull
+ public Instant toInstant() {
+ return mInstant;
+ }
+
+ /**
+ * Creates a new {@link OperationalDatasetTimestamp} object from the OperationalDatasetTimestamp
+ * TLV value.
+ *
+ * @hide
+ */
+ @NonNull
+ public static OperationalDatasetTimestamp fromTlvValue(@NonNull byte[] encodedTimestamp) {
+ requireNonNull(encodedTimestamp, "encodedTimestamp cannot be null");
+ checkArgument(
+ encodedTimestamp.length == LENGTH_TIMESTAMP,
+ "Invalid Thread OperationalDatasetTimestamp length (length = %d,"
+ + " expectedLength=%d)",
+ encodedTimestamp.length,
+ LENGTH_TIMESTAMP);
+ long longTimestamp = ByteBuffer.wrap(encodedTimestamp).getLong();
+ return new OperationalDatasetTimestamp(
+ (longTimestamp >> 16) & 0x0000ffffffffffffL,
+ (int) ((longTimestamp >> 1) & 0x7fffL),
+ (longTimestamp & 0x01) != 0);
+ }
+
+ /**
+ * Converts this {@link OperationalDatasetTimestamp} object to Thread TLV value.
+ *
+ * @hide
+ */
+ @NonNull
+ public byte[] toTlvValue() {
+ byte[] tlv = new byte[LENGTH_TIMESTAMP];
+ ByteBuffer buffer = ByteBuffer.wrap(tlv);
+ long encodedValue =
+ (mInstant.getEpochSecond() << 16)
+ | ((mInstant.getNano() * TICKS_UPPER_BOUND / 1000000000L) << 1)
+ | (mIsAuthoritativeSource ? 1 : 0);
+ buffer.putLong(encodedValue);
+ return tlv;
+ }
+
+ /**
+ * Creates a new {@link OperationalDatasetTimestamp} object.
+ *
+ * @param seconds the value encodes a Unix Time value. Must be in the range of
+ * 0x0-0xffffffffffffL
+ * @param ticks the value encodes the fractional Unix Time value in 32.768 kHz resolution. Must
+ * be in the range of 0x0-0x7fff
+ * @param isAuthoritativeSource the flag indicates the time was obtained from an authoritative
+ * source: either NTP (Network Time Protocol), GPS (Global Positioning System), cell
+ * network, or other method
+ * @throws IllegalArgumentException if the {@code seconds} is not in range of
+ * 0x0-0xffffffffffffL or {@code ticks} is not in range of 0x0-0x7fff
+ */
+ public OperationalDatasetTimestamp(
+ @IntRange(from = 0x0, to = 0xffffffffffffL) long seconds,
+ @IntRange(from = 0x0, to = 0x7fff) int ticks,
+ boolean isAuthoritativeSource) {
+ this(makeInstant(seconds, ticks), isAuthoritativeSource);
+ }
+
+ private static Instant makeInstant(long seconds, int ticks) {
+ checkArgument(
+ seconds >= 0 && seconds <= 0xffffffffffffL,
+ "seconds exceeds allowed range (seconds = %d,"
+ + " allowedRange = [0x0, 0xffffffffffffL])",
+ seconds);
+ checkArgument(
+ ticks >= 0 && ticks <= 0x7fff,
+ "ticks exceeds allowed ranged (ticks = %d, allowedRange" + " = [0x0, 0x7fff])",
+ ticks);
+ long nanos = Math.round((double) ticks * 1000000000L / TICKS_UPPER_BOUND);
+ return Instant.ofEpochSecond(seconds, nanos);
+ }
+
+ /**
+ * Creates new {@link OperationalDatasetTimestamp} object.
+ *
+ * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
+ * 0xffffffffffffL}
+ */
+ private OperationalDatasetTimestamp(@NonNull Instant instant, boolean isAuthoritativeSource) {
+ requireNonNull(instant, "instant cannot be null");
+ long seconds = instant.getEpochSecond();
+ checkArgument(
+ seconds >= 0 && seconds <= 0xffffffffffffL,
+ "instant seconds exceeds allowed range (seconds = %d, allowedRange = [0x0,"
+ + " 0xffffffffffffL])",
+ seconds);
+ mInstant = instant;
+ mIsAuthoritativeSource = isAuthoritativeSource;
+ }
+
+ /**
+ * Returns the rounded ticks converted from the nano seconds.
+ *
+ * <p>Note that rhe return value can be as large as {@code TICKS_UPPER_BOUND}.
+ */
+ private static int getRoundedTicks(long nanos) {
+ return (int) Math.round((double) nanos * TICKS_UPPER_BOUND / 1000000000L);
+ }
+
+ /** Returns the seconds portion of the timestamp. */
+ public @IntRange(from = 0x0, to = 0xffffffffffffL) long getSeconds() {
+ return mInstant.getEpochSecond() + getRoundedTicks(mInstant.getNano()) / TICKS_UPPER_BOUND;
+ }
+
+ /** Returns the ticks portion of the timestamp. */
+ public @IntRange(from = 0x0, to = 0x7fff) int getTicks() {
+ // the rounded ticks can be 0x8000 if mInstant.getNano() >= 999984742
+ return (int) (getRoundedTicks(mInstant.getNano()) % TICKS_UPPER_BOUND);
+ }
+
+ /** Returns {@code true} if the timestamp comes from an authoritative source. */
+ public boolean isAuthoritativeSource() {
+ return mIsAuthoritativeSource;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{seconds=")
+ .append(getSeconds())
+ .append(", ticks=")
+ .append(getTicks())
+ .append(", isAuthoritativeSource=")
+ .append(isAuthoritativeSource())
+ .append(", instant=")
+ .append(toInstant())
+ .append("}");
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof OperationalDatasetTimestamp)) {
+ return false;
+ } else {
+ OperationalDatasetTimestamp otherTimestamp = (OperationalDatasetTimestamp) other;
+ return mInstant.equals(otherTimestamp.mInstant)
+ && mIsAuthoritativeSource == otherTimestamp.mIsAuthoritativeSource;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mInstant, mIsAuthoritativeSource);
+ }
+}
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl b/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl
new file mode 100644
index 0000000..e5bc05e
--- /dev/null
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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;
+
+parcelable PendingOperationalDataset;
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
new file mode 100644
index 0000000..4762d7f
--- /dev/null
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -0,0 +1,226 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * Data interface for managing a Thread Pending Operational Dataset.
+ *
+ * <p>The Pending Operational Dataset represents an Operational Dataset which will become Active in
+ * a given delay. This is typically used to deploy new network parameters (e.g. Network Key or
+ * Channel) to all devices in the network.
+ *
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class PendingOperationalDataset implements Parcelable {
+ // Value defined in Thread spec 8.10.1.16
+ private static final int TYPE_PENDING_TIMESTAMP = 51;
+
+ // Values defined in Thread spec 8.10.1.17
+ private static final int TYPE_DELAY_TIMER = 52;
+ private static final int LENGTH_DELAY_TIMER_BYTES = 4;
+
+ @NonNull
+ public static final Creator<PendingOperationalDataset> CREATOR =
+ new Creator<>() {
+ @Override
+ public PendingOperationalDataset createFromParcel(Parcel in) {
+ return PendingOperationalDataset.fromThreadTlvs(in.createByteArray());
+ }
+
+ @Override
+ public PendingOperationalDataset[] newArray(int size) {
+ return new PendingOperationalDataset[size];
+ }
+ };
+
+ @NonNull private final ActiveOperationalDataset mActiveOpDataset;
+ @NonNull private final OperationalDatasetTimestamp mPendingTimestamp;
+ @NonNull private final Duration mDelayTimer;
+
+ /** Creates a new {@link PendingOperationalDataset} object. */
+ public PendingOperationalDataset(
+ @NonNull ActiveOperationalDataset activeOpDataset,
+ @NonNull OperationalDatasetTimestamp pendingTimestamp,
+ @NonNull Duration delayTimer) {
+ requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
+ requireNonNull(pendingTimestamp, "pendingTimestamp cannot be null");
+ requireNonNull(delayTimer, "delayTimer cannot be null");
+ this.mActiveOpDataset = activeOpDataset;
+ this.mPendingTimestamp = pendingTimestamp;
+ this.mDelayTimer = delayTimer;
+ }
+
+ /**
+ * Creates a new {@link PendingOperationalDataset} object from a series of Thread TLVs.
+ *
+ * <p>{@code tlvs} can be obtained from the value of a Thread Pending Operational Dataset TLV
+ * (see the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> for the definition) or the return value of {@link #toThreadTlvs}.
+ *
+ * @throws IllegalArgumentException if {@code tlvs} is malformed or contains an invalid Thread
+ * TLV
+ */
+ @NonNull
+ public static PendingOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
+ requireNonNull(tlvs, "tlvs cannot be null");
+
+ SparseArray<byte[]> newUnknownTlvs = new SparseArray<>();
+ OperationalDatasetTimestamp pendingTimestamp = null;
+ Duration delayTimer = null;
+ ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(tlvs);
+ SparseArray<byte[]> unknownTlvs = activeDataset.getUnknownTlvs();
+ for (int i = 0; i < unknownTlvs.size(); i++) {
+ int key = unknownTlvs.keyAt(i);
+ byte[] value = unknownTlvs.valueAt(i);
+ switch (key) {
+ case TYPE_PENDING_TIMESTAMP:
+ pendingTimestamp = OperationalDatasetTimestamp.fromTlvValue(value);
+ break;
+ case TYPE_DELAY_TIMER:
+ checkArgument(
+ value.length == LENGTH_DELAY_TIMER_BYTES,
+ "Invalid delay timer (length = %d, expectedLength = %d)",
+ value.length,
+ LENGTH_DELAY_TIMER_BYTES);
+ int millis = ByteBuffer.wrap(value).getInt();
+ delayTimer = Duration.ofMillis(Integer.toUnsignedLong(millis));
+ break;
+ default:
+ newUnknownTlvs.put(key, value);
+ break;
+ }
+ }
+
+ if (pendingTimestamp == null) {
+ throw new IllegalArgumentException("Pending Timestamp is missing");
+ }
+ if (delayTimer == null) {
+ throw new IllegalArgumentException("Delay Timer is missing");
+ }
+
+ activeDataset =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setUnknownTlvs(newUnknownTlvs)
+ .build();
+ return new PendingOperationalDataset(activeDataset, pendingTimestamp, delayTimer);
+ }
+
+ /** Returns the Active Operational Dataset. */
+ @NonNull
+ public ActiveOperationalDataset getActiveOperationalDataset() {
+ return mActiveOpDataset;
+ }
+
+ /** Returns the Pending Timestamp. */
+ @NonNull
+ public OperationalDatasetTimestamp getPendingTimestamp() {
+ return mPendingTimestamp;
+ }
+
+ /** Returns the Delay Timer. */
+ @NonNull
+ public Duration getDelayTimer() {
+ return mDelayTimer;
+ }
+
+ /**
+ * Converts this {@link PendingOperationalDataset} object to a series of Thread TLVs.
+ *
+ * <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> for the definition of the Thread TLV format.
+ */
+ @NonNull
+ public byte[] toThreadTlvs() {
+ ByteArrayOutputStream dataset = new ByteArrayOutputStream();
+
+ byte[] activeDatasetBytes = mActiveOpDataset.toThreadTlvs();
+ dataset.write(activeDatasetBytes, 0, activeDatasetBytes.length);
+
+ dataset.write(TYPE_PENDING_TIMESTAMP);
+ byte[] pendingTimestampBytes = mPendingTimestamp.toTlvValue();
+ dataset.write(pendingTimestampBytes.length);
+ dataset.write(pendingTimestampBytes, 0, pendingTimestampBytes.length);
+
+ dataset.write(TYPE_DELAY_TIMER);
+ byte[] delayTimerBytes = new byte[LENGTH_DELAY_TIMER_BYTES];
+ ByteBuffer.wrap(delayTimerBytes).putInt((int) mDelayTimer.toMillis());
+ dataset.write(delayTimerBytes.length);
+ dataset.write(delayTimerBytes, 0, delayTimerBytes.length);
+
+ return dataset.toByteArray();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof PendingOperationalDataset)) {
+ return false;
+ } else {
+ PendingOperationalDataset otherDataset = (PendingOperationalDataset) other;
+ return mActiveOpDataset.equals(otherDataset.mActiveOpDataset)
+ && mPendingTimestamp.equals(otherDataset.mPendingTimestamp)
+ && mDelayTimer.equals(otherDataset.mDelayTimer);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mActiveOpDataset, mPendingTimestamp, mDelayTimer);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{activeDataset=")
+ .append(getActiveOperationalDataset())
+ .append(", pendingTimestamp=")
+ .append(getPendingTimestamp())
+ .append(", delayTimer=")
+ .append(getDelayTimer())
+ .append("}");
+ return sb.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeByteArray(toThreadTlvs());
+ }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 9db8132..7575757 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -34,7 +34,7 @@
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
-public class ThreadNetworkController {
+public final class ThreadNetworkController {
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 3e8288c..c3bdbd7 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -38,7 +38,7 @@
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
@SystemService(ThreadNetworkManager.SERVICE_NAME)
-public class ThreadNetworkManager {
+public final class ThreadNetworkManager {
/**
* This value tracks {@link Context#THREAD_NETWORK_SERVICE}.
*
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 278798e..ce770e0 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -37,6 +37,7 @@
"androidx.test.ext.junit",
"compatibility-device-util-axt",
"ctstestrunner-axt",
+ "guava-android-testlib",
"net-tests-utils",
"truth",
],
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index 5ba605f..ffc181c 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -47,5 +47,7 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.net.thread.cts" />
+ <!-- Ignores tests introduced by guava-android-testlib -->
+ <option name="exclude-annotation" value="org.junit.Ignore"/>
</test>
</configuration>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
new file mode 100644
index 0000000..39df21b
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
@@ -0,0 +1,737 @@
+/*
+ * 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.cts;
+
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ActiveOperationalDataset.Builder;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+
+/** CTS tests for {@link ActiveOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ActiveOperationalDatasetTest {
+ private static final int TYPE_ACTIVE_TIMESTAMP = 14;
+ private static final int TYPE_CHANNEL = 0;
+ private static final int TYPE_CHANNEL_MASK = 53;
+ private static final int TYPE_EXTENDED_PAN_ID = 2;
+ private static final int TYPE_MESH_LOCAL_PREFIX = 7;
+ private static final int TYPE_NETWORK_KEY = 5;
+ private static final int TYPE_NETWORK_NAME = 3;
+ private static final int TYPE_PAN_ID = 1;
+ private static final int TYPE_PSKC = 4;
+ private static final int TYPE_SECURITY_POLICY = 12;
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ private static final byte[] VALID_DATASET =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+
+ private static byte[] removeTlv(byte[] dataset, int type) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream(dataset.length);
+ int i = 0;
+ while (i < dataset.length) {
+ int ty = dataset[i++] & 0xff;
+ byte length = dataset[i++];
+ if (ty != type) {
+ byte[] value = Arrays.copyOfRange(dataset, i, i + length);
+ os.write(ty);
+ os.write(length);
+ os.writeBytes(value);
+ }
+ i += length;
+ }
+ return os.toByteArray();
+ }
+
+ private static byte[] addTlv(byte[] dataset, String tlvHex) {
+ return Bytes.concat(dataset, base16().decode(tlvHex));
+ }
+
+ private static byte[] replaceTlv(byte[] dataset, int type, String newTlvHex) {
+ return addTlv(removeTlv(dataset, type), newTlvHex);
+ }
+
+ @Test
+ public void parcelable_parcelingIsLossLess() {
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET);
+
+ assertParcelingIsLossless(dataset);
+ }
+
+ @Test
+ public void fromThreadTlvs_tooLongTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = new byte[255];
+ invalidTlv[0] = (byte) 0xff;
+
+ // This is invalid because the TLV has max total length of 254 bytes and the value length
+ // can't exceeds 252 ( = 254 - 1 - 1)
+ invalidTlv[1] = (byte) 253;
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidNetworkKeyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_KEY, "05080000000000000000");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noNetworkKeyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_KEY);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidActiveTimestampTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP, "0E0700000000010000");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noActiveTimestampTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidNetworkNameTlv_emptyName_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_NAME, "0300");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidNetworkNameTlv_tooLongName_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(
+ VALID_DATASET, TYPE_NETWORK_NAME, "03114142434445464748494A4B4C4D4E4F5051");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noNetworkNameTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_NAME);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidChannelTlv_channelMissing_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000100");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_undefinedChannelPage_success() {
+ byte[] datasetTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003010020");
+
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlv);
+
+ assertThat(dataset.getChannelPage()).isEqualTo(0x01);
+ assertThat(dataset.getChannel()).isEqualTo(0x20);
+ }
+
+ @Test
+ public void fromThreadTlvs_invalid2P4GhzChannel_throwsIllegalArgument() {
+ byte[] invalidTlv1 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300000A");
+ byte[] invalidTlv2 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300001B");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv1));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv2));
+ }
+
+ @Test
+ public void fromThreadTlvs_valid2P4GhzChannelTlv_success() {
+ byte[] validTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003000010");
+
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validTlv);
+
+ assertThat(dataset.getChannel()).isEqualTo(16);
+ }
+
+ @Test
+ public void fromThreadTlvs_noChannelTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_prematureEndOfChannelMaskEntry_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350100");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_inconsistentChannelMaskLength_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "3506000500010000");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_unsupportedChannelMaskLength_success() {
+ ActiveOperationalDataset dataset =
+ ActiveOperationalDataset.fromThreadTlvs(
+ replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350700050001000000"));
+
+ SparseArray<byte[]> channelMask = dataset.getChannelMask();
+ assertThat(channelMask.size()).isEqualTo(1);
+ assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
+ .isEqualTo(new byte[] {0x00, 0x01, 0x00, 0x00, 0x00});
+ }
+
+ @Test
+ public void fromThreadTlvs_noChannelMaskTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL_MASK);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_PAN_ID, "010101");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PAN_ID);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidExtendedPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID, "020700010203040506");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noExtendedPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidPskcTlv_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET, TYPE_PSKC, "0411000102030405060708090A0B0C0D0E0F10");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noPskcTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PSKC);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidMeshLocalPrefixTlv_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX, "0709FD0001020304050607");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noMeshLocalPrefixTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_tooShortSecurityPolicyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_SECURITY_POLICY, "0C0101");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noSecurityPolicyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_SECURITY_POLICY);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_lengthAndDataMissing_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {(byte) 0x00}));
+ }
+
+ @Test
+ public void fromThreadTlvs_prematureEndOfData_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {0x00, 0x03, 0x00, 0x00}));
+ }
+
+ @Test
+ public void fromThreadTlvs_validFullDataset_success() {
+ // A valid Thread active operational dataset:
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ byte[] validDatasetTlv =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validDatasetTlv);
+
+ assertThat(dataset.getNetworkKey())
+ .isEqualTo(base16().decode("F26B3153760F519A63BAFDDFFC80D2AF"));
+ assertThat(dataset.getPanId()).isEqualTo(0xd9a0);
+ assertThat(dataset.getExtendedPanId()).isEqualTo(base16().decode("ACC214689BC40BDF"));
+ assertThat(dataset.getChannel()).isEqualTo(19);
+ assertThat(dataset.getNetworkName()).isEqualTo("OpenThread-d9a0");
+ assertThat(dataset.getPskc())
+ .isEqualTo(base16().decode("A245479C836D551B9CA557F7B9D351B4"));
+ assertThat(dataset.getActiveTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+ SparseArray<byte[]> channelMask = dataset.getChannelMask();
+ assertThat(channelMask.size()).isEqualTo(1);
+ assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
+ .isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+ assertThat(dataset.getMeshLocalPrefix())
+ .isEqualTo(new IpPrefix("fd64:db12:25f4:7e0b::/64"));
+ assertThat(dataset.getSecurityPolicy())
+ .isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ }
+
+ @Test
+ public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
+ final byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102");
+
+ ActiveOperationalDataset dataset =
+ ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
+
+ byte[] newDatasetTlvs = dataset.toThreadTlvs();
+ String newDatasetTlvsHex = base16().encode(newDatasetTlvs);
+ assertThat(newDatasetTlvs.length).isEqualTo(datasetWithUnknownTlvs.length);
+ assertThat(newDatasetTlvsHex).contains("AA01FF");
+ assertThat(newDatasetTlvsHex).contains("BB020102");
+ }
+
+ @Test
+ public void toThreadTlvs_conversionIsLossLess() {
+ ActiveOperationalDataset dataset1 = ActiveOperationalDataset.createRandomDataset();
+
+ ActiveOperationalDataset dataset2 =
+ ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+ assertThat(dataset2).isEqualTo(dataset1);
+ }
+
+ @Test
+ public void builder_buildWithdefaultValues_throwsIllegalState() {
+ assertThrows(IllegalStateException.class, () -> new Builder().build());
+ }
+
+ @Test
+ public void builder_setValidNetworkKey_success() {
+ final byte[] networkKey =
+ new byte[] {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
+ 0x0d, 0x0e, 0x0f
+ };
+
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setNetworkKey(networkKey)
+ .build();
+
+ assertThat(dataset.getNetworkKey()).isEqualTo(networkKey);
+ }
+
+ @Test
+ public void builder_setInvalidNetworkKey_throwsIllegalArgument() {
+ byte[] invalidNetworkKey = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(
+ IllegalArgumentException.class, () -> builder.setNetworkKey(invalidNetworkKey));
+ }
+
+ @Test
+ public void builder_setValidExtendedPanId_success() {
+ byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setExtendedPanId(extendedPanId)
+ .build();
+
+ assertThat(dataset.getExtendedPanId()).isEqualTo(extendedPanId);
+ }
+
+ @Test
+ public void builder_setInvalidExtendedPanId_throwsIllegalArgument() {
+ byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setExtendedPanId(extendedPanId));
+ }
+
+ @Test
+ public void builder_setValidPanId_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setPanId(0xfffe)
+ .build();
+
+ assertThat(dataset.getPanId()).isEqualTo(0xfffe);
+ }
+
+ @Test
+ public void builder_setInvalidPanId_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setPanId(0xffff));
+ }
+
+ @Test
+ public void builder_setInvalidChannel_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 0));
+ assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 27));
+ }
+
+ @Test
+ public void builder_setValid2P4GhzChannel_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setChannel(CHANNEL_PAGE_24_GHZ, 16)
+ .build();
+
+ assertThat(dataset.getChannel()).isEqualTo(16);
+ assertThat(dataset.getChannelPage()).isEqualTo(CHANNEL_PAGE_24_GHZ);
+ }
+
+ @Test
+ public void builder_setValidNetworkName_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setNetworkName("ot-network")
+ .build();
+
+ assertThat(dataset.getNetworkName()).isEqualTo("ot-network");
+ }
+
+ @Test
+ public void builder_setEmptyNetworkName_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName(""));
+ }
+
+ @Test
+ public void builder_setTooLongNetworkName_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(
+ IllegalArgumentException.class, () -> builder.setNetworkName("openthread-network"));
+ }
+
+ @Test
+ public void builder_setTooLongUtf8NetworkName_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ // UTF-8 encoded length of "我的线程网络" is 18 bytes which exceeds the max length
+ assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName("我的线程网络"));
+ }
+
+ @Test
+ public void builder_setValidUtf8NetworkName_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setNetworkName("我的网络")
+ .build();
+
+ assertThat(dataset.getNetworkName()).isEqualTo("我的网络");
+ }
+
+ @Test
+ public void builder_setValidPskc_success() {
+ byte[] pskc = base16().decode("A245479C836D551B9CA557F7B9D351B4");
+
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset()).setPskc(pskc).build();
+
+ assertThat(dataset.getPskc()).isEqualTo(pskc);
+ }
+
+ @Test
+ public void builder_setTooLongPskc_throwsIllegalArgument() {
+ byte[] tooLongPskc = base16().decode("A245479C836D551B9CA557F7B9D351B400");
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setPskc(tooLongPskc));
+ }
+
+ @Test
+ public void builder_setValidChannelMask_success() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ SparseArray<byte[]> channelMask = new SparseArray<byte[]>(1);
+ channelMask.put(0, new byte[] {0x00, 0x00, 0x01, 0x00});
+
+ ActiveOperationalDataset dataset = builder.setChannelMask(channelMask).build();
+
+ SparseArray<byte[]> resultChannelMask = dataset.getChannelMask();
+ assertThat(resultChannelMask.size()).isEqualTo(1);
+ assertThat(resultChannelMask.get(0)).isEqualTo(new byte[] {0x00, 0x00, 0x01, 0x00});
+ }
+
+ @Test
+ public void builder_setEmptyChannelMask_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setChannelMask(new SparseArray<byte[]>()));
+ }
+
+ @Test
+ public void builder_setValidActiveTimestamp_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setActiveTimestamp(
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 1,
+ /* ticks= */ 0,
+ /* isAuthoritativeSource= */ true))
+ .build();
+
+ assertThat(dataset.getActiveTimestamp().getSeconds()).isEqualTo(1);
+ assertThat(dataset.getActiveTimestamp().getTicks()).isEqualTo(0);
+ assertThat(dataset.getActiveTimestamp().isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void builder_wrongMeshLocalPrefixLength_throwsIllegalArguments() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ // The Mesh-Local Prefix length must be 64 bits
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/32")));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/96")));
+
+ // The Mesh-Local Prefix must start with 0xfd
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
+ }
+
+ @Test
+ public void builder_meshLocalPrefixNotStartWith0xfd_throwsIllegalArguments() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
+ }
+
+ @Test
+ public void builder_setValidMeshLocalPrefix_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setMeshLocalPrefix(new IpPrefix("fd00::/64"))
+ .build();
+
+ assertThat(dataset.getMeshLocalPrefix()).isEqualTo(new IpPrefix("fd00::/64"));
+ }
+
+ @Test
+ public void builder_setValid1P2SecurityPolicy_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setSecurityPolicy(
+ new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .build();
+
+ assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+ assertThat(dataset.getSecurityPolicy().getFlags())
+ .isEqualTo(new byte[] {(byte) 0xff, (byte) 0xf8});
+ }
+
+ @Test
+ public void builder_setValid1P1SecurityPolicy_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff}))
+ .build();
+
+ assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+ assertThat(dataset.getSecurityPolicy().getFlags()).isEqualTo(new byte[] {(byte) 0xff});
+ }
+
+ @Test
+ public void securityPolicy_invalidRotationTime_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new SecurityPolicy(0, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new SecurityPolicy(0x1ffff, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ }
+
+ @Test
+ public void securityPolicy_emptyFlags_throwsIllegalArguments() {
+ assertThrows(IllegalArgumentException.class, () -> new SecurityPolicy(672, new byte[] {}));
+ }
+
+ @Test
+ public void securityPolicy_tooLongFlags_success() {
+ SecurityPolicy securityPolicy =
+ new SecurityPolicy(672, new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
+
+ assertThat(securityPolicy.getFlags()).isEqualTo(new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
+ }
+
+ @Test
+ public void securityPolicy_equals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}),
+ new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .addEqualityGroup(
+ new SecurityPolicy(1, new byte[] {(byte) 0xff}),
+ new SecurityPolicy(1, new byte[] {(byte) 0xff}))
+ .addEqualityGroup(
+ new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}),
+ new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .testEquals();
+ }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
new file mode 100644
index 0000000..9be3d56
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.thread.OperationalDatasetTimestamp;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+
+/** Tests for {@link OperationalDatasetTimestamp}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class OperationalDatasetTimestampTest {
+ @Test
+ public void fromInstant_tooLargeInstant_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ OperationalDatasetTimestamp.fromInstant(
+ Instant.ofEpochSecond(0xffffffffffffL + 1L)));
+ }
+
+ @Test
+ public void fromInstant_ticksIsRounded() {
+ Instant instant = Instant.ofEpochSecond(100L);
+
+ // 32767.5 / 32768 * 1000000000 = 999984741.2109375 and given the `ticks` is rounded, so
+ // the `ticks` should be 32767 for 999984741 and 0 (carried over to seconds) for 999984742.
+ OperationalDatasetTimestamp timestampTicks32767 =
+ OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984741));
+ OperationalDatasetTimestamp timestampTicks0 =
+ OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984742));
+
+ assertThat(timestampTicks32767.getSeconds()).isEqualTo(100L);
+ assertThat(timestampTicks0.getSeconds()).isEqualTo(101L);
+ assertThat(timestampTicks32767.getTicks()).isEqualTo(32767);
+ assertThat(timestampTicks0.getTicks()).isEqualTo(0);
+ assertThat(timestampTicks32767.isAuthoritativeSource()).isTrue();
+ assertThat(timestampTicks0.isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void toInstant_nanosIsRounded() {
+ // 32767 / 32768 * 1000000000 = 999969482.421875
+ assertThat(new OperationalDatasetTimestamp(100L, 32767, false).toInstant().getNano())
+ .isEqualTo(999969482);
+
+ // 32766 / 32768 * 1000000000 = 999938964.84375
+ assertThat(new OperationalDatasetTimestamp(100L, 32766, false).toInstant().getNano())
+ .isEqualTo(999938965);
+ }
+
+ @Test
+ public void toInstant_onlyAuthoritativeSourceDiscarded() {
+ OperationalDatasetTimestamp timestamp1 =
+ new OperationalDatasetTimestamp(100L, 0x7fff, false);
+
+ OperationalDatasetTimestamp timestamp2 =
+ OperationalDatasetTimestamp.fromInstant(timestamp1.toInstant());
+
+ assertThat(timestamp2.getSeconds()).isEqualTo(100L);
+ assertThat(timestamp2.getTicks()).isEqualTo(0x7fff);
+ assertThat(timestamp2.isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void constructor_tooLargeSeconds_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 0x0001112233445566L,
+ /* ticks= */ 0,
+ /* isAuthoritativeSource= */ true));
+ }
+
+ @Test
+ public void constructor_tooLargeTicks_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 0x01L,
+ /* ticks= */ 0x8000,
+ /* isAuthoritativeSource= */ true));
+ }
+
+ @Test
+ public void equalityTests() {
+ new EqualsTester()
+ .addEqualityGroup(
+ new OperationalDatasetTimestamp(100, 100, false),
+ new OperationalDatasetTimestamp(100, 100, false))
+ .addEqualityGroup(
+ new OperationalDatasetTimestamp(0, 0, false),
+ new OperationalDatasetTimestamp(0, 0, false))
+ .addEqualityGroup(
+ new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true),
+ new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true))
+ .testEquals();
+ }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
new file mode 100644
index 0000000..7a49957
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.cts;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+
+/** Tests for {@link PendingOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class PendingOperationalDatasetTest {
+ private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
+ ActiveOperationalDataset.createRandomDataset();
+
+ @Test
+ public void parcelable_parcelingIsLossLess() {
+ PendingOperationalDataset dataset =
+ new PendingOperationalDataset(
+ DEFAULT_ACTIVE_DATASET,
+ new OperationalDatasetTimestamp(31536000, 200, false),
+ Duration.ofHours(100));
+
+ assertParcelingIsLossless(dataset);
+ }
+
+ @Test
+ public void equalityTests() {
+ ActiveOperationalDataset activeDataset1 = ActiveOperationalDataset.createRandomDataset();
+ ActiveOperationalDataset activeDataset2 = ActiveOperationalDataset.createRandomDataset();
+
+ new EqualsTester()
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)),
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)))
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)),
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)))
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(0)),
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(0)))
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(100)),
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(100)))
+ .testEquals();
+ }
+
+ @Test
+ public void constructor_correctValuesAreSet() {
+ PendingOperationalDataset dataset =
+ new PendingOperationalDataset(
+ DEFAULT_ACTIVE_DATASET,
+ new OperationalDatasetTimestamp(31536000, 200, false),
+ Duration.ofHours(100));
+
+ assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET);
+ assertThat(dataset.getPendingTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(31536000, 200, false));
+ assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofHours(100));
+ }
+
+ @Test
+ public void fromThreadTlvs_openthreadTlvs_success() {
+ // An example Pending Operational Dataset which is generated with OpenThread CLI:
+ // Pending Timestamp: 2
+ // Active Timestamp: 1
+ // Channel: 26
+ // Channel Mask: 0x07fff800
+ // Delay: 46354
+ // Ext PAN ID: a74182f4d3f4de41
+ // Mesh Local Prefix: fd46:c1b9:e159:5574::/64
+ // Network Key: ed916e454d96fd00184f10a6f5c9e1d3
+ // Network Name: OpenThread-bff8
+ // PAN ID: 0xbff8
+ // PSKc: 264f78414adc683191863d968f72d1b7
+ // Security Policy: 672 onrc
+ final byte[] OPENTHREAD_PENDING_DATASET_TLVS =
+ base16().lowerCase()
+ .decode(
+ "0e0800000000000100003308000000000002000034040000b51200030000"
+ + "1a35060004001fffe00208a74182f4d3f4de410708fd46c1b9"
+ + "e15955740510ed916e454d96fd00184f10a6f5c9e1d3030f4f"
+ + "70656e5468726561642d626666380102bff80410264f78414a"
+ + "dc683191863d968f72d1b70c0402a0f7f8");
+
+ PendingOperationalDataset pendingDataset =
+ PendingOperationalDataset.fromThreadTlvs(OPENTHREAD_PENDING_DATASET_TLVS);
+
+ ActiveOperationalDataset activeDataset = pendingDataset.getActiveOperationalDataset();
+ assertThat(pendingDataset.getPendingTimestamp().getSeconds()).isEqualTo(2L);
+ assertThat(activeDataset.getActiveTimestamp().getSeconds()).isEqualTo(1L);
+ assertThat(activeDataset.getChannel()).isEqualTo(26);
+ assertThat(activeDataset.getChannelMask().get(0))
+ .isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+ assertThat(pendingDataset.getDelayTimer().toMillis()).isEqualTo(46354);
+ assertThat(activeDataset.getExtendedPanId())
+ .isEqualTo(base16().lowerCase().decode("a74182f4d3f4de41"));
+ assertThat(activeDataset.getMeshLocalPrefix())
+ .isEqualTo(new IpPrefix("fd46:c1b9:e159:5574::/64"));
+ assertThat(activeDataset.getNetworkKey())
+ .isEqualTo(base16().lowerCase().decode("ed916e454d96fd00184f10a6f5c9e1d3"));
+ assertThat(activeDataset.getNetworkName()).isEqualTo("OpenThread-bff8");
+ assertThat(activeDataset.getPanId()).isEqualTo(0xbff8);
+ assertThat(activeDataset.getPskc())
+ .isEqualTo(base16().lowerCase().decode("264f78414adc683191863d968f72d1b7"));
+ assertThat(activeDataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+ assertThat(activeDataset.getSecurityPolicy().getFlags())
+ .isEqualTo(new byte[] {(byte) 0xf7, (byte) 0xf8});
+ }
+
+ @Test
+ public void fromThreadTlvs_completePendingDatasetTlvs_success() {
+ // Type Length Value
+ // 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
+ // 0x34 0x04 0x0000012C (Delay Timer TLV)
+ final byte[] pendingTimestampAndDelayTimerTlvs =
+ base16().decode("3308000000000001000034040000012C");
+ final byte[] pendingDatasetTlvs =
+ Bytes.concat(
+ pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
+
+ PendingOperationalDataset dataset =
+ PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs);
+
+ assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET);
+ assertThat(dataset.getPendingTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+ assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofMillis(300));
+ }
+
+ @Test
+ public void fromThreadTlvs_PendingTimestampTlvIsMissing_throwsIllegalArgument() {
+ // Type Length Value
+ // 0x34 0x04 0x00000064 (Delay Timer TLV)
+ final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("34040000012C");
+ final byte[] pendingDatasetTlvs =
+ Bytes.concat(
+ pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
+ }
+
+ @Test
+ public void fromThreadTlvs_delayTimerTlvIsMissing_throwsIllegalArgument() {
+ // Type Length Value
+ // 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
+ final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("33080000000000010000");
+ final byte[] pendingDatasetTlvs =
+ Bytes.concat(
+ pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
+ }
+
+ @Test
+ public void fromThreadTlvs_activeDatasetTlvs_throwsIllegalArgument() {
+ final byte[] activeDatasetTlvs = DEFAULT_ACTIVE_DATASET.toThreadTlvs();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(activeDatasetTlvs));
+ }
+
+ @Test
+ public void fromThreadTlvs_malformedTlvs_throwsIllegalArgument() {
+ final byte[] invalidTlvs = new byte[] {0x00};
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(invalidTlvs));
+ }
+
+ @Test
+ public void toThreadTlvs_conversionIsLossLess() {
+ PendingOperationalDataset dataset1 =
+ new PendingOperationalDataset(
+ DEFAULT_ACTIVE_DATASET,
+ new OperationalDatasetTimestamp(31536000, 200, false),
+ Duration.ofHours(100));
+
+ PendingOperationalDataset dataset2 =
+ PendingOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+ assertThat(dataset2).isEqualTo(dataset1);
+ }
+}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
new file mode 100644
index 0000000..3a087c7
--- /dev/null
+++ b/thread/tests/unit/Android.bp
@@ -0,0 +1,50 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "ThreadNetworkUnitTests",
+ min_sdk_version: "33",
+ sdk_version: "module_current",
+ manifest: "AndroidManifest.xml",
+ test_config: "AndroidTest.xml",
+ srcs: [
+ "src/**/*.java",
+ ],
+ test_suites: [
+ "general-tests",
+ ],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "compatibility-device-util-axt",
+ "ctstestrunner-axt",
+ "framework-connectivity-pre-jarjar",
+ "framework-connectivity-t-pre-jarjar",
+ "guava-android-testlib",
+ "net-tests-utils",
+ "truth",
+ ],
+ libs: [
+ "android.test.base",
+ "android.test.runner",
+ ],
+ // Test coverage system runs on different devices. Need to
+ // compile for all architectures.
+ compile_multilib: "both",
+}
diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..ace7c52
--- /dev/null
+++ b/thread/tests/unit/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.net.thread.unittests">
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.net.thread.unittests"
+ android:label="Unit tests for android.net.thread" />
+</manifest>
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..663ff74
--- /dev/null
+++ b/thread/tests/unit/AndroidTest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<configuration description="Config for Thread network unit test cases">
+ <option name="test-tag" value="ThreadNetworkUnitTests" />
+ <option name="test-suite-tag" value="apct" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
+ <option name="check-min-sdk" value="true" />
+ <option name="cleanup-apks" value="true" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.net.thread.unittests" />
+ <!-- Ignores tests introduced by guava-android-testlib -->
+ <option name="exclude-annotation" value="org.junit.Ignore"/>
+ </test>
+</configuration>
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
new file mode 100644
index 0000000..78eb3d0
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset.Builder;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/** Unit tests for {@link ActiveOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActiveOperationalDatasetTest {
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ private static final byte[] VALID_DATASET =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+
+ @Mock private Random mockRandom;
+ @Mock private SecureRandom mockSecureRandom;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private static byte[] addTlv(byte[] dataset, String tlvHex) {
+ return Bytes.concat(dataset, base16().decode(tlvHex));
+ }
+
+ @Test
+ public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
+ byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102");
+
+ ActiveOperationalDataset dataset1 =
+ ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
+ ActiveOperationalDataset dataset2 =
+ ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+ SparseArray<byte[]> unknownTlvs = dataset2.getUnknownTlvs();
+ assertThat(unknownTlvs.size()).isEqualTo(2);
+ assertThat(unknownTlvs.get(0xAA)).isEqualTo(new byte[] {(byte) 0xFF});
+ assertThat(unknownTlvs.get(0xBB)).isEqualTo(new byte[] {0x01, 0x02});
+ assertThat(dataset2).isEqualTo(dataset1);
+ }
+
+ @Test
+ public void createRandomDataset_fieldsAreRandomized() {
+ // Always return the max bounded value
+ doAnswer(invocation -> (int) invocation.getArgument(0) - 1)
+ .when(mockRandom)
+ .nextInt(anyInt());
+ doAnswer(
+ invocation -> {
+ byte[] output = invocation.getArgument(0);
+ for (int i = 0; i < output.length; ++i) {
+ output[i] = (byte) (i + 10);
+ }
+ return null;
+ })
+ .when(mockRandom)
+ .nextBytes(any(byte[].class));
+ doAnswer(
+ invocation -> {
+ byte[] output = invocation.getArgument(0);
+ for (int i = 0; i < output.length; ++i) {
+ output[i] = (byte) (i + 30);
+ }
+ return null;
+ })
+ .when(mockSecureRandom)
+ .nextBytes(any(byte[].class));
+
+ ActiveOperationalDataset dataset =
+ ActiveOperationalDataset.createRandomDataset(mockRandom, mockSecureRandom);
+
+ assertThat(dataset.getActiveTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+ assertThat(dataset.getExtendedPanId())
+ .isEqualTo(new byte[] {10, 11, 12, 13, 14, 15, 16, 17});
+ assertThat(dataset.getMeshLocalPrefix())
+ .isEqualTo(new IpPrefix("fd0b:0c0d:0e0f:1011::/64"));
+ verify(mockRandom, times(2)).nextBytes(any(byte[].class));
+ assertThat(dataset.getPanId()).isEqualTo(0xfffe); // PAN ID <= 0xfffe
+ verify(mockRandom, times(1)).nextInt(eq(0xffff));
+ assertThat(dataset.getChannel()).isEqualTo(26);
+ verify(mockRandom, times(1)).nextInt(eq(16));
+ assertThat(dataset.getChannelPage()).isEqualTo(0);
+ assertThat(dataset.getChannelMask().size()).isEqualTo(1);
+ assertThat(dataset.getPskc())
+ .isEqualTo(
+ new byte[] {
+ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
+ });
+ assertThat(dataset.getNetworkKey())
+ .isEqualTo(
+ new byte[] {
+ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
+ });
+ verify(mockSecureRandom, times(2)).nextBytes(any(byte[].class));
+ assertThat(dataset.getSecurityPolicy())
+ .isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ }
+
+ @Test
+ public void builder_buildWithTooLongTlvs_throwsIllegalState() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ for (int i = 0; i < 10; i++) {
+ builder.addUnknownTlv(i, new byte[20]);
+ }
+
+ assertThrows(IllegalStateException.class, () -> new Builder().build());
+ }
+
+ @Test
+ public void builder_setUnknownTlvs_success() {
+ ActiveOperationalDataset dataset1 = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET);
+ SparseArray<byte[]> unknownTlvs = new SparseArray<>(2);
+ unknownTlvs.put(0x33, new byte[] {1, 2, 3});
+ unknownTlvs.put(0x44, new byte[] {1, 2, 3, 4});
+
+ ActiveOperationalDataset dataset2 =
+ new ActiveOperationalDataset.Builder(dataset1).setUnknownTlvs(unknownTlvs).build();
+
+ assertThat(dataset1.getUnknownTlvs().size()).isEqualTo(0);
+ assertThat(dataset2.getUnknownTlvs().size()).isEqualTo(2);
+ assertThat(dataset2.getUnknownTlvs().get(0x33)).isEqualTo(new byte[] {1, 2, 3});
+ assertThat(dataset2.getUnknownTlvs().get(0x44)).isEqualTo(new byte[] {1, 2, 3, 4});
+ }
+
+ @Test
+ public void securityPolicy_fromTooShortTlvValue_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> SecurityPolicy.fromTlvValue(new byte[] {0x01}));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> SecurityPolicy.fromTlvValue(new byte[] {0x01, 0x02}));
+ }
+
+ @Test
+ public void securityPolicy_toTlvValue_conversionIsLossLess() {
+ SecurityPolicy policy1 = new SecurityPolicy(200, new byte[] {(byte) 0xFF, (byte) 0xF8});
+
+ SecurityPolicy policy2 = SecurityPolicy.fromTlvValue(policy1.toTlvValue());
+
+ assertThat(policy2).isEqualTo(policy1);
+ }
+}
diff --git a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
new file mode 100644
index 0000000..32063fc
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link OperationalDatasetTimestamp}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class OperationalDatasetTimestampTest {
+ @Test
+ public void fromTlvValue_invalidTimestamp_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> OperationalDatasetTimestamp.fromTlvValue(new byte[7]));
+ }
+
+ @Test
+ public void fromTlvValue_goodValue_success() {
+ OperationalDatasetTimestamp timestamp =
+ OperationalDatasetTimestamp.fromTlvValue(base16().decode("FFEEDDCCBBAA9989"));
+
+ assertThat(timestamp.getSeconds()).isEqualTo(0xFFEEDDCCBBAAL);
+ // 0x9989 is 0x4CC4 << 1 + 1
+ assertThat(timestamp.getTicks()).isEqualTo(0x4CC4);
+ assertThat(timestamp.isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void toTlvValue_conversionIsLossLess() {
+ OperationalDatasetTimestamp timestamp1 = new OperationalDatasetTimestamp(100L, 10, true);
+
+ OperationalDatasetTimestamp timestamp2 =
+ OperationalDatasetTimestamp.fromTlvValue(timestamp1.toTlvValue());
+
+ assertThat(timestamp2).isEqualTo(timestamp1);
+ }
+}