Merge "Support get multiple ipv4/ipv6 addresses in `apf_utils`" into main
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 70b38a4..15ad226 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -79,6 +79,7 @@
],
defaults: ["TetheringExternalLibs"],
libs: [
+ "framework-annotations-lib",
"framework-tethering.impl",
],
manifest: "AndroidManifestBase.xml",
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index 21e55b4..1d5df61 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -28,8 +28,6 @@
import static java.util.Arrays.asList;
-import android.content.Context;
-import android.net.ConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
@@ -53,6 +51,7 @@
import java.util.List;
import java.util.Random;
import java.util.Set;
+import java.util.function.Supplier;
/**
* This class coordinate IP addresses conflict problem.
@@ -65,6 +64,7 @@
* @hide
*/
public class PrivateAddressCoordinator {
+ // WARNING: Keep in sync with chooseDownstreamAddress
public static final int PREFIX_LENGTH = 24;
// Upstream monitor would be stopped when tethering is down. When tethering restart, downstream
@@ -77,19 +77,20 @@
private static final String LEGACY_WIFI_P2P_IFACE_ADDRESS = "192.168.49.1/24";
private static final String LEGACY_BLUETOOTH_IFACE_ADDRESS = "192.168.44.1/24";
private final List<IpPrefix> mTetheringPrefixes;
- private final ConnectivityManager mConnectivityMgr;
+ // A supplier that returns ConnectivityManager#getAllNetworks.
+ private final Supplier<Network[]> mGetAllNetworksSupplier;
private final boolean mIsRandomPrefixBaseEnabled;
private final boolean mShouldEnableWifiP2pDedicatedIp;
// keyed by downstream type(TetheringManager.TETHERING_*).
private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
private final Random mRandom;
- public PrivateAddressCoordinator(Context context, boolean isRandomPrefixBase,
- boolean shouldEnableWifiP2pDedicatedIp) {
+ public PrivateAddressCoordinator(Supplier<Network[]> getAllNetworksSupplier,
+ boolean isRandomPrefixBase,
+ boolean shouldEnableWifiP2pDedicatedIp) {
mDownstreams = new ArraySet<>();
mUpstreamPrefixMap = new ArrayMap<>();
- mConnectivityMgr = (ConnectivityManager) context.getSystemService(
- Context.CONNECTIVITY_SERVICE);
+ mGetAllNetworksSupplier = getAllNetworksSupplier;
mIsRandomPrefixBaseEnabled = isRandomPrefixBase;
mShouldEnableWifiP2pDedicatedIp = shouldEnableWifiP2pDedicatedIp;
mCachedAddresses = new ArrayMap<AddressKey, LinkAddress>();
@@ -166,7 +167,7 @@
// Remove all upstreams that are no longer valid networks
final Set<Network> toBeRemoved = new HashSet<>(mUpstreamPrefixMap.keySet());
- toBeRemoved.removeAll(asList(mConnectivityMgr.getAllNetworks()));
+ toBeRemoved.removeAll(asList(mGetAllNetworksSupplier.get()));
mUpstreamPrefixMap.removeAll(toBeRemoved);
}
@@ -194,7 +195,7 @@
return cachedAddress;
}
- final int prefixIndex = getStartedPrefixIndex();
+ final int prefixIndex = getRandomPrefixIndex();
for (int i = 0; i < mTetheringPrefixes.size(); i++) {
final IpPrefix prefixRange = mTetheringPrefixes.get(
(prefixIndex + i) % mTetheringPrefixes.size());
@@ -210,7 +211,7 @@
return null;
}
- private int getStartedPrefixIndex() {
+ private int getRandomPrefixIndex() {
if (!mIsRandomPrefixBaseEnabled) return 0;
final int random = getRandomInt() & 0xffffff;
@@ -247,123 +248,62 @@
return getInUseDownstreamPrefix(prefix);
}
- // Get the next non-conflict sub prefix. E.g: To get next sub prefix from 10.0.0.0/8, if the
- // previously selected prefix is 10.20.42.0/24(subPrefix: 0.20.42.0) and the conflicting prefix
- // is 10.16.0.0/20 (10.16.0.0 ~ 10.16.15.255), then the max address under subPrefix is
- // 0.16.15.255 and the next subPrefix is 0.16.16.255/24 (0.16.15.255 + 0.0.1.0).
- // Note: the sub address 0.0.0.255 here is fine to be any value that it will be replaced as
- // selected random sub address later.
- private int getNextSubPrefix(final IpPrefix conflictPrefix, final int prefixRangeMask) {
- final int suffixMask = ~prefixLengthToV4NetmaskIntHTH(conflictPrefix.getPrefixLength());
- // The largest offset within the prefix assignment block that still conflicts with
- // conflictPrefix.
- final int maxConflict =
- (getPrefixBaseAddress(conflictPrefix) | suffixMask) & ~prefixRangeMask;
-
- final int prefixMask = prefixLengthToV4NetmaskIntHTH(PREFIX_LENGTH);
- // Pick a sub prefix a full prefix (1 << (32 - PREFIX_LENGTH) addresses) greater than
- // maxConflict. This ensures that the selected prefix never overlaps with conflictPrefix.
- // There is no need to mask the result with PREFIX_LENGTH bits because this is done by
- // findAvailablePrefixFromRange when it constructs the prefix.
- return maxConflict + (1 << (32 - PREFIX_LENGTH));
- }
-
- private LinkAddress chooseDownstreamAddress(final IpPrefix prefixRange) {
+ @VisibleForTesting
+ public LinkAddress chooseDownstreamAddress(final IpPrefix prefixRange) {
// The netmask of the prefix assignment block (e.g., 0xfff00000 for 172.16.0.0/12).
final int prefixRangeMask = prefixLengthToV4NetmaskIntHTH(prefixRange.getPrefixLength());
// The zero address in the block (e.g., 0xac100000 for 172.16.0.0/12).
final int baseAddress = getPrefixBaseAddress(prefixRange);
- // The subnet mask corresponding to PREFIX_LENGTH.
- final int prefixMask = prefixLengthToV4NetmaskIntHTH(PREFIX_LENGTH);
+ // Try to get an address within the given prefix that does not conflict with any other
+ // prefix in the system.
+ for (int i = 0; i < 20; ++i) {
+ final int randomSuffix = mRandom.nextInt() & ~prefixRangeMask;
+ final int randomAddress = baseAddress | randomSuffix;
- // The offset within prefixRange of a randomly-selected prefix of length PREFIX_LENGTH.
- // This may not be the prefix of the address returned by this method:
- // - If it is already in use, the method will return an address in another prefix.
- // - If all prefixes within prefixRange are in use, the method will return null. For
- // example, for a /24 prefix within 172.26.0.0/12, this will be a multiple of 256 in
- // [0, 1048576). In other words, a random 32-bit number with mask 0x000fff00.
- //
- // prefixRangeMask is required to ensure no wrapping. For example, consider:
- // - prefixRange 127.0.0.0/8
- // - randomPrefixStart 127.255.255.0
- // - A conflicting prefix of 127.255.254.0/23
- // In this case without prefixRangeMask, getNextSubPrefix would return 128.0.0.0, which
- // means the "start < end" check in findAvailablePrefixFromRange would not reject the prefix
- // because Java doesn't have unsigned integers, so 128.0.0.0 = 0x80000000 = -2147483648
- // is less than 127.0.0.0 = 0x7f000000 = 2130706432.
- //
- // Additionally, it makes debug output easier to read by making the numbers smaller.
- final int randomInt = getRandomInt();
- final int randomPrefixStart = randomInt & ~prefixRangeMask & prefixMask;
+ // Avoid selecting x.x.x.[0, 1, 255] addresses.
+ switch (randomAddress & 0xFF) {
+ case 0:
+ case 1:
+ case 255:
+ // Try selecting a different address
+ continue;
+ }
- // A random offset within the prefix. Used to determine the local address once the prefix
- // is selected. It does not result in an IPv4 address ending in .0, .1, or .255
- // For a PREFIX_LENGTH of 24, this is a number between 2 and 254.
- final int subAddress = getSanitizedSubAddr(randomInt, ~prefixMask);
+ // Avoid selecting commonly used subnets.
+ switch (randomAddress & 0xFFFFFF00) {
+ case 0xC0A80000: // 192.168.0.0/24
+ case 0xC0A80100: // 192.168.1.0/24
+ case 0xC0A85800: // 192.168.88.0/24
+ case 0xC0A86400: // 192.168.100.0/24
+ continue;
+ }
- // Find a prefix length PREFIX_LENGTH between randomPrefixStart and the end of the block,
- // such that the prefix does not conflict with any upstream.
- IpPrefix downstreamPrefix = findAvailablePrefixFromRange(
- randomPrefixStart, (~prefixRangeMask) + 1, baseAddress, prefixRangeMask);
- if (downstreamPrefix != null) return getLinkAddress(downstreamPrefix, subAddress);
+ // Avoid 10.0.0.0 - 10.10.255.255
+ if (randomAddress >= 0x0A000000 && randomAddress <= 0x0A0AFFFF) {
+ continue;
+ }
- // If that failed, do the same, but between 0 and randomPrefixStart.
- downstreamPrefix = findAvailablePrefixFromRange(
- 0, randomPrefixStart, baseAddress, prefixRangeMask);
-
- return getLinkAddress(downstreamPrefix, subAddress);
- }
-
- private LinkAddress getLinkAddress(final IpPrefix prefix, final int subAddress) {
- if (prefix == null) return null;
-
- final InetAddress address = intToInet4AddressHTH(getPrefixBaseAddress(prefix) | subAddress);
- return new LinkAddress(address, PREFIX_LENGTH);
- }
-
- private IpPrefix findAvailablePrefixFromRange(final int start, final int end,
- final int baseAddress, final int prefixRangeMask) {
- int newSubPrefix = start;
- while (newSubPrefix < end) {
- final InetAddress address = intToInet4AddressHTH(baseAddress | newSubPrefix);
+ final InetAddress address = intToInet4AddressHTH(randomAddress);
final IpPrefix prefix = new IpPrefix(address, PREFIX_LENGTH);
-
- final IpPrefix conflictPrefix = getConflictPrefix(prefix);
-
- if (conflictPrefix == null) return prefix;
-
- newSubPrefix = getNextSubPrefix(conflictPrefix, prefixRangeMask);
+ if (getConflictPrefix(prefix) != null) {
+ // Prefix is conflicting with another prefix used in the system, find another one.
+ continue;
+ }
+ return new LinkAddress(address, PREFIX_LENGTH);
}
-
+ // Could not find a prefix, return null and let caller try another range.
return null;
}
/** Get random int which could be used to generate random address. */
+ // TODO: get rid of this function and mock getRandomPrefixIndex in tests.
@VisibleForTesting
public int getRandomInt() {
return mRandom.nextInt();
}
- /** Get random subAddress and avoid selecting x.x.x.0, x.x.x.1 and x.x.x.255 address. */
- private int getSanitizedSubAddr(final int randomInt, final int subAddrMask) {
- final int randomSubAddr = randomInt & subAddrMask;
- // If prefix length > 30, the selecting speace would be less than 4 which may be hard to
- // avoid 3 consecutive address.
- if (PREFIX_LENGTH > 30) return randomSubAddr;
-
- // TODO: maybe it is not necessary to avoid .0, .1 and .255 address because tethering
- // address would not be conflicted. This code only works because PREFIX_LENGTH is not longer
- // than 24
- final int candidate = randomSubAddr & 0xff;
- if (candidate == 0 || candidate == 1 || candidate == 255) {
- return (randomSubAddr & 0xfffffffc) + 2;
- }
-
- return randomSubAddr;
- }
-
/** Release downstream record for IpServer. */
public void releaseDownstream(final IpServer ipServer) {
mDownstreams.remove(ipServer);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 5bb1694..81f057c 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -21,6 +21,7 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothPan;
import android.content.Context;
+import android.net.ConnectivityManager;
import android.net.INetd;
import android.net.connectivity.ConnectivityInternalApiUtil;
import android.net.ip.IpServer;
@@ -176,9 +177,12 @@
/**
* Make PrivateAddressCoordinator to be used by Tethering.
*/
- public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx,
- TetheringConfiguration cfg) {
- return new PrivateAddressCoordinator(ctx, cfg.isRandomPrefixBaseEnabled(),
+ public PrivateAddressCoordinator makePrivateAddressCoordinator(
+ Context ctx, TetheringConfiguration cfg) {
+ final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
+ return new PrivateAddressCoordinator(
+ cm::getAllNetworks,
+ cfg.isRandomPrefixBaseEnabled(),
cfg.shouldEnableWifiP2pDedicatedIp());
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 148787f..a5c06f3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -16,7 +16,6 @@
package com.android.networkstack.tethering;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
@@ -29,9 +28,11 @@
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -103,6 +104,7 @@
MockitoAnnotations.initMocks(this);
when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mConnectivityMgr);
+ when(mContext.getSystemService(ConnectivityManager.class)).thenReturn(mConnectivityMgr);
when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks);
when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(false);
when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(false);
@@ -110,7 +112,7 @@
mPrivateAddressCoordinator =
spy(
new PrivateAddressCoordinator(
- mContext,
+ mConnectivityMgr::getAllNetworks,
mConfig.isRandomPrefixBaseEnabled(),
mConfig.shouldEnableWifiP2pDedicatedIp()));
}
@@ -153,37 +155,6 @@
}
@Test
- public void testSanitizedAddress() throws Exception {
- int fakeSubAddr = 0x2b00; // 43.0.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- LinkAddress actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.43.2/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
- fakeSubAddr = 0x2d01; // 45.1.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.45.2/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
- fakeSubAddr = 0x2eff; // 46.255.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.46.254/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
- fakeSubAddr = 0x2f05; // 47.5.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.47.5/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
- }
-
- @Test
public void testReservedPrefix() throws Exception {
// - Test bluetooth prefix is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
@@ -218,22 +189,15 @@
@Test
public void testRequestLastDownstreamAddress() throws Exception {
- final int fakeHotspotSubAddr = 0x2b05; // 43.5
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.45.5/24"), usbAddress);
mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
- final int newFakeSubAddr = 0x3c05;
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
-
final LinkAddress newHotspotAddress = requestDownstreamAddress(mHotspotIpServer,
CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals(hotspotAddress, newHotspotAddress);
@@ -271,262 +235,27 @@
}
@Test
- public void testNoConflictUpstreamPrefix() throws Exception {
- final int fakeHotspotSubAddr = 0x2b05; // 43.5
- final IpPrefix predefinedPrefix = new IpPrefix("192.168.43.0/24");
- // Force always get subAddress "43.5" for conflict testing.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
- // - Enable hotspot with prefix 192.168.43.0/24
- final LinkAddress hotspotAddr = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddr);
- assertEquals("Wrong wifi prefix: ", predefinedPrefix, hotspotPrefix);
- // - test mobile network with null NetworkCapabilities. Ideally this should not happen
- // because NetworkCapabilities update should always happen before LinkProperties update
- // and the UpstreamNetworkState update, just make sure no crash in this case.
- final UpstreamNetworkState noCapUpstream = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("10.0.0.8/24"), null, null);
- updateUpstreamPrefix(noCapUpstream);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - test mobile upstream with no address.
- final UpstreamNetworkState noAddress = buildUpstreamNetworkState(mMobileNetwork,
- null, null, makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(noCapUpstream);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v6 only mobile network, hotspot prefix should not be removed.
- final UpstreamNetworkState v6OnlyMobile = buildUpstreamNetworkState(mMobileNetwork,
- null, new LinkAddress("2001:db8::/64"),
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(v6OnlyMobile);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- mPrivateAddressCoordinator.removeUpstreamPrefix(mMobileNetwork);
- // - Update v4 only mobile network, hotspot prefix should not be removed.
- final UpstreamNetworkState v4OnlyMobile = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("10.0.0.8/24"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(v4OnlyMobile);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v4v6 mobile network, hotspot prefix should not be removed.
- final UpstreamNetworkState v4v6Mobile = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("10.0.0.8/24"), new LinkAddress("2001:db8::/64"),
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(v4v6Mobile);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v6 only wifi network, hotspot prefix should not be removed.
- final UpstreamNetworkState v6OnlyWifi = buildUpstreamNetworkState(mWifiNetwork,
- null, new LinkAddress("2001:db8::/64"), makeNetworkCapabilities(TRANSPORT_WIFI));
- updateUpstreamPrefix(v6OnlyWifi);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork);
- // - Update vpn network, it conflict with hotspot prefix but VPN networks are ignored.
- final UpstreamNetworkState v4OnlyVpn = buildUpstreamNetworkState(mVpnNetwork,
- new LinkAddress("192.168.43.5/24"), null, makeNetworkCapabilities(TRANSPORT_VPN));
- updateUpstreamPrefix(v4OnlyVpn);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v4 only wifi network, it conflict with hotspot prefix.
- final UpstreamNetworkState v4OnlyWifi = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.43.5/24"), null, makeNetworkCapabilities(TRANSPORT_WIFI));
- updateUpstreamPrefix(v4OnlyWifi);
- verify(mHotspotIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- reset(mHotspotIpServer);
- // - Restart hotspot again and its prefix is different previous.
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
- final LinkAddress hotspotAddr2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix hotspotPrefix2 = asIpPrefix(hotspotAddr2);
- assertNotEquals(hotspotPrefix, hotspotPrefix2);
- updateUpstreamPrefix(v4OnlyWifi);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Usb tethering can be enabled and its prefix is different with conflict one.
- final LinkAddress usbAddr = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix usbPrefix = asIpPrefix(usbAddr);
- assertNotEquals(predefinedPrefix, usbPrefix);
- assertNotEquals(hotspotPrefix2, usbPrefix);
- // - Disable wifi upstream, then wifi's prefix can be selected again.
- mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork);
- final LinkAddress ethAddr = requestDownstreamAddress(mEthernetIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix ethPrefix = asIpPrefix(ethAddr);
- assertEquals(predefinedPrefix, ethPrefix);
+ public void testChooseDownstreamAddress_noUpstreamConflicts() throws Exception {
+ LinkAddress address = new LinkAddress("192.168.42.42/24");
+ UpstreamNetworkState ns = buildUpstreamNetworkState(mMobileNetwork, address, null, null);
+ updateUpstreamPrefix(ns);
+ // try to look for a /24 in upstream that does not conflict with upstream -> impossible.
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(asIpPrefix(address)));
+
+ IpPrefix prefix = new IpPrefix("192.168.0.0/16");
+ LinkAddress chosenAddress = mPrivateAddressCoordinator.chooseDownstreamAddress(prefix);
+ assertNotNull(chosenAddress);
+ assertTrue(prefix.containsPrefix(asIpPrefix(chosenAddress)));
}
@Test
- public void testChooseAvailablePrefix() throws Exception {
- final int randomAddress = 0x8605; // 134.5
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
- final LinkAddress addr0 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.134.5.
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.134.5/24"), addr0);
- final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.134.13/26"), null,
- makeNetworkCapabilities(TRANSPORT_WIFI));
- updateUpstreamPrefix(wifiUpstream);
-
- // Check whether return address is next prefix of 192.168.134.0/24.
- final LinkAddress addr1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.135.5/24"), addr1);
- final UpstreamNetworkState wifiUpstream2 = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.149.16/19"), null,
- makeNetworkCapabilities(TRANSPORT_WIFI));
- updateUpstreamPrefix(wifiUpstream2);
-
-
- // The conflict range is 128 ~ 159, so the address is 192.168.160.5/24.
- final LinkAddress addr2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.160.5/24"), addr2);
- final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("192.168.129.53/18"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- // Update another conflict upstream which is covered by the previous one (but not the first
- // one) and verify whether this would affect the result.
- final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2,
- new LinkAddress("192.168.170.7/19"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream);
- updateUpstreamPrefix(mobileUpstream2);
-
- // The conflict range are 128 ~ 159 and 159 ~ 191, so the address is 192.168.192.5/24.
- final LinkAddress addr3 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.192.5/24"), addr3);
- final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
- new LinkAddress("192.168.188.133/17"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream3);
-
- // Conflict range: 128 ~ 255. The next available address is 192.168.0.5 because
- // 192.168.134/24 ~ 192.168.255.255/24 is not available.
- final LinkAddress addr4 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.0.5/24"), addr4);
- final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
- new LinkAddress("192.168.3.59/21"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream4);
-
- // Conflict ranges: 128 ~ 255 and 0 ~ 7, so the address is 192.168.8.5/24.
- final LinkAddress addr5 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr5);
- final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
- new LinkAddress("192.168.68.43/21"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream5);
-
- // Update an upstream that does *not* conflict, check whether return the same address
- // 192.168.5/24.
- final LinkAddress addr6 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr6);
- final UpstreamNetworkState mobileUpstream6 = buildUpstreamNetworkState(mMobileNetwork6,
- new LinkAddress("192.168.10.97/21"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream6);
-
- // Conflict ranges: 0 ~ 15 and 128 ~ 255, so the address is 192.168.16.5/24.
- final LinkAddress addr7 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.16.5/24"), addr7);
- final UpstreamNetworkState mobileUpstream7 = buildUpstreamNetworkState(mMobileNetwork6,
- new LinkAddress("192.168.0.0/17"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream7);
-
- // Choose prefix from next range(172.16.0.0/12) when no available prefix in 192.168.0.0/16.
- final LinkAddress addr8 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.134.5/24"), addr8);
- }
-
- @Test
- public void testChoosePrefixFromDifferentRanges() throws Exception {
- final int randomAddress = 0x1f2b2a; // 31.43.42
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
- final LinkAddress classC1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.43.42.
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.43.42/24"), classC1);
- final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.88.23/17"), null,
- makeNetworkCapabilities(TRANSPORT_WIFI));
- updateUpstreamPrefix(wifiUpstream);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
-
- // Check whether return address is next address of prefix 192.168.128.0/17.
- final LinkAddress classC2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.128.42/24"), classC2);
- final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("192.1.2.3/8"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
-
- // Check whether return address is under prefix 172.16.0.0/12.
- final LinkAddress classB1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.31.43.42/24"), classB1);
- final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2,
- new LinkAddress("172.28.123.100/14"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream2);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
-
- // 172.28.0.0 ~ 172.31.255.255 is not available.
- // Check whether return address is next address of prefix 172.16.0.0/14.
- final LinkAddress classB2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.0.42/24"), classB2);
-
- // Check whether new downstream is next address of address 172.16.0.42/24.
- final LinkAddress classB3 = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.1.42/24"), classB3);
- final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
- new LinkAddress("172.16.0.1/24"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream3);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
- verify(mUsbIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-
- // Check whether return address is next address of prefix 172.16.1.42/24.
- final LinkAddress classB4 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.2.42/24"), classB4);
- final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
- new LinkAddress("172.16.0.1/13"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream4);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
- verifyNotifyConflictAndRelease(mUsbIpServer);
-
- // Check whether return address is next address of prefix 172.16.0.1/13.
- final LinkAddress classB5 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.24.0.42/24"), classB5);
- // Check whether return address is next address of prefix 172.24.0.42/24.
- final LinkAddress classB6 = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.24.1.42/24"), classB6);
- final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
- new LinkAddress("172.24.0.1/12"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- updateUpstreamPrefix(mobileUpstream5);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
- verifyNotifyConflictAndRelease(mUsbIpServer);
-
- // Check whether return address is prefix 10.0.0.0/8 + subAddress 0.31.43.42.
- final LinkAddress classA1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("10.31.43.42/24"), classA1);
- // Check whether new downstream is next address of address 10.31.43.42/24.
- final LinkAddress classA2 = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("10.31.44.42/24"), classA2);
+ public void testChooseDownstreamAddress_excludesWellKnownPrefixes() throws Exception {
+ IpPrefix prefix = new IpPrefix("192.168.0.0/24");
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
+ prefix = new IpPrefix("192.168.100.0/24");
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
+ prefix = new IpPrefix("10.3.0.0/16");
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
}
private void verifyNotifyConflictAndRelease(final IpServer ipServer) throws Exception {
@@ -572,17 +301,18 @@
@Test
public void testEnableSapAndLohsConcurrently() throws Exception {
- // 0x2b05 -> 43.5, 0x8605 -> 134.5
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(0x2b05, 0x8605);
-
final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong hotspot prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
+ assertNotNull(hotspotAddress);
final LinkAddress localHotspotAddress = requestDownstreamAddress(mLocalHotspotIpServer,
CONNECTIVITY_SCOPE_LOCAL, true /* useLastAddress */);
- assertEquals("Wrong local hotspot prefix: ", new LinkAddress("192.168.134.5/24"),
- localHotspotAddress);
+ assertNotNull(localHotspotAddress);
+
+ final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
+ final IpPrefix localHotspotPrefix = asIpPrefix(localHotspotAddress);
+ assertFalse(hotspotPrefix.containsPrefix(localHotspotPrefix));
+ assertFalse(localHotspotPrefix.containsPrefix(hotspotPrefix));
}
@Test
@@ -615,7 +345,7 @@
mPrivateAddressCoordinator =
spy(
new PrivateAddressCoordinator(
- mContext,
+ mConnectivityMgr::getAllNetworks,
mConfig.isRandomPrefixBaseEnabled(),
mConfig.shouldEnableWifiP2pDedicatedIp()));
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomIntForPrefixBase);
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 6ba5d48..9a4945e 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -124,6 +124,7 @@
import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.usb.UsbManager;
+import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.EthernetManager;
import android.net.EthernetManager.TetheredInterfaceCallback;
@@ -374,6 +375,7 @@
@Override
public String getSystemServiceName(Class<?> serviceClass) {
if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE;
+ if (ConnectivityManager.class.equals(serviceClass)) return Context.CONNECTIVITY_SERVICE;
return super.getSystemServiceName(serviceClass);
}
}
diff --git a/DnsResolver/Android.bp b/bpf/dns_helper/Android.bp
similarity index 100%
rename from DnsResolver/Android.bp
rename to bpf/dns_helper/Android.bp
diff --git a/DnsResolver/DnsBpfHelper.cpp b/bpf/dns_helper/DnsBpfHelper.cpp
similarity index 100%
rename from DnsResolver/DnsBpfHelper.cpp
rename to bpf/dns_helper/DnsBpfHelper.cpp
diff --git a/DnsResolver/DnsBpfHelper.h b/bpf/dns_helper/DnsBpfHelper.h
similarity index 100%
rename from DnsResolver/DnsBpfHelper.h
rename to bpf/dns_helper/DnsBpfHelper.h
diff --git a/DnsResolver/DnsBpfHelperTest.cpp b/bpf/dns_helper/DnsBpfHelperTest.cpp
similarity index 100%
rename from DnsResolver/DnsBpfHelperTest.cpp
rename to bpf/dns_helper/DnsBpfHelperTest.cpp
diff --git a/DnsResolver/DnsHelper.cpp b/bpf/dns_helper/DnsHelper.cpp
similarity index 100%
rename from DnsResolver/DnsHelper.cpp
rename to bpf/dns_helper/DnsHelper.cpp
diff --git a/DnsResolver/include/DnsHelperPublic.h b/bpf/dns_helper/include/DnsHelperPublic.h
similarity index 100%
rename from DnsResolver/include/DnsHelperPublic.h
rename to bpf/dns_helper/include/DnsHelperPublic.h
diff --git a/DnsResolver/libcom.android.tethering.dns_helper.map.txt b/bpf/dns_helper/libcom.android.tethering.dns_helper.map.txt
similarity index 100%
rename from DnsResolver/libcom.android.tethering.dns_helper.map.txt
rename to bpf/dns_helper/libcom.android.tethering.dns_helper.map.txt
diff --git a/bpf/headers/include/bpf/KernelUtils.h b/bpf/headers/include/bpf/KernelUtils.h
index 417a5c4..68bc607 100644
--- a/bpf/headers/include/bpf/KernelUtils.h
+++ b/bpf/headers/include/bpf/KernelUtils.h
@@ -55,11 +55,12 @@
isKernelVersion(4, 9) || // minimum for Android S & T
isKernelVersion(4, 14) || // minimum for Android U
isKernelVersion(4, 19) || // minimum for Android V
- isKernelVersion(5, 4) || // first supported in Android R
+ isKernelVersion(5, 4) || // first supported in Android R, min for W
isKernelVersion(5, 10) || // first supported in Android S
isKernelVersion(5, 15) || // first supported in Android T
isKernelVersion(6, 1) || // first supported in Android U
- isKernelVersion(6, 6); // first supported in Android V
+ isKernelVersion(6, 6) || // first supported in Android V
+ isKernelVersion(6, 12); // first supported in Android W
}
// Figure out the bitness of userspace.
diff --git a/bpf/progs/Android.bp b/bpf/progs/Android.bp
index 52eb1b3..20d194c 100644
--- a/bpf/progs/Android.bp
+++ b/bpf/progs/Android.bp
@@ -47,8 +47,8 @@
"com.android.tethering",
],
visibility: [
+ "//packages/modules/Connectivity/bpf/dns_helper",
"//packages/modules/Connectivity/bpf/netd",
- "//packages/modules/Connectivity/DnsResolver",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service/native/libs/libclat",
"//packages/modules/Connectivity/Tethering",
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
index c11c6c0..14b70d0 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -26,3 +26,12 @@
description: "Controls whether the Android Thread setting max power of channel feature is enabled"
bug: "346686506"
}
+
+flag {
+ name: "epskc_enabled"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "thread_network"
+ description: "Controls whether the Android Thread Ephemeral Key feature is enabled"
+ bug: "348323500"
+}
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 9f26bcf..09a3681 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -507,7 +507,10 @@
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void activateEphemeralKeyMode(@NonNull java.time.Duration, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void deactivateEphemeralKeyMode(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @NonNull public java.time.Duration getMaxEphemeralKeyLifetime();
method public int getThreadVersion();
method public static boolean isAttached(int);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
@@ -526,6 +529,9 @@
field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
+ field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_DISABLED = 0; // 0x0
+ field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_ENABLED = 1; // 0x1
+ field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_IN_USE = 2; // 0x2
field public static final int MAX_POWER_CHANNEL_DISABLED = -2147483648; // 0x80000000
field public static final int STATE_DISABLED = 0; // 0x0
field public static final int STATE_DISABLING = 2; // 0x2
@@ -540,6 +546,7 @@
public static interface ThreadNetworkController.StateCallback {
method public void onDeviceRoleChanged(int);
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public default void onEphemeralKeyStateChanged(int, @Nullable String, @Nullable java.time.Instant);
method public default void onPartitionIdChanged(long);
method public default void onThreadEnableStateChanged(int);
}
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
index 9d0a571..57c365b 100644
--- a/thread/framework/java/android/net/thread/IStateCallback.aidl
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -23,4 +23,6 @@
void onDeviceRoleChanged(int deviceRole);
void onPartitionIdChanged(long partitionId);
void onThreadEnableStateChanged(int enabledState);
+ void onEphemeralKeyStateChanged(
+ int ephemeralKeyState, @nullable String ephemeralKey, long expiryMillis);
}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index b7f68c9..e9cbb83 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -53,4 +53,7 @@
void setConfiguration(in ThreadConfiguration config, in IOperationReceiver receiver);
void registerConfigurationCallback(in IConfigurationReceiver receiver);
void unregisterConfigurationCallback(in IConfigurationReceiver receiver);
+
+ void activateEphemeralKeyMode(long lifetimeMillis, in IOperationReceiver receiver);
+ void deactivateEphemeralKeyMode(in IOperationReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index cb4e8de..1222398 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -40,6 +40,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Duration;
+import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
@@ -82,6 +83,25 @@
/** The Thread radio is being disabled. */
public static final int STATE_DISABLING = 2;
+ /** The ephemeral key mode is disabled. */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ public static final int EPHEMERAL_KEY_DISABLED = 0;
+
+ /**
+ * The ephemeral key mode is enabled, an external commissioner candidate can use the ephemeral
+ * key to connect to this device and get Thread credential shared.
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ public static final int EPHEMERAL_KEY_ENABLED = 1;
+
+ /**
+ * The ephemeral key is in use. This state means there is already an active secure session
+ * connected to this device with the ephemeral key, it's not possible to use the ephemeral key
+ * for new connections in this state.
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ public static final int EPHEMERAL_KEY_IN_USE = 2;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -100,6 +120,13 @@
value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
public @interface EnabledState {}
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = {"EPHEMERAL_KEY_"},
+ value = {EPHEMERAL_KEY_DISABLED, EPHEMERAL_KEY_ENABLED, EPHEMERAL_KEY_IN_USE})
+ public @interface EphemeralKeyState {}
+
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
@@ -110,6 +137,9 @@
@SuppressLint("MinMaxConstant")
public static final int MAX_POWER_CHANNEL_DISABLED = Integer.MIN_VALUE;
+ /** The maximum lifetime of an ephemeral key. @hide */
+ @NonNull private static final Duration EPHEMERAL_KEY_LIFETIME_MAX = Duration.ofMinutes(10);
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({THREAD_VERSION_1_3})
@@ -174,6 +204,87 @@
}
}
+ /** Returns the maximum lifetime allowed when activating ephemeral key mode. */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @NonNull
+ public Duration getMaxEphemeralKeyLifetime() {
+ return EPHEMERAL_KEY_LIFETIME_MAX;
+ }
+
+ /**
+ * Activates ephemeral key mode with a given {@code lifetime}. The ephemeral key is a temporary,
+ * single-use numeric code that is used for Thread Administration Sharing. After activation, the
+ * mode may expire or get deactivated, caller to this method should subscribe to the ephemeral
+ * key state updates with {@link #registerStateCallback} to get notified when the ephemeral key
+ * state changes.
+ *
+ * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The ephemeral
+ * key string contains a sequence of numeric digits 0-9 of user-input friendly length (typically
+ * 9). Subscribers to ephemeral key state updates with {@link #registerStateCallback} will be
+ * notified with a call to {@link #onEphemeralKeyStateChanged}.
+ *
+ * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
+ * specific error:
+ *
+ * <ul>
+ * <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not
+ * attached to Thread network
+ * <li>{@link ThreadNetworkException#ERROR_BUSY} when ephemeral key mode is already activated
+ * on the device, caller can recover from this error when the ephemeral key mode gets
+ * deactivated
+ * </ul>
+ *
+ * @param lifetime valid lifetime of the generated ephemeral key, should be larger than {@link
+ * Duration#ZERO} and at most the duration returned by {@link #getMaxEphemeralKeyLifetime}.
+ * @param executor the executor on which to execute {@code receiver}
+ * @param receiver the receiver to receive the result of this operation
+ * @throws IllegalArgumentException if the {@code lifetime} exceeds the allowed range
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void activateEphemeralKeyMode(
+ @NonNull Duration lifetime,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ if (lifetime.compareTo(Duration.ZERO) <= 0
+ || lifetime.compareTo(EPHEMERAL_KEY_LIFETIME_MAX) > 0) {
+ throw new IllegalArgumentException(
+ "Invalid ephemeral key lifetime: the value must be in range of (0, "
+ + EPHEMERAL_KEY_LIFETIME_MAX
+ + "]");
+ }
+ long lifetimeMillis = lifetime.toMillis();
+ try {
+ mControllerService.activateEphemeralKeyMode(
+ lifetimeMillis, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Deactivates ephemeral key mode. If there is an active connection with the ephemeral key, the
+ * connection will be terminated.
+ *
+ * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The call will
+ * always succeed if the device is not in ephemeral key mode.
+ *
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive the result of this operation
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void deactivateEphemeralKeyMode(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ try {
+ mControllerService.deactivateEphemeralKeyMode(
+ new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/** Returns the Thread version this device is operating on. */
@ThreadVersion
public int getThreadVersion() {
@@ -248,6 +359,24 @@
* @param enabledState the new Thread enabled state
*/
default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
+
+ /**
+ * The ephemeral key state has changed.
+ *
+ * @param ephemeralKeyState the ephemeral key state
+ * @param ephemeralKey the ephemeral key string which contains a sequence of numeric digits
+ * 0-9 of user-input friendly length (typically 9), or {@code null} if {@code
+ * ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED} or the caller doesn't have the
+ * permission {@link android.permission.THREAD_NETWORK_PRIVILEGED}
+ * @param expiry a timestamp of when the ephemeral key will expireor {@code null} if {@code
+ * ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED}
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ default void onEphemeralKeyStateChanged(
+ @EphemeralKeyState int ephemeralKeyState,
+ @Nullable String ephemeralKey,
+ @Nullable Instant expiry) {}
}
private static final class StateCallbackProxy extends IStateCallback.Stub {
@@ -288,13 +417,37 @@
Binder.restoreCallingIdentity(identity);
}
}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ @EphemeralKeyState int ephemeralKeyState, String ephemeralKey, long expiryMillis) {
+ if (!Flags.epskcEnabled()) {
+ throw new IllegalStateException(
+ "This should not be called when Ephemeral key API is disabled");
+ }
+
+ final long identity = Binder.clearCallingIdentity();
+ final Instant expiry =
+ ephemeralKeyState == EPHEMERAL_KEY_DISABLED
+ ? null
+ : Instant.ofEpochMilli(expiryMillis);
+
+ try {
+ mExecutor.execute(
+ () ->
+ mCallback.onEphemeralKeyStateChanged(
+ ephemeralKeyState, ephemeralKey, expiry));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
}
/**
* Registers a callback to be called when Thread network states are changed.
*
- * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
- * existing states.
+ * <p>Upon return of this method, all methods of {@code callback} will be invoked immediately
+ * with existing states. The order of the invoked callbacks is not guaranteed.
*
* @param executor the executor to execute the {@code callback}
* @param callback the callback to receive Thread network state changes
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 57fea34..3d854d7 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -15,6 +15,7 @@
package com.android.server.thread;
import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE;
import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
@@ -26,6 +27,7 @@
import static android.net.thread.ActiveOperationalDataset.MESH_LOCAL_PREFIX_FIRST_BYTE;
import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
@@ -855,6 +857,47 @@
}
@Override
+ public void activateEphemeralKeyMode(long lifetimeMillis, IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(
+ () ->
+ activateEphemeralKeyModeInternal(
+ lifetimeMillis, new OperationReceiverWrapper(receiver)));
+ }
+
+ private void activateEphemeralKeyModeInternal(
+ long lifetimeMillis, OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().activateEphemeralKeyMode(lifetimeMillis, newOtStatusReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("otDaemon.activateEphemeralKeyMode failed", e);
+ receiver.onError(e);
+ }
+ }
+
+ @Override
+ public void deactivateEphemeralKeyMode(IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(
+ () -> deactivateEphemeralKeyModeInternal(new OperationReceiverWrapper(receiver)));
+ }
+
+ private void deactivateEphemeralKeyModeInternal(OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().deactivateEphemeralKeyMode(newOtStatusReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("otDaemon.deactivateEphemeralKeyMode failed", e);
+ receiver.onError(e);
+ }
+ }
+
+ @Override
public void createRandomizedDataset(
String networkName, IActiveOperationalDatasetReceiver receiver) {
ActiveOperationalDatasetReceiverWrapper receiverWrapper =
@@ -1003,7 +1046,14 @@
@Override
public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
- mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
+ boolean hasThreadPrivilegedPermission =
+ (mContext.checkCallingOrSelfPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+ == PERMISSION_GRANTED);
+
+ mHandler.post(
+ () ->
+ mOtDaemonCallbackProxy.registerStateCallback(
+ stateCallback, hasThreadPrivilegedPermission));
}
@Override
@@ -1458,9 +1508,13 @@
final IBinder.DeathRecipient deathRecipient;
- CallbackMetadata(IBinder.DeathRecipient deathRecipient) {
+ final boolean hasThreadPrivilegedPermission;
+
+ CallbackMetadata(
+ IBinder.DeathRecipient deathRecipient, boolean hasThreadPrivilegedPermission) {
this.id = allocId();
this.deathRecipient = deathRecipient;
+ this.hasThreadPrivilegedPermission = hasThreadPrivilegedPermission;
}
private static long allocId() {
@@ -1502,7 +1556,8 @@
private ActiveOperationalDataset mActiveDataset;
private PendingOperationalDataset mPendingDataset;
- public void registerStateCallback(IStateCallback callback) {
+ public void registerStateCallback(
+ IStateCallback callback, boolean hasThreadPrivilegedPermission) {
checkOnHandlerThread();
if (mStateCallbacks.containsKey(callback)) {
throw new IllegalStateException("Registering the same IStateCallback twice");
@@ -1510,7 +1565,8 @@
IBinder.DeathRecipient deathRecipient =
() -> mHandler.post(() -> unregisterStateCallback(callback));
- CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+ CallbackMetadata callbackMetadata =
+ new CallbackMetadata(deathRecipient, hasThreadPrivilegedPermission);
mStateCallbacks.put(callback, callbackMetadata);
try {
callback.asBinder().linkToDeath(deathRecipient, 0);
@@ -1543,7 +1599,8 @@
IBinder.DeathRecipient deathRecipient =
() -> mHandler.post(() -> unregisterDatasetCallback(callback));
- CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+ CallbackMetadata callbackMetadata =
+ new CallbackMetadata(deathRecipient, true /* hasThreadPrivilegedPermission */);
mOpDatasetCallbacks.put(callback, callbackMetadata);
try {
callback.asBinder().linkToDeath(deathRecipient, 0);
@@ -1622,16 +1679,18 @@
}
@Override
- public void onStateChanged(OtDaemonState newState, long listenerId) {
+ public void onStateChanged(@NonNull OtDaemonState newState, long listenerId) {
mHandler.post(() -> onStateChangedInternal(newState, listenerId));
}
private void onStateChangedInternal(OtDaemonState newState, long listenerId) {
checkOnHandlerThread();
+
onInterfaceStateChanged(newState.isInterfaceUp);
onDeviceRoleChanged(newState.deviceRole, listenerId);
onPartitionIdChanged(newState.partitionId, listenerId);
onThreadEnabledChanged(newState.threadEnabled, listenerId);
+ onEphemeralKeyStateChanged(newState, listenerId);
mState = newState;
ActiveOperationalDataset newActiveDataset;
@@ -1707,6 +1766,43 @@
}
}
+ private void onEphemeralKeyStateChanged(OtDaemonState newState, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = isEphemeralKeyStateChanged(mState, newState);
+
+ for (var callbackEntry : mStateCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ String passcode =
+ callbackEntry.getValue().hasThreadPrivilegedPermission
+ ? newState.ephemeralKeyPasscode
+ : null;
+ if (newState.ephemeralKeyState == EPHEMERAL_KEY_DISABLED) {
+ passcode = null;
+ }
+ try {
+ callbackEntry
+ .getKey()
+ .onEphemeralKeyStateChanged(
+ newState.ephemeralKeyState,
+ passcode,
+ newState.ephemeralKeyExpiryMillis);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ private static boolean isEphemeralKeyStateChanged(
+ OtDaemonState oldState, @NonNull OtDaemonState newState) {
+ if (oldState == null) return true;
+ if (oldState.ephemeralKeyState != newState.ephemeralKeyState) return true;
+ if (oldState.ephemeralKeyState == EPHEMERAL_KEY_DISABLED) return false;
+ return (!Objects.equals(oldState.ephemeralKeyPasscode, newState.ephemeralKeyPasscode)
+ || oldState.ephemeralKeyExpiryMillis != newState.ephemeralKeyExpiryMillis);
+ }
+
private void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeDataset, long listenerId) {
checkOnHandlerThread();
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index 34aabe2..e954d3b 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -56,4 +56,14 @@
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
</test>
+
+ <!--
+ This doesn't override a read-only flag, to run the tests locally with `epskc_enabled` flag
+ enabled, set the flag to `is_fixed_read_only: false`. This should be removed after the
+ `epskc_enabled` flag is rolled out.
+ -->
+ <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
+ <option name="flag-value"
+ value="thread_network/com.android.net.thread.flags.epskc_enabled=true"/>
+ </target_preparer>
</configuration>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index c048394..1792bfb 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -27,11 +27,14 @@
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
+import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_DISABLED;
+import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_ENABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
+import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
@@ -72,6 +75,8 @@
import android.os.HandlerThread;
import android.os.OutcomeReceiver;
import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
@@ -82,6 +87,8 @@
import com.android.net.thread.flags.Flags;
import com.android.testutils.FunctionalUtils.ThrowingRunnable;
+import kotlin.Triple;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
@@ -96,6 +103,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -134,9 +142,13 @@
put(VALID_CHANNEL, VALID_POWER);
}
};
+ private static final Duration EPHEMERAL_KEY_LIFETIME = Duration.ofSeconds(1);
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private final Context mContext = ApplicationProvider.getApplicationContext();
private ExecutorService mExecutor;
private ThreadNetworkController mController;
@@ -164,6 +176,7 @@
setEnabledAndWait(mController, true);
setConfigurationAndWait(mController, DEFAULT_CONFIG);
+ deactivateEphemeralKeyModeAndWait(mController);
}
@After
@@ -183,6 +196,7 @@
}
}
mConfigurationCallbacksToCleanUp.clear();
+ deactivateEphemeralKeyModeAndWait(mController);
}
@Test
@@ -819,6 +833,221 @@
listener.unregisterStateCallback();
}
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void getMaxEphemeralKeyLifetime_isLargerThanZero() {
+ assertThat(mController.getMaxEphemeralKeyLifetime()).isGreaterThan(Duration.ZERO);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withPrivilegedPermission_succeeds() throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+ CompletableFuture<Void> startFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME,
+ mExecutor,
+ newOutcomeReceiver(startFuture)));
+
+ startFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME, mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withZeroLifetime_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.activateEphemeralKeyMode(Duration.ZERO, mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withInvalidLargeLifetime_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+ Duration lifetime = mController.getMaxEphemeralKeyLifetime().plusMillis(1);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.activateEphemeralKeyMode(lifetime, Runnable::run, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_concurrentRequests_secondOneFailsWithBusyError()
+ throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+ CompletableFuture<Void> future1 = new CompletableFuture<>();
+ CompletableFuture<Void> future2 = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME, mExecutor, newOutcomeReceiver(future1));
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME, mExecutor, newOutcomeReceiver(future2));
+ });
+
+ var thrown =
+ assertThrows(
+ ExecutionException.class,
+ () -> {
+ future2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ });
+ var threadException = (ThreadNetworkException) thrown.getCause();
+ assertThat(threadException.getErrorCode()).isEqualTo(ERROR_BUSY);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void deactivateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.deactivateEphemeralKeyMode(mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_permissionsGranted_returnsCurrentState() throws Exception {
+ CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
+ CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
+ CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
+ StateCallback callback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ int state, String ephemeralKey, Instant expiry) {
+ stateFuture.complete(state);
+ ephemeralKeyFuture.complete(ephemeralKey);
+ expiryFuture.complete(expiry);
+ }
+ };
+
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.registerStateCallback(mExecutor, callback));
+
+ try {
+ assertThat(stateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+ .isEqualTo(EPHEMERAL_KEY_DISABLED);
+ assertThat(ephemeralKeyFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ assertThat(expiryFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_withoutThreadPriviledgedPermission_returnsNullEphemeralKey()
+ throws Exception {
+ CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
+ CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
+ CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
+ StateCallback callback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ int state, String ephemeralKey, Instant expiry) {
+ stateFuture.complete(state);
+ ephemeralKeyFuture.complete(ephemeralKey);
+ expiryFuture.complete(expiry);
+ }
+ };
+ joinRandomizedDatasetAndWait(mController);
+ activateEphemeralKeyModeAndWait(mController);
+
+ runAsShell(
+ ACCESS_NETWORK_STATE, () -> mController.registerStateCallback(mExecutor, callback));
+
+ try {
+ assertThat(stateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+ .isEqualTo(EPHEMERAL_KEY_ENABLED);
+ assertThat(ephemeralKeyFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ assertThat(
+ expiryFuture
+ .get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)
+ .isAfter(Instant.now()))
+ .isTrue();
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_ephemralKeyStateChanged_returnsUpdatedState() throws Exception {
+ EphemeralKeyStateListener listener = new EphemeralKeyStateListener(mController);
+ joinRandomizedDatasetAndWait(mController);
+
+ try {
+ activateEphemeralKeyModeAndWait(mController);
+ deactivateEphemeralKeyModeAndWait(mController);
+
+ listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_DISABLED);
+ listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+ listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_DISABLED);
+ } finally {
+ listener.unregisterStateCallback();
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_epskcEnabled_returnsSameExpiry() throws Exception {
+ EphemeralKeyStateListener listener1 = new EphemeralKeyStateListener(mController);
+ Triple<Integer, String, Instant> epskc1;
+ try {
+ joinRandomizedDatasetAndWait(mController);
+ activateEphemeralKeyModeAndWait(mController);
+ epskc1 = listener1.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+ } finally {
+ listener1.unregisterStateCallback();
+ }
+
+ EphemeralKeyStateListener listener2 = new EphemeralKeyStateListener(mController);
+ try {
+ Triple<Integer, String, Instant> epskc2 =
+ listener2.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+
+ assertThat(epskc2.getSecond()).isEqualTo(epskc1.getSecond());
+ assertThat(epskc2.getThird()).isEqualTo(epskc1.getThird());
+ } finally {
+ listener2.unregisterStateCallback();
+ }
+ }
+
// TODO (b/322437869): add test case to verify when Thread is in DISABLING state, any commands
// (join/leave/scheduleMigration/setEnabled) fail with ERROR_BUSY. This is not currently tested
// because DISABLING has very short lifecycle, it's not possible to guarantee the command can be
@@ -1274,6 +1503,71 @@
setFuture.get(SET_CONFIGURATION_TIMEOUT_MILLIS, MILLISECONDS);
}
+ private void deactivateEphemeralKeyModeAndWait(ThreadNetworkController controller)
+ throws Exception {
+ CompletableFuture<Void> clearFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ controller.deactivateEphemeralKeyMode(
+ mExecutor, newOutcomeReceiver(clearFuture)));
+ clearFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private void activateEphemeralKeyModeAndWait(ThreadNetworkController controller)
+ throws Exception {
+ CompletableFuture<Void> startFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ controller.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME,
+ mExecutor,
+ newOutcomeReceiver(startFuture)));
+ startFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private class EphemeralKeyStateListener {
+ private ArrayTrackRecord<Triple<Integer, String, Instant>> mEphemeralKeyStates =
+ new ArrayTrackRecord<>();
+ private final ArrayTrackRecord<Triple<Integer, String, Instant>>.ReadHead mReadHead =
+ mEphemeralKeyStates.newReadHead();
+ ThreadNetworkController mController;
+ StateCallback mCallback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ int state, String ephemeralKey, Instant expiry) {
+ mEphemeralKeyStates.add(new Triple<>(state, ephemeralKey, expiry));
+ }
+ };
+
+ EphemeralKeyStateListener(ThreadNetworkController controller) {
+ this.mController = controller;
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.registerStateCallback(mExecutor, mCallback));
+ }
+
+ // Expect that EphemeralKey has the expected state, and return a Triple of <state,
+ // passcode, expiry>.
+ public Triple<Integer, String, Instant> expectThreadEphemeralKeyMode(int state) {
+ Triple<Integer, String, Instant> epskc =
+ mReadHead.poll(
+ ENABLED_TIMEOUT_MILLIS, e -> Objects.equals(e.getFirst(), state));
+ assertThat(epskc).isNotNull();
+ return epskc;
+ }
+
+ public void unregisterStateCallback() {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
+ }
+ }
+
private CompletableFuture joinRandomizedDataset(
ThreadNetworkController controller, String networkName) throws Exception {
ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index 0423578..62801bf 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -17,6 +17,8 @@
package android.net.thread;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
@@ -26,6 +28,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
@@ -134,6 +137,16 @@
return (IOperationalDatasetCallback) invocation.getArguments()[0];
}
+ private static IOperationReceiver getActivateEphemeralKeyModeReceiver(
+ InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
+ private static IOperationReceiver getDeactivateEphemeralKeyModeReceiver(
+ InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[0];
+ }
+
@Test
public void registerStateCallback_callbackIsInvokedWithCallingAppIdentity() throws Exception {
setBinderUid(SYSTEM_UID);
@@ -440,4 +453,88 @@
assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
assertThat(callbackUid.get()).isEqualTo(Process.myUid());
}
+
+ @Test
+ public void activateEphemeralKeyMode_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+ Duration lifetime = Duration.ofSeconds(100);
+ doAnswer(
+ invoke -> {
+ getActivateEphemeralKeyModeReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .activateEphemeralKeyMode(anyLong(), any(IOperationReceiver.class));
+ mController.activateEphemeralKeyMode(
+ lifetime, Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getActivateEphemeralKeyModeReceiver(invoke)
+ .onError(ERROR_FAILED_PRECONDITION, "");
+ return null;
+ })
+ .when(mMockService)
+ .activateEphemeralKeyMode(anyLong(), any(IOperationReceiver.class));
+ mController.activateEphemeralKeyMode(
+ lifetime,
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+
+ @Test
+ public void deactivateEphemeralKeyMode_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+ doAnswer(
+ invoke -> {
+ getDeactivateEphemeralKeyModeReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .deactivateEphemeralKeyMode(any(IOperationReceiver.class));
+ mController.deactivateEphemeralKeyMode(
+ Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getDeactivateEphemeralKeyModeReceiver(invoke)
+ .onError(ERROR_INTERNAL_ERROR, "");
+ return null;
+ })
+ .when(mMockService)
+ .deactivateEphemeralKeyMode(any(IOperationReceiver.class));
+ mController.deactivateEphemeralKeyMode(
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index d8cdbc4..b97e2b7 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -838,4 +838,26 @@
verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
}
+
+ @Test
+ public void activateEphemeralKeyMode_succeed() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+ mService.activateEphemeralKeyMode(1_000L, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ }
+
+ @Test
+ public void deactivateEphemeralKeyMode_succeed() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+ mService.deactivateEphemeralKeyMode(mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ }
}