Merge "Thread: simulate Thread on eth1 for tests" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index d8d4c21..2856554 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,4 +1,152 @@
{
+ "captiveportal-networkstack-resolve-tethering-mainline-presubmit": [
+ {
+ "name": "CtsNetTestCasesLatestSdk",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
+ {
+ "name": "CtsNetTestCasesMaxTargetSdk30",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
+ {
+ "name": "CtsNetTestCasesMaxTargetSdk31",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
+ {
+ "name": "CtsNetTestCasesMaxTargetSdk33",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
+ {
+ "name": "bpf_existence_test"
+ },
+ {
+ "name": "connectivity_native_test"
+ },
+ {
+ "name": "netd_updatable_unit_test"
+ },
+ {
+ "name": "ConnectivityCoverageTests",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ }
+ ]
+ },
+ {
+ "name": "libnetworkstats_test"
+ },
+ {
+ "name": "NetHttpCoverageTests",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ // These sometimes take longer than 1 min which is the presubmit timeout
+ "exclude-annotation": "androidx.test.filters.LargeTest"
+ }
+ ]
+ },
+ {
+ "name": "CtsTetheringTestLatestSdk",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
+ }
+ ]
+ }
+ ],
+ "captiveportal-networkstack-mainline-presubmit": [
+ // Test with APK modules only, in cases where APEX is not supported, or the other modules
+ // were simply not updated
+ {
+ "name": "CtsNetTestCasesLatestSdk",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.ConnectivityModuleTest"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.DnsResolverModuleTest"
+ }
+ ]
+ }
+ ],
+ "tethering-mainline-presubmit": [
+ // Test with connectivity/tethering module only, to catch integration issues with older versions
+ // of other modules. "new tethering + old NetworkStack" is not a configuration that should
+ // really exist in the field, but there is no strong guarantee, and it is required by MTS
+ // testing for module qualification, where modules are tested independently.
+ {
+ "name": "CtsNetTestCasesLatestSdk",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.DnsResolverModuleTest"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
+ }
+ ]
+ }
+ ],
"presubmit": [
{
"name": "ConnectivityCoverageTests",
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 40e6cd8..55a96ac 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -91,3 +91,11 @@
description: "Flag for metered network firewall chain API"
bug: "332628891"
}
+
+flag {
+ name: "blocked_reason_oem_deny_chains"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for oem deny chains blocked reasons API"
+ bug: "328732146"
+}
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index 4e10a96..70c9bc8 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -124,7 +124,7 @@
private IpSecTransform activate()
throws IOException, IpSecManager.ResourceUnavailableException,
IpSecManager.SpiUnavailableException {
- synchronized (this) {
+ synchronized (mLock) {
try {
IpSecTransformResponse result = getIpSecManager(mContext).createTransform(
mConfig, new Binder(), mContext.getOpPackageName());
@@ -164,20 +164,23 @@
public void close() {
Log.d(TAG, "Removing Transform with Id " + mResourceId);
- // Always safe to attempt cleanup
- if (mResourceId == INVALID_RESOURCE_ID) {
- mCloseGuard.close();
- return;
- }
- try {
- getIpSecManager(mContext).deleteTransform(mResourceId);
- } catch (Exception e) {
- // On close we swallow all random exceptions since failure to close is not
- // actionable by the user.
- Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
- } finally {
- mResourceId = INVALID_RESOURCE_ID;
- mCloseGuard.close();
+ synchronized(mLock) {
+ // Always safe to attempt cleanup
+ if (mResourceId == INVALID_RESOURCE_ID) {
+ mCloseGuard.close();
+ return;
+ }
+
+ try {
+ getIpSecManager(mContext).deleteTransform(mResourceId);
+ } catch (Exception e) {
+ // On close we swallow all random exceptions since failure to close is not
+ // actionable by the user.
+ Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
+ } finally {
+ mResourceId = INVALID_RESOURCE_ID;
+ mCloseGuard.close();
+ }
}
}
@@ -196,14 +199,17 @@
}
private final IpSecConfig mConfig;
- private int mResourceId;
+ private final Object mLock = new Object();
+ private int mResourceId; // Partly guarded by mLock to ensure basic safety, not correctness
private final Context mContext;
private final CloseGuard mCloseGuard = CloseGuard.get();
/** @hide */
@VisibleForTesting
public int getResourceId() {
- return mResourceId;
+ synchronized(mLock) {
+ return mResourceId;
+ }
}
/**
@@ -224,8 +230,10 @@
// TODO: Consider adding check to prevent DDoS attack.
try {
- final IpSecTransformState ipSecTransformState =
- getIpSecManager(mContext).getTransformState(mResourceId);
+ IpSecTransformState ipSecTransformState;
+ synchronized(mLock) {
+ ipSecTransformState = getIpSecManager(mContext).getTransformState(mResourceId);
+ }
executor.execute(
() -> {
callback.onResult(ipSecTransformState);
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index b2aafa0..d233f3e 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -52,6 +52,7 @@
field public static final int BLOCKED_REASON_LOCKDOWN_VPN = 16; // 0x10
field public static final int BLOCKED_REASON_LOW_POWER_STANDBY = 32; // 0x20
field public static final int BLOCKED_REASON_NONE = 0; // 0x0
+ field @FlaggedApi("com.android.net.flags.blocked_reason_oem_deny_chains") public static final int BLOCKED_REASON_OEM_DENY = 128; // 0x80
field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
field @FlaggedApi("com.android.net.flags.basic_background_restrictions_enabled") public static final int FIREWALL_CHAIN_BACKGROUND = 6; // 0x6
field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 4e01fee..4099e2a 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -37,6 +37,18 @@
import static android.net.BpfNetMapsConstants.RESTRICTED_MATCH;
import static android.net.BpfNetMapsConstants.STANDBY_MATCH;
import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_ADMIN_DISABLED;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_OEM_DENY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_RESTRICTED_MODE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
@@ -238,6 +250,67 @@
}
/**
+ * Get blocked reasons for specified uid
+ *
+ * @param uid Target Uid
+ * @return Reasons of network access blocking for an UID
+ */
+ public static int getUidNetworkingBlockedReasons(final int uid,
+ IBpfMap<S32, U32> configurationMap,
+ IBpfMap<S32, UidOwnerValue> uidOwnerMap,
+ IBpfMap<S32, U8> dataSaverEnabledMap
+ ) {
+ final long uidRuleConfig;
+ final long uidMatch;
+ try {
+ uidRuleConfig = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val;
+ final UidOwnerValue value = uidOwnerMap.getValue(new Struct.S32(uid));
+ uidMatch = (value != null) ? value.rule : 0L;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get firewall chain status: " + Os.strerror(e.errno));
+ }
+ final long blockingMatches = (uidRuleConfig & ~uidMatch & sMaskDropIfUnset)
+ | (uidRuleConfig & uidMatch & sMaskDropIfSet);
+
+ int blockedReasons = BLOCKED_REASON_NONE;
+ if ((blockingMatches & POWERSAVE_MATCH) != 0) {
+ blockedReasons |= BLOCKED_REASON_BATTERY_SAVER;
+ }
+ if ((blockingMatches & DOZABLE_MATCH) != 0) {
+ blockedReasons |= BLOCKED_REASON_DOZE;
+ }
+ if ((blockingMatches & STANDBY_MATCH) != 0) {
+ blockedReasons |= BLOCKED_REASON_APP_STANDBY;
+ }
+ if ((blockingMatches & RESTRICTED_MATCH) != 0) {
+ blockedReasons |= BLOCKED_REASON_RESTRICTED_MODE;
+ }
+ if ((blockingMatches & LOW_POWER_STANDBY_MATCH) != 0) {
+ blockedReasons |= BLOCKED_REASON_LOW_POWER_STANDBY;
+ }
+ if ((blockingMatches & BACKGROUND_MATCH) != 0) {
+ blockedReasons |= BLOCKED_REASON_APP_BACKGROUND;
+ }
+ if ((blockingMatches & (OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)) != 0) {
+ blockedReasons |= BLOCKED_REASON_OEM_DENY;
+ }
+
+ // Metered chains are not enabled by configuration map currently.
+ if ((uidMatch & PENALTY_BOX_USER_MATCH) != 0) {
+ blockedReasons |= BLOCKED_METERED_REASON_USER_RESTRICTED;
+ }
+ if ((uidMatch & PENALTY_BOX_ADMIN_MATCH) != 0) {
+ blockedReasons |= BLOCKED_METERED_REASON_ADMIN_DISABLED;
+ }
+ if ((uidMatch & HAPPY_BOX_MATCH) == 0 && getDataSaverEnabled(dataSaverEnabledMap)) {
+ blockedReasons |= BLOCKED_METERED_REASON_DATA_SAVER;
+ }
+
+ return blockedReasons;
+ }
+
+ /**
* Return whether the network is blocked by firewall chains for the given uid.
*
* Note that {@link #getDataSaverEnabled(IBpfMap)} has a latency before V.
@@ -263,27 +336,16 @@
return false;
}
- final long uidRuleConfig;
- final long uidMatch;
- try {
- uidRuleConfig = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val;
- final UidOwnerValue value = uidOwnerMap.getValue(new Struct.S32(uid));
- uidMatch = (value != null) ? value.rule : 0L;
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno,
- "Unable to get firewall chain status: " + Os.strerror(e.errno));
+ final int blockedReasons = getUidNetworkingBlockedReasons(
+ uid,
+ configurationMap,
+ uidOwnerMap,
+ dataSaverEnabledMap);
+ if (isNetworkMetered) {
+ return blockedReasons != BLOCKED_REASON_NONE;
+ } else {
+ return (blockedReasons & ~BLOCKED_METERED_REASON_MASK) != BLOCKED_REASON_NONE;
}
-
- final boolean blockedByAllowChains = 0 != (uidRuleConfig & ~uidMatch & sMaskDropIfUnset);
- final boolean blockedByDenyChains = 0 != (uidRuleConfig & uidMatch & sMaskDropIfSet);
- if (blockedByAllowChains || blockedByDenyChains) {
- return true;
- }
-
- if (!isNetworkMetered) return false;
- if ((uidMatch & (PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH)) != 0) return true;
- if ((uidMatch & HAPPY_BOX_MATCH) != 0) return false;
- return getDataSaverEnabled(dataSaverEnabledMap);
}
/**
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 7823258..48ed732 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -130,6 +130,8 @@
"com.android.net.flags.basic_background_restrictions_enabled";
static final String METERED_NETWORK_FIREWALL_CHAINS =
"com.android.net.flags.metered_network_firewall_chains";
+ static final String BLOCKED_REASON_OEM_DENY_CHAINS =
+ "com.android.net.flags.blocked_reason_oem_deny_chains";
}
/**
@@ -913,6 +915,19 @@
public static final int BLOCKED_REASON_APP_BACKGROUND = 1 << 6;
/**
+ * Flag to indicate that an app is subject to OEM-specific application restrictions that would
+ * result in its network access being blocked.
+ *
+ * @see #FIREWALL_CHAIN_OEM_DENY_1
+ * @see #FIREWALL_CHAIN_OEM_DENY_2
+ * @see #FIREWALL_CHAIN_OEM_DENY_3
+ * @hide
+ */
+ @FlaggedApi(Flags.BLOCKED_REASON_OEM_DENY_CHAINS)
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static final int BLOCKED_REASON_OEM_DENY = 1 << 7;
+
+ /**
* Flag to indicate that an app is subject to Data saver restrictions that would
* result in its metered network access being blocked.
*
@@ -952,6 +967,7 @@
BLOCKED_REASON_LOCKDOWN_VPN,
BLOCKED_REASON_LOW_POWER_STANDBY,
BLOCKED_REASON_APP_BACKGROUND,
+ BLOCKED_REASON_OEM_DENY,
BLOCKED_METERED_REASON_DATA_SAVER,
BLOCKED_METERED_REASON_USER_RESTRICTED,
BLOCKED_METERED_REASON_ADMIN_DISABLED,
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index a80db85..02ef8b7 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -99,6 +99,28 @@
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static final long ENABLE_MATCH_LOCAL_NETWORK = 319212206L;
+ /**
+ * On Android {@link android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher releases,
+ * network access from apps targeting Android 36 or higher that do not have the
+ * {@link android.Manifest.permission#INTERNET} permission is considered blocked.
+ * This results in API behaviors change for apps without
+ * {@link android.Manifest.permission#INTERNET} permission.
+ * {@link android.net.NetworkInfo} returned from {@link android.net.ConnectivityManager} APIs
+ * always has {@link android.net.NetworkInfo.DetailedState#BLOCKED}.
+ * {@link android.net.ConnectivityManager#getActiveNetwork()} always returns null.
+ * {@link android.net.ConnectivityManager.NetworkCallback#onBlockedStatusChanged()} is always
+ * called with blocked=true.
+ * <p>
+ * For backwards compatibility, apps running on older releases, or targeting older SDK levels,
+ * network access from apps without {@link android.Manifest.permission#INTERNET} permission is
+ * considered not blocked even though apps cannot access any networks.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = 35) // TODO: change to VANILLA_ICE_CREAM.
+ public static final long NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION = 333340911L;
+
private ConnectivityCompatChanges() {
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index 39e8bcc..36f3982 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -34,6 +34,7 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseArray;
+import android.util.SparseIntArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
@@ -87,6 +88,12 @@
private static final String[] DNS_SD_SERVICE_TYPE =
new String[] { "_services", "_dns-sd", "_udp", LOCAL_TLD };
+ private enum RecordConflictType {
+ NO_CONFLICT,
+ CONFLICT,
+ IDENTICAL
+ }
+
@NonNull
private final Random mDelayGenerator = new Random();
// Map of service unique ID -> records for service
@@ -1172,38 +1179,49 @@
* {@link MdnsInterfaceAdvertiser#CONFLICT_HOST}.
*/
public Map<Integer, Integer> getConflictingServices(MdnsPacket packet) {
- // Avoid allocating a new set for each incoming packet: use an empty set by default.
- Map<Integer, Integer> conflicting = Collections.emptyMap();
+ Map<Integer, Integer> conflicting = new ArrayMap<>();
for (MdnsRecord record : packet.answers) {
+ SparseIntArray conflictingWithRecord = new SparseIntArray();
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
if (registration.exiting) continue;
- int conflictType = 0;
+ final RecordConflictType conflictForService =
+ conflictForService(record, registration);
+ final RecordConflictType conflictForHost = conflictForHost(record, registration);
- if (conflictForService(record, registration)) {
- conflictType |= CONFLICT_SERVICE;
+ // Identical record is found in the repository so there won't be a conflict.
+ if (conflictForService == RecordConflictType.IDENTICAL
+ || conflictForHost == RecordConflictType.IDENTICAL) {
+ conflictingWithRecord.clear();
+ break;
}
- if (conflictForHost(record, registration)) {
+ int conflictType = 0;
+ if (conflictForService == RecordConflictType.CONFLICT) {
+ conflictType |= CONFLICT_SERVICE;
+ }
+ if (conflictForHost == RecordConflictType.CONFLICT) {
conflictType |= CONFLICT_HOST;
}
if (conflictType != 0) {
- if (conflicting.isEmpty()) {
- // Conflict was found: use a mutable set
- conflicting = new ArrayMap<>();
- }
final int serviceId = mServices.keyAt(i);
- conflicting.put(serviceId, conflictType);
+ conflictingWithRecord.put(serviceId, conflictType);
}
}
+ for (int i = 0; i < conflictingWithRecord.size(); i++) {
+ final int serviceId = conflictingWithRecord.keyAt(i);
+ final int conflictType = conflictingWithRecord.valueAt(i);
+ final int oldConflictType = conflicting.getOrDefault(serviceId, 0);
+ conflicting.put(serviceId, oldConflictType | conflictType);
+ }
}
return conflicting;
}
- private static boolean conflictForService(
+ private static RecordConflictType conflictForService(
@NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
String[] fullServiceName;
if (registration.srvRecord != null) {
@@ -1211,75 +1229,75 @@
} else if (registration.serviceKeyRecord != null) {
fullServiceName = registration.serviceKeyRecord.record.getName();
} else {
- return false;
+ return RecordConflictType.NO_CONFLICT;
}
if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
- return false;
+ return RecordConflictType.NO_CONFLICT;
}
// As per RFC6762 9., it's fine if the "conflict" is an identical record with same
// data.
if (record instanceof MdnsServiceRecord && equals(record, registration.srvRecord)) {
- return false;
+ return RecordConflictType.IDENTICAL;
}
if (record instanceof MdnsTextRecord && equals(record, registration.txtRecord)) {
- return false;
+ return RecordConflictType.IDENTICAL;
}
if (record instanceof MdnsKeyRecord && equals(record, registration.serviceKeyRecord)) {
- return false;
+ return RecordConflictType.IDENTICAL;
}
- return true;
+ return RecordConflictType.CONFLICT;
}
- private boolean conflictForHost(
+ private RecordConflictType conflictForHost(
@NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
// Only custom hosts are checked. When using the default host, the hostname is derived from
// a UUID and it's supposed to be unique.
if (registration.serviceInfo.getHostname() == null) {
- return false;
+ return RecordConflictType.NO_CONFLICT;
}
- // It cannot be a hostname conflict because not record is registered with the hostname.
+ // It cannot be a hostname conflict because no record is registered with the hostname.
if (registration.addressRecords.isEmpty() && registration.hostKeyRecord == null) {
- return false;
+ return RecordConflictType.NO_CONFLICT;
}
// The record's name cannot be registered by NsdManager so it's not a conflict.
if (record.getName().length != 2 || !record.getName()[1].equals(LOCAL_TLD)) {
- return false;
+ return RecordConflictType.NO_CONFLICT;
}
// Different names. There won't be a conflict.
if (!MdnsUtils.equalsIgnoreDnsCase(
record.getName()[0], registration.serviceInfo.getHostname())) {
- return false;
+ return RecordConflictType.NO_CONFLICT;
}
// As per RFC6762 9., it's fine if the "conflict" is an identical record with same
// data.
if (record instanceof MdnsInetAddressRecord
&& hasInetAddressRecord(registration, (MdnsInetAddressRecord) record)) {
- return false;
+ return RecordConflictType.IDENTICAL;
}
if (record instanceof MdnsKeyRecord && equals(record, registration.hostKeyRecord)) {
- return false;
+ return RecordConflictType.IDENTICAL;
}
// Per RFC 6762 8.1, when a record is being probed, any answer containing a record with that
// name, of any type, MUST be considered a conflicting response.
if (registration.isProbing) {
- return true;
+ return RecordConflictType.CONFLICT;
}
if (record instanceof MdnsInetAddressRecord && !registration.addressRecords.isEmpty()) {
- return true;
+ return RecordConflictType.CONFLICT;
}
if (record instanceof MdnsKeyRecord && registration.hostKeyRecord != null) {
- return true;
+ return RecordConflictType.CONFLICT;
}
- return false;
+ return RecordConflictType.NO_CONFLICT;
}
private List<RecordInfo<MdnsInetAddressRecord>> getInetAddressRecordsForHostname(
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index 92f1953..b8689d6 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -108,11 +108,7 @@
PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG);
}
- // This causes thread-unsafe access on mIpConfigurations which might
- // race with calls to EthernetManager#updateConfiguration().
- // EthernetManager#getConfiguration() has been marked as
- // @UnsupportedAppUsage since Android R.
- return mTracker.getIpConfiguration(iface);
+ return new IpConfiguration(mTracker.getIpConfiguration(iface));
}
/**
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index a60592f..71f289e 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -31,6 +31,8 @@
import android.net.ITetheredInterfaceCallback;
import android.net.InterfaceConfigurationParcel;
import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
import android.net.LinkAddress;
import android.net.NetworkCapabilities;
import android.net.StaticIpConfiguration;
@@ -109,7 +111,6 @@
/** Mapping between {iface name | mac address} -> {NetworkCapabilities} */
private final ConcurrentHashMap<String, NetworkCapabilities> mNetworkCapabilities =
new ConcurrentHashMap<>();
- /** Mapping between {iface name | mac address} -> {IpConfiguration} */
private final ConcurrentHashMap<String, IpConfiguration> mIpConfigurations =
new ConcurrentHashMap<>();
@@ -297,7 +298,7 @@
}
private IpConfiguration getIpConfigurationForCallback(String iface, int state) {
- return (state == EthernetManager.STATE_ABSENT) ? null : getIpConfiguration(iface);
+ return (state == EthernetManager.STATE_ABSENT) ? null : getOrCreateIpConfiguration(iface);
}
private void ensureRunningOnEthernetServiceThread() {
@@ -390,83 +391,8 @@
mHandler.post(() -> setInterfaceAdministrativeState(iface, enabled, cb));
}
- private @Nullable String getHwAddress(String iface) {
- if (getInterfaceRole(iface) == EthernetManager.ROLE_SERVER) {
- return mTetheringInterfaceHwAddr;
- }
-
- return mFactory.getHwAddress(iface);
- }
-
- /**
- * Get the IP configuration of the interface, or the default if the interface doesn't exist.
- * @param iface the name of the interface to retrieve.
- *
- * @return The IP configuration
- */
- public IpConfiguration getIpConfiguration(String iface) {
- return getIpConfiguration(iface, getHwAddress(iface));
- }
-
- private IpConfiguration getIpConfiguration(String iface, @Nullable String hwAddress) {
- // Look up Ip configuration first by ifname, then by MAC address.
- IpConfiguration ipConfig = mIpConfigurations.get(iface);
- if (ipConfig != null) {
- return ipConfig;
- }
-
- if (hwAddress == null) {
- // should never happen.
- Log.wtf(TAG, "No hardware address for interface " + iface);
- } else {
- ipConfig = mIpConfigurations.get(hwAddress);
- }
-
- if (ipConfig == null) {
- ipConfig = new IpConfiguration.Builder().build();
- }
-
- return ipConfig;
- }
-
- private NetworkCapabilities getNetworkCapabilities(String iface) {
- return getNetworkCapabilities(iface, getHwAddress(iface));
- }
-
- private NetworkCapabilities getNetworkCapabilities(String iface, @Nullable String hwAddress) {
- // Look up network capabilities first by ifname, then by MAC address.
- NetworkCapabilities networkCapabilities = mNetworkCapabilities.get(iface);
- if (networkCapabilities != null) {
- return networkCapabilities;
- }
-
- if (hwAddress == null) {
- // should never happen.
- Log.wtf(TAG, "No hardware address for interface " + iface);
- } else {
- networkCapabilities = mNetworkCapabilities.get(hwAddress);
- }
-
- if (networkCapabilities != null) {
- return networkCapabilities;
- }
-
- final NetworkCapabilities.Builder builder = createNetworkCapabilities(
- false /* clear default capabilities */, null, null)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
-
- if (isValidTestInterface(iface)) {
- builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
- } else {
- builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
- }
-
- return builder.build();
+ IpConfiguration getIpConfiguration(String iface) {
+ return mIpConfigurations.get(iface);
}
@VisibleForTesting(visibility = PACKAGE)
@@ -507,8 +433,8 @@
* NET_CAPABILITY_NOT_RESTRICTED) capability. Otherwise, returns false.
*/
boolean isRestrictedInterface(String iface) {
- final NetworkCapabilities nc = getNetworkCapabilities(iface);
- return !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+ final NetworkCapabilities nc = mNetworkCapabilities.get(iface);
+ return nc != null && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
}
void addListener(IEthernetServiceListener listener, boolean canUseRestrictedNetworks) {
@@ -697,9 +623,17 @@
return;
}
- final NetworkCapabilities nc = getNetworkCapabilities(iface, hwAddress);
- final IpConfiguration ipConfiguration = getIpConfiguration(iface, hwAddress);
+ NetworkCapabilities nc = mNetworkCapabilities.get(iface);
+ if (nc == null) {
+ // Try to resolve using mac address
+ nc = mNetworkCapabilities.get(hwAddress);
+ if (nc == null) {
+ final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP);
+ nc = createDefaultNetworkCapabilities(isTestIface);
+ }
+ }
+ IpConfiguration ipConfiguration = getOrCreateIpConfiguration(iface);
Log.d(TAG, "Tracking interface in client mode: " + iface);
mFactory.addInterface(iface, hwAddress, ipConfiguration, nc);
@@ -839,6 +773,25 @@
return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4));
}
+ private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) {
+ NetworkCapabilities.Builder builder = createNetworkCapabilities(
+ false /* clear default capabilities */, null, null)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+
+ if (isTestIface) {
+ builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
+ } else {
+ builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ }
+
+ return builder.build();
+ }
+
/**
* Parses a static list of network capabilities
*
@@ -973,6 +926,15 @@
return new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
}
+ private IpConfiguration getOrCreateIpConfiguration(String iface) {
+ IpConfiguration ret = mIpConfigurations.get(iface);
+ if (ret != null) return ret;
+ ret = new IpConfiguration();
+ ret.setIpAssignment(IpAssignment.DHCP);
+ ret.setProxySettings(ProxySettings.NONE);
+ return ret;
+ }
+
private boolean isValidEthernetInterface(String iface) {
return iface.matches(mIfaceMatch) || isValidTestInterface(iface);
}
@@ -1059,7 +1021,7 @@
pw.println("IP Configurations:");
pw.increaseIndent();
for (String iface : mIpConfigurations.keySet()) {
- pw.println(iface + ": " + getIpConfiguration(iface));
+ pw.println(iface + ": " + mIpConfigurations.get(iface));
}
pw.decreaseIndent();
pw.println();
@@ -1067,7 +1029,7 @@
pw.println("Network Capabilities:");
pw.increaseIndent();
for (String iface : mNetworkCapabilities.keySet()) {
- pw.println(iface + ": " + getNetworkCapabilities(iface));
+ pw.println(iface + ": " + mNetworkCapabilities.get(iface));
}
pw.decreaseIndent();
pw.println();
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 04d8ea4..23af0f8 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -32,6 +32,8 @@
import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
import static android.net.BpfNetMapsUtils.isFirewallAllowList;
import static android.net.BpfNetMapsUtils.matchToString;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.INetd.PERMISSION_INTERNET;
@@ -804,6 +806,25 @@
}
/**
+ * Get granted permissions for specified uid. If uid is not in the map, this method returns
+ * {@link android.net.INetd.PERMISSION_INTERNET} since this is a default permission.
+ * See {@link #setNetPermForUids}
+ *
+ * @param uid target uid
+ * @return granted permissions.
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public int getNetPermForUid(final int uid) {
+ try {
+ final U8 permissions = sUidPermissionMap.getValue(new S32(uid));
+ return permissions != null ? permissions.val : PERMISSION_INTERNET;
+ } catch (ErrnoException e) {
+ Log.wtf(TAG, "Failed to get permission for uid: " + uid);
+ return PERMISSION_INTERNET;
+ }
+ }
+
+ /**
* Set Data Saver enabled or disabled
*
* @param enable whether Data Saver is enabled or disabled.
@@ -863,6 +884,49 @@
}
}
+ /**
+ * Get blocked reasons for specified uid
+ *
+ * @param uid Target Uid
+ * @return Reasons of network access blocking for an UID
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public int getUidNetworkingBlockedReasons(final int uid) {
+ return BpfNetMapsUtils.getUidNetworkingBlockedReasons(uid,
+ sConfigurationMap, sUidOwnerMap, sDataSaverEnabledMap);
+ }
+
+ /**
+ * Return whether the network access of specified uid is blocked on metered networks
+ *
+ * @param uid The target uid.
+ * @return True if the network access is blocked on metered networks. Otherwise, false
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public boolean isUidRestrictedOnMeteredNetworks(final int uid) {
+ final int blockedReasons = getUidNetworkingBlockedReasons(uid);
+ return (blockedReasons & BLOCKED_METERED_REASON_MASK) != BLOCKED_REASON_NONE;
+ }
+
+ /*
+ * Return whether the network is blocked by firewall chains for the given uid.
+ *
+ * Note that {@link #getDataSaverEnabled()} has a latency before V.
+ *
+ * @param uid The target uid.
+ * @param isNetworkMetered Whether the target network is metered.
+ *
+ * @return True if the network is blocked. Otherwise, false.
+ * @throws ServiceSpecificException if the read fails.
+ *
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered) {
+ return BpfNetMapsUtils.isUidNetworkingBlocked(uid, isNetworkMetered,
+ sConfigurationMap, sUidOwnerMap, sDataSaverEnabledMap);
+ }
+
/** Register callback for statsd to pull atom. */
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void setPullAtomCallback(final Context context) {
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b99d0de..39ea286 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -64,6 +64,7 @@
import static android.net.ConnectivityManager.getNetworkTypeName;
import static android.net.ConnectivityManager.isNetworkTypeValid;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.INetd.PERMISSION_INTERNET;
import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_SKIPPED;
@@ -101,6 +102,7 @@
import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY;
import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK;
import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION;
+import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
import static android.os.Process.INVALID_UID;
import static android.os.Process.VPN_UID;
import static android.system.OsConstants.ETH_P_ALL;
@@ -2227,6 +2229,11 @@
return nai;
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private boolean hasInternetPermission(final int uid) {
+ return (mBpfNetMaps.getNetPermForUid(uid) & PERMISSION_INTERNET) != 0;
+ }
+
/**
* Check if UID should be blocked from using the specified network.
*/
@@ -2240,7 +2247,17 @@
final long ident = Binder.clearCallingIdentity();
try {
final boolean metered = nc == null ? true : nc.isMetered();
- return mPolicyManager.isUidNetworkingBlocked(uid, metered);
+ if (mDeps.isAtLeastV()) {
+ final boolean hasInternetPermission = hasInternetPermission(uid);
+ final boolean blockedByUidRules = mBpfNetMaps.isUidNetworkingBlocked(uid, metered);
+ if (mDeps.isChangeEnabled(NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION, uid)) {
+ return blockedByUidRules || !hasInternetPermission;
+ } else {
+ return hasInternetPermission && blockedByUidRules;
+ }
+ } else {
+ return mPolicyManager.isUidNetworkingBlocked(uid, metered);
+ }
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -2318,7 +2335,7 @@
final int uid = mDeps.getCallingUid();
final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
if (nai == null) return null;
- final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false);
+ final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false /* ignoreBlocked */);
maybeLogBlockedNetworkInfo(networkInfo, uid);
return networkInfo;
}
@@ -7951,6 +7968,13 @@
// Policy already enforced.
return;
}
+ if (mDeps.isAtLeastV()) {
+ if (mBpfNetMaps.isUidRestrictedOnMeteredNetworks(uid)) {
+ // If UID is restricted, don't allow them to bring up metered APNs.
+ networkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
+ }
+ return;
+ }
final long ident = Binder.clearCallingIdentity();
try {
if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 220a973..50d6e76 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -1943,7 +1943,7 @@
.build();
final CtsNetUtils.TestNetworkCallback callback = new CtsNetUtils.TestNetworkCallback();
mCM.requestNetwork(request, callback);
- final FileDescriptor srcTunFd = runWithShellPermissionIdentity(() -> {
+ final ParcelFileDescriptor srcTunFd = runWithShellPermissionIdentity(() -> {
final TestNetworkManager tnm = mTestContext.getSystemService(TestNetworkManager.class);
List<LinkAddress> linkAddresses = duplicatedAddress
? List.of(new LinkAddress("192.0.2.2/24"),
@@ -1952,7 +1952,7 @@
new LinkAddress("2001:db8:3:4::ffe/64"));
final TestNetworkInterface iface = tnm.createTunInterface(linkAddresses);
tnm.setupTestNetwork(iface.getInterfaceName(), new Binder());
- return iface.getFileDescriptor().getFileDescriptor();
+ return iface.getFileDescriptor();
}, MANAGE_TEST_NETWORKS);
final Network testNetwork = callback.waitForAvailable();
assertNotNull(testNetwork);
@@ -1966,11 +1966,11 @@
false /* isAlwaysMetered */);
final FileDescriptor dstUdpFd = dstSock.getFileDescriptor$();
- checkBlockUdp(srcTunFd, dstUdpFd,
+ checkBlockUdp(srcTunFd.getFileDescriptor(), dstUdpFd,
InetAddresses.parseNumericAddress("192.0.2.2") /* dstAddress */,
InetAddresses.parseNumericAddress("192.0.2.1") /* srcAddress */,
duplicatedAddress ? EXPECT_PASS : EXPECT_BLOCK);
- checkBlockUdp(srcTunFd, dstUdpFd,
+ checkBlockUdp(srcTunFd.getFileDescriptor(), dstUdpFd,
InetAddresses.parseNumericAddress("2001:db8:1:2::ffe") /* dstAddress */,
InetAddresses.parseNumericAddress("2001:db8:1:2::ffa") /* srcAddress */,
duplicatedAddress ? EXPECT_PASS : EXPECT_BLOCK);
@@ -1978,7 +1978,7 @@
// Traffic on VPN should not be affected
checkTrafficOnVpn();
}, /* cleanup */ () -> {
- Os.close(srcTunFd);
+ srcTunFd.close();
dstSock.close();
}, /* cleanup */ () -> {
runWithShellPermissionIdentity(() -> {
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index 5c02b0d..e2fdd3d 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -27,35 +27,32 @@
using namespace android::bpf;
-void doBpfStructSizeTest(const char *elfPath) {
+void doBpfStructSizeTest(const char *elfPath, unsigned mapSz, unsigned progSz) {
std::ifstream elfFile(elfPath, std::ios::in | std::ios::binary);
ASSERT_TRUE(elfFile.is_open());
- if (android::modules::sdklevel::IsAtLeastU()) {
- EXPECT_EQ(120, readSectionUint("size_of_bpf_map_def", elfFile, 0));
- EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
- } else if (android::modules::sdklevel::IsAtLeastT()) {
- EXPECT_EQ(116, readSectionUint("size_of_bpf_map_def", elfFile, 0));
- EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
- } else {
- EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
- EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
- }
-}
-
-TEST(BpfTest, bpfStructSizeTestPreT) {
- if (android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() << "T+ device.";
- doBpfStructSizeTest("/system/etc/bpf/netd.o");
- doBpfStructSizeTest("/system/etc/bpf/clatd.o");
+ EXPECT_EQ(mapSz, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+ EXPECT_EQ(progSz, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
}
TEST(BpfTest, bpfStructSizeTest) {
- if (android::modules::sdklevel::IsAtLeastU()) {
- doBpfStructSizeTest("/system/etc/bpf/gpuMem.o");
- doBpfStructSizeTest("/system/etc/bpf/timeInState.o");
+ if (android::modules::sdklevel::IsAtLeastV()) {
+ // Due to V+ using mainline netbpfload, there is no longer a need to
+ // enforce consistency between platform and mainline bpf .o files.
+ GTEST_SKIP() << "V+ device.";
+ } else if (android::modules::sdklevel::IsAtLeastU()) {
+ doBpfStructSizeTest("/system/etc/bpf/gpuMem.o", 120, 92);
+ doBpfStructSizeTest("/system/etc/bpf/timeInState.o", 120, 92);
+ } else if (android::modules::sdklevel::IsAtLeastT()) {
+ doBpfStructSizeTest("/system/etc/bpf/gpu_mem.o", 116, 92);
+ doBpfStructSizeTest("/system/etc/bpf/time_in_state.o", 116, 92);
+ } else if (android::modules::sdklevel::IsAtLeastS()) {
+ // These files were moved to mainline in Android T
+ doBpfStructSizeTest("/system/etc/bpf/netd.o", 48, 28);
+ doBpfStructSizeTest("/system/etc/bpf/clatd.o", 48, 28);
} else {
- doBpfStructSizeTest("/system/etc/bpf/gpu_mem.o");
- doBpfStructSizeTest("/system/etc/bpf/time_in_state.o");
+ // There is no mainline bpf code before S.
+ GTEST_SKIP() << "R- device.";
}
}
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 933cde4..a679498 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -313,12 +313,14 @@
// - [GMS-VSR-5.3.12-004] MUST indicate at least 1024 bytes of usable memory from calls to
// the getApfPacketFilterCapabilities HAL method.
// TODO: check whether above text should be changed "34 or higher"
- // This should assert apfVersionSupported >= 4 as per the VSR requirements, but there are
- // currently no tests for APFv6 and there cannot be a valid implementation as the
- // interpreter has yet to be finalized.
- assertThat(caps.apfVersionSupported).isEqualTo(4)
+ assertThat(caps.apfVersionSupported).isAtLeast(4)
assertThat(caps.maximumApfProgramSize).isAtLeast(1024)
+ if (caps.apfVersionSupported > 4) {
+ assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
+ assertThat(caps.apfVersionSupported).isEqualTo(6000) // v6.0000
+ }
+
// DEVICEs launching with Android 15 (AOSP experimental) or higher with CHIPSETs that set
// ro.board.first_api_level or ro.board.api_level to 202404 or higher:
// - [GMS-VSR-5.3.12-009] MUST indicate at least 2048 bytes of usable memory from calls to
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 670889f..0dd2a23 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -93,7 +93,7 @@
private static final int SOCKET_TIMEOUT_MS = 10_000;
private static final int PRIVATE_DNS_PROBE_MS = 1_000;
- private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 10_000;
+ private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 30_000;
private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30;
private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index fa79795..cbc060a 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -17,6 +17,7 @@
package com.android.server;
import static android.net.BpfNetMapsConstants.ALLOW_CHAINS;
+import static android.net.BpfNetMapsConstants.BACKGROUND_MATCH;
import static android.net.BpfNetMapsConstants.CURRENT_STATS_MAP_CONFIGURATION_KEY;
import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
import static android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED;
@@ -37,6 +38,14 @@
import static android.net.BpfNetMapsConstants.RESTRICTED_MATCH;
import static android.net.BpfNetMapsConstants.STANDBY_MATCH;
import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_ADMIN_DISABLED;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_OEM_DENY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW;
@@ -839,6 +848,21 @@
}
@Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testGetNetPermFoUid() throws Exception {
+ mUidPermissionMap.deleteEntry(new S32(TEST_UID));
+ assertEquals(PERMISSION_INTERNET, mBpfNetMaps.getNetPermForUid(TEST_UID));
+
+ mUidPermissionMap.updateEntry(new S32(TEST_UID), new U8((short) PERMISSION_NONE));
+ assertEquals(PERMISSION_NONE, mBpfNetMaps.getNetPermForUid(TEST_UID));
+
+ mUidPermissionMap.updateEntry(new S32(TEST_UID),
+ new U8((short) (PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS)));
+ assertEquals(PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS,
+ mBpfNetMaps.getNetPermForUid(TEST_UID));
+ }
+
+ @Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testSwapActiveStatsMap() throws Exception {
mConfigurationMap.updateEntry(
@@ -1150,4 +1174,138 @@
assertDumpContains(dump, TEST_V6_ADDRESS.getHostAddress());
assertDumpContains(dump, TEST_IF_INDEX + "(" + TEST_IF_NAME + ")");
}
+
+ private void doTestGetUidNetworkingBlockedReasons(
+ final long configurationMatches,
+ final long uidRules,
+ final short dataSaverStatus,
+ final int expectedBlockedReasons
+ ) throws Exception {
+ mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(configurationMatches));
+ mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(NULL_IIF, uidRules));
+ mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(dataSaverStatus));
+
+ assertEquals(expectedBlockedReasons, mBpfNetMaps.getUidNetworkingBlockedReasons(TEST_UID));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testGetUidNetworkingBlockedReasons() throws Exception {
+ doTestGetUidNetworkingBlockedReasons(
+ NO_MATCH,
+ NO_MATCH,
+ DATA_SAVER_DISABLED,
+ BLOCKED_REASON_NONE
+ );
+ doTestGetUidNetworkingBlockedReasons(
+ DOZABLE_MATCH,
+ NO_MATCH,
+ DATA_SAVER_DISABLED,
+ BLOCKED_REASON_DOZE
+ );
+ doTestGetUidNetworkingBlockedReasons(
+ DOZABLE_MATCH | POWERSAVE_MATCH | STANDBY_MATCH,
+ DOZABLE_MATCH | STANDBY_MATCH,
+ DATA_SAVER_DISABLED,
+ BLOCKED_REASON_BATTERY_SAVER | BLOCKED_REASON_APP_STANDBY
+ );
+ doTestGetUidNetworkingBlockedReasons(
+ OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH,
+ OEM_DENY_1_MATCH | OEM_DENY_3_MATCH,
+ DATA_SAVER_DISABLED,
+ BLOCKED_REASON_OEM_DENY
+ );
+ doTestGetUidNetworkingBlockedReasons(
+ DOZABLE_MATCH,
+ DOZABLE_MATCH | BACKGROUND_MATCH | STANDBY_MATCH,
+ DATA_SAVER_DISABLED,
+ BLOCKED_REASON_NONE
+ );
+
+ // Note that HAPPY_BOX and PENALTY_BOX are not disabled by configuration map
+ doTestGetUidNetworkingBlockedReasons(
+ NO_MATCH,
+ PENALTY_BOX_USER_MATCH,
+ DATA_SAVER_DISABLED,
+ BLOCKED_METERED_REASON_USER_RESTRICTED
+ );
+ doTestGetUidNetworkingBlockedReasons(
+ NO_MATCH,
+ PENALTY_BOX_ADMIN_MATCH,
+ DATA_SAVER_ENABLED,
+ BLOCKED_METERED_REASON_ADMIN_DISABLED | BLOCKED_METERED_REASON_DATA_SAVER
+ );
+ doTestGetUidNetworkingBlockedReasons(
+ NO_MATCH,
+ PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH | HAPPY_BOX_MATCH,
+ DATA_SAVER_ENABLED,
+ BLOCKED_METERED_REASON_USER_RESTRICTED | BLOCKED_METERED_REASON_ADMIN_DISABLED
+ );
+ doTestGetUidNetworkingBlockedReasons(
+ STANDBY_MATCH,
+ STANDBY_MATCH | PENALTY_BOX_USER_MATCH | HAPPY_BOX_MATCH,
+ DATA_SAVER_ENABLED,
+ BLOCKED_REASON_APP_STANDBY | BLOCKED_METERED_REASON_USER_RESTRICTED
+ );
+ }
+
+ private void doTestIsUidRestrictedOnMeteredNetworks(
+ final long enabledMatches,
+ final long uidRules,
+ final short dataSaver,
+ final boolean expectedRestricted
+ ) throws Exception {
+ mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(enabledMatches));
+ mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(NULL_IIF, uidRules));
+ mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(dataSaver));
+
+ assertEquals(expectedRestricted, mBpfNetMaps.isUidRestrictedOnMeteredNetworks(TEST_UID));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
+ doTestIsUidRestrictedOnMeteredNetworks(
+ NO_MATCH,
+ NO_MATCH,
+ DATA_SAVER_DISABLED,
+ false /* expectRestricted */
+ );
+ doTestIsUidRestrictedOnMeteredNetworks(
+ DOZABLE_MATCH | POWERSAVE_MATCH | STANDBY_MATCH,
+ DOZABLE_MATCH | STANDBY_MATCH ,
+ DATA_SAVER_DISABLED,
+ false /* expectRestricted */
+ );
+ doTestIsUidRestrictedOnMeteredNetworks(
+ NO_MATCH,
+ PENALTY_BOX_USER_MATCH,
+ DATA_SAVER_DISABLED,
+ true /* expectRestricted */
+ );
+ doTestIsUidRestrictedOnMeteredNetworks(
+ NO_MATCH,
+ PENALTY_BOX_ADMIN_MATCH,
+ DATA_SAVER_DISABLED,
+ true /* expectRestricted */
+ );
+ doTestIsUidRestrictedOnMeteredNetworks(
+ NO_MATCH,
+ PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH | HAPPY_BOX_MATCH,
+ DATA_SAVER_DISABLED,
+ true /* expectRestricted */
+ );
+ doTestIsUidRestrictedOnMeteredNetworks(
+ NO_MATCH,
+ NO_MATCH,
+ DATA_SAVER_ENABLED,
+ true /* expectRestricted */
+ );
+ doTestIsUidRestrictedOnMeteredNetworks(
+ NO_MATCH,
+ HAPPY_BOX_MATCH,
+ DATA_SAVER_ENABLED,
+ false /* expectRestricted */
+ );
+ }
}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 9f13d79..b6cb09b 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -87,6 +87,7 @@
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+import static android.net.INetd.PERMISSION_INTERNET;
import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS;
import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_FALLBACK;
import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP;
@@ -151,6 +152,7 @@
import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED;
import static android.net.Proxy.PROXY_CHANGE_ACTION;
import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_ADDED;
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_REMOVED;
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE;
@@ -1727,6 +1729,8 @@
private void mockUidNetworkingBlocked() {
doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
).when(mNetworkPolicyManager).isUidNetworkingBlocked(anyInt(), anyBoolean());
+ doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
+ ).when(mBpfNetMaps).isUidNetworkingBlocked(anyInt(), anyBoolean());
}
private boolean isUidBlocked(int blockedReasons, boolean meteredNetwork) {
@@ -1943,6 +1947,10 @@
setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT);
setAlwaysOnNetworks(false);
setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com");
+
+ mDeps.setChangeIdEnabled(
+ true, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION, Process.myUid());
+ doReturn(PERMISSION_INTERNET).when(mBpfNetMaps).getNetPermForUid(anyInt());
// Note : Please do not add any new instrumentation here. If you need new instrumentation,
// please add it in CSTest and use subclasses of CSTest instead of adding more
// tools in ConnectivityServiceTest.
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index d735dc6..2cb97c9 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -1625,6 +1625,44 @@
}
@Test
+ fun testGetConflictingServices_multipleRegistrationsForHostKey_noConflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+ hostname = "MyHost"
+ hostAddresses = listOf(
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::2"))
+ publicKey = TEST_PUBLIC_KEY
+ })
+ repository.addService(TEST_SERVICE_ID_2, NsdServiceInfo().apply {
+ serviceType = "_testservice._tcp"
+ serviceName = "MyTestService"
+ port = TEST_PORT
+ hostname = "MyHost"
+ publicKey = TEST_PUBLIC_KEY
+ }, null /* ttl */)
+
+ // Although there's a KEY RR in the second registration being probed, it shouldn't conflict
+ // with an address record which is from a probed registration in the repository.
+ val otherTtlMillis = 1234L
+ val packet = MdnsPacket(
+ 0 /* flags */,
+ emptyList() /* questions */,
+ listOf(
+ MdnsInetAddressRecord(
+ arrayOf("MyHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ otherTtlMillis,
+ parseNumericAddress("2001:db8::1"))
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(mapOf(), repository.getConflictingServices(packet))
+ }
+
+ @Test
fun testGetConflictingServices_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSActiveNetworkInfoTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSActiveNetworkInfoTest.kt
new file mode 100644
index 0000000..360a298
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSActiveNetworkInfoTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 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
+
+import android.net.INetd.PERMISSION_INTERNET
+import android.net.INetd.PERMISSION_NONE
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkInfo.DetailedState.BLOCKED
+import android.net.NetworkInfo.DetailedState.CONNECTED
+import android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+
+private fun nc() = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CSActiveNetworkInfoTest : CSTest() {
+
+ fun doTestGetActiveNetworkInfo(
+ changeEnabled: Boolean,
+ permissions: Int,
+ expectBlocked: Boolean
+ ) {
+ deps.setChangeIdEnabled(changeEnabled, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION)
+ doReturn(permissions).`when`(bpfNetMaps).getNetPermForUid(anyInt())
+
+ val agent = Agent(nc = nc())
+ agent.connect()
+
+ val networkInfo = cm.activeNetworkInfo
+ assertNotNull(networkInfo)
+ if (expectBlocked) {
+ assertEquals(BLOCKED, networkInfo.detailedState)
+ } else {
+ assertEquals(CONNECTED, networkInfo.detailedState)
+ }
+ assertEquals(expectBlocked, cm.activeNetwork == null)
+ agent.disconnect()
+ }
+
+ @Test
+ fun testGetActiveNetworkInfo() {
+ doReturn(true).`when`(bpfNetMaps).isUidNetworkingBlocked(anyInt(), anyBoolean())
+ doTestGetActiveNetworkInfo(
+ changeEnabled = true,
+ permissions = PERMISSION_NONE,
+ expectBlocked = true
+ )
+ doTestGetActiveNetworkInfo(
+ changeEnabled = false,
+ permissions = PERMISSION_INTERNET,
+ expectBlocked = true
+ )
+ // Network access is considered not blocked if the compat change is disabled and an app
+ // does not have PERMISSION_INTERNET
+ doTestGetActiveNetworkInfo(
+ changeEnabled = false,
+ permissions = PERMISSION_NONE,
+ expectBlocked = false
+ )
+ }
+}
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index ebbb9af..34d67bb 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -11,5 +11,8 @@
}
],
"postsubmit": [
+ {
+ "name": "ThreadNetworkMultiDeviceTests"
+ }
]
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 0559499..0c200fd 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -106,6 +106,7 @@
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserManager;
+import android.provider.Settings;
import android.util.Log;
import android.util.SparseArray;
@@ -121,6 +122,7 @@
import com.android.server.thread.openthread.IOtStatusReceiver;
import com.android.server.thread.openthread.Ipv6AddressInfo;
import com.android.server.thread.openthread.MeshcopTxtAttributes;
+import com.android.server.thread.openthread.OnMeshPrefixConfig;
import com.android.server.thread.openthread.OtDaemonState;
import libcore.util.HexEncoding;
@@ -199,6 +201,7 @@
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
private boolean mUserRestricted;
+ private boolean mAirplaneModeOn;
private boolean mForceStopOtDaemonEnabled;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -282,7 +285,7 @@
}
private void maybeInitializeOtDaemon() {
- if (!isEnabled()) {
+ if (!shouldEnableThread()) {
return;
}
@@ -317,7 +320,7 @@
otDaemon.initialize(
mTunIfController.getTunFd(),
- isEnabled(),
+ shouldEnableThread(),
mNsdPublisher,
getMeshcopTxtAttributes(mResources.get()),
mOtDaemonCallbackProxy,
@@ -382,7 +385,7 @@
Log.d(
TAG,
"Initializing Thread system service: Thread is "
- + (isEnabled() ? "enabled" : "disabled"));
+ + (shouldEnableThread() ? "enabled" : "disabled"));
try {
mTunIfController.createTunInterface();
} catch (IOException e) {
@@ -394,6 +397,8 @@
requestThreadNetwork();
mUserRestricted = isThreadUserRestricted();
registerUserRestrictionsReceiver();
+ mAirplaneModeOn = isAirplaneModeOn();
+ registerAirplaneModeReceiver();
maybeInitializeOtDaemon();
});
}
@@ -474,6 +479,15 @@
// the otDaemon set enabled state operation succeeded or not, so that it can recover
// to the desired value after reboot.
mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
+
+ // Remember whether the user wanted to keep Thread enabled in airplane mode. If once
+ // the user disabled Thread again in airplane mode, the persistent settings state is
+ // reset (so that Thread will be auto-disabled again when airplane mode is turned on).
+ // This behavior is consistent with Wi-Fi and bluetooth.
+ if (mAirplaneModeOn) {
+ mPersistentSettings.put(
+ ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE.key, isEnabled);
+ }
}
try {
@@ -510,36 +524,30 @@
+ newUserRestrictedState);
mUserRestricted = newUserRestrictedState;
- final boolean isEnabled = isEnabled();
+ final boolean shouldEnableThread = shouldEnableThread();
final IOperationReceiver receiver =
new IOperationReceiver.Stub() {
@Override
public void onSuccess() {
Log.d(
TAG,
- (isEnabled ? "Enabled" : "Disabled")
+ (shouldEnableThread ? "Enabled" : "Disabled")
+ " Thread due to user restriction change");
}
@Override
- public void onError(int otError, String messages) {
+ public void onError(int errorCode, String errorMessage) {
Log.e(
TAG,
"Failed to "
- + (isEnabled ? "enable" : "disable")
+ + (shouldEnableThread ? "enable" : "disable")
+ " Thread for user restriction change");
}
};
// Do not save the user restriction state to persistent settings so that the user
// configuration won't be overwritten
- setEnabledInternal(isEnabled, false /* persist */, new OperationReceiverWrapper(receiver));
- }
-
- /** Returns {@code true} if Thread is set enabled. */
- private boolean isEnabled() {
- return !mForceStopOtDaemonEnabled
- && !mUserRestricted
- && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+ setEnabledInternal(
+ shouldEnableThread, false /* persist */, new OperationReceiverWrapper(receiver));
}
/** Returns {@code true} if Thread has been restricted for the user. */
@@ -547,6 +555,74 @@
return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
}
+ private void registerAirplaneModeReceiver() {
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ onAirplaneModeChanged(isAirplaneModeOn());
+ }
+ },
+ new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED),
+ null /* broadcastPermission */,
+ mHandler);
+ }
+
+ private void onAirplaneModeChanged(boolean newAirplaneModeOn) {
+ checkOnHandlerThread();
+ if (mAirplaneModeOn == newAirplaneModeOn) {
+ return;
+ }
+ Log.i(TAG, "Airplane mode changed: " + mAirplaneModeOn + " -> " + newAirplaneModeOn);
+ mAirplaneModeOn = newAirplaneModeOn;
+
+ final boolean shouldEnableThread = shouldEnableThread();
+ final IOperationReceiver receiver =
+ new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ Log.d(
+ TAG,
+ (shouldEnableThread ? "Enabled" : "Disabled")
+ + " Thread due to airplane mode change");
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ Log.e(
+ TAG,
+ "Failed to "
+ + (shouldEnableThread ? "enable" : "disable")
+ + " Thread for airplane mode change");
+ }
+ };
+ // Do not save the user restriction state to persistent settings so that the user
+ // configuration won't be overwritten
+ setEnabledInternal(
+ shouldEnableThread, false /* persist */, new OperationReceiverWrapper(receiver));
+ }
+
+ /** Returns {@code true} if Airplane mode has been turned on. */
+ private boolean isAirplaneModeOn() {
+ return Settings.Global.getInt(
+ mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0)
+ == 1;
+ }
+
+ /**
+ * Returns {@code true} if Thread should be enabled based on current settings, runtime user
+ * restriction and airplane mode state.
+ */
+ private boolean shouldEnableThread() {
+ final boolean enabledInAirplaneMode =
+ mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE);
+
+ return !mForceStopOtDaemonEnabled
+ && !mUserRestricted
+ && (!mAirplaneModeOn || enabledInAirplaneMode)
+ && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+ }
+
private void requestUpstreamNetwork() {
if (mUpstreamNetworkCallback != null) {
throw new AssertionError("The upstream network request is already there.");
@@ -1018,7 +1094,7 @@
checkOnHandlerThread();
// Fails early to avoid waking up ot-daemon by the ThreadNetworkCountryCode class
- if (!isEnabled()) {
+ if (!shouldEnableThread()) {
receiver.onError(
ERROR_THREAD_DISABLED, "Can't set country code when Thread is disabled");
return;
@@ -1159,6 +1235,18 @@
}
}
+ private void handlePrefixChanged(List<OnMeshPrefixConfig> onMeshPrefixConfigList) {
+ checkOnHandlerThread();
+
+ mTunIfController.updatePrefixes(onMeshPrefixConfigList);
+
+ // The OT daemon can send link property updates before the networkAgent is
+ // registered
+ if (mNetworkAgent != null) {
+ mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
+ }
+ }
+
private void sendLocalNetworkConfig() {
if (mNetworkAgent == null) {
return;
@@ -1283,7 +1371,6 @@
private void notifyThreadEnabledUpdated(IStateCallback callback, int enabledState) {
try {
callback.onThreadEnableStateChanged(enabledState);
- Log.i(TAG, "onThreadEnableStateChanged " + enabledState);
} catch (RemoteException ignored) {
// do nothing if the client is dead
}
@@ -1507,5 +1594,10 @@
public void onBackboneRouterStateChanged(BackboneRouterState state) {
mHandler.post(() -> handleMulticastForwardingChanged(state));
}
+
+ @Override
+ public void onPrefixChanged(List<OnMeshPrefixConfig> onMeshPrefixConfigList) {
+ mHandler.post(() -> handlePrefixChanged(onMeshPrefixConfigList));
+ }
}
}
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index 8aaff60..f18aac9 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -63,6 +63,14 @@
/** Stores the Thread feature toggle state, true for enabled and false for disabled. */
public static final Key<Boolean> THREAD_ENABLED = new Key<>("thread_enabled", true);
+ /**
+ * Indicates that Thread was enabled (i.e. via the setEnabled() API) when the airplane mode is
+ * turned on in settings. When this value is {@code true}, the current airplane mode state will
+ * be ignored when evaluating the Thread enabled state.
+ */
+ public static final Key<Boolean> THREAD_ENABLED_IN_AIRPLANE_MODE =
+ new Key<>("thread_enabled_in_airplane_mode", false);
+
/** Stores the Thread country code, null if no country code is stored. */
public static final Key<String> THREAD_COUNTRY_CODE = new Key<>("thread_country_code", null);
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index dec72b2..c3f6ace 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -35,6 +35,7 @@
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
import com.android.server.thread.openthread.Ipv6AddressInfo;
+import com.android.server.thread.openthread.OnMeshPrefixConfig;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -66,7 +67,8 @@
private static int sNetlinkSeqNo = 0;
private final MulticastSocket mMulticastSocket; // For join group and leave group
private NetworkInterface mNetworkInterface;
- private List<InetAddress> mMulticastAddresses = new ArrayList<>();
+ private final List<InetAddress> mMulticastAddresses = new ArrayList<>();
+ private final List<RouteInfo> mNetDataPrefixes = new ArrayList<>();
/** Creates a new {@link TunInterfaceController} instance for given interface. */
public TunInterfaceController(String interfaceName) {
@@ -243,13 +245,40 @@
mMulticastAddresses.addAll(newMulticastAddresses);
}
+ public void updatePrefixes(List<OnMeshPrefixConfig> onMeshPrefixConfigList) {
+ final List<RouteInfo> newNetDataPrefixes = new ArrayList<>();
+
+ for (OnMeshPrefixConfig onMeshPrefixConfig : onMeshPrefixConfigList) {
+ newNetDataPrefixes.add(getRouteForOnMeshPrefix(onMeshPrefixConfig));
+ }
+
+ final CompareResult<RouteInfo> prefixDiff =
+ new CompareResult<>(mNetDataPrefixes, newNetDataPrefixes);
+ for (RouteInfo routeRemoved : prefixDiff.removed) {
+ mLinkProperties.removeRoute(routeRemoved);
+ }
+ for (RouteInfo routeAdded : prefixDiff.added) {
+ mLinkProperties.addRoute(routeAdded);
+ }
+
+ mNetDataPrefixes.clear();
+ mNetDataPrefixes.addAll(newNetDataPrefixes);
+ }
+
private RouteInfo getRouteForAddress(LinkAddress linkAddress) {
- return new RouteInfo(
- new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()),
- null,
- mIfName,
- RouteInfo.RTN_UNICAST,
- MTU);
+ return getRouteForIpPrefix(
+ new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()));
+ }
+
+ private RouteInfo getRouteForOnMeshPrefix(OnMeshPrefixConfig onMeshPrefixConfig) {
+ return getRouteForIpPrefix(
+ new IpPrefix(
+ bytesToInet6Address(onMeshPrefixConfig.prefix),
+ onMeshPrefixConfig.prefixLength));
+ }
+
+ private RouteInfo getRouteForIpPrefix(IpPrefix ipPrefix) {
+ return new RouteInfo(ipPrefix, null, mIfName, RouteInfo.RTN_UNICAST, MTU);
}
/** Called by {@link ThreadNetworkControllerService} to do clean up when ot-daemon is dead. */
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 998e70d..4028014 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -22,6 +22,8 @@
import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
+import static android.net.thread.utils.IntegrationTestUtils.getPrefixesFromNetData;
+import static android.net.thread.utils.IntegrationTestUtils.getThreadNetwork;
import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
@@ -34,9 +36,11 @@
import static com.google.common.truth.Truth.assertWithMessage;
import android.content.Context;
+import android.net.ConnectivityManager;
import android.net.InetAddresses;
import android.net.IpPrefix;
import android.net.LinkAddress;
+import android.net.LinkProperties;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule;
@@ -76,6 +80,9 @@
// The maximum time for OT addresses to be propagated to the TUN interface "thread-wpan"
private static final Duration TUN_ADDR_UPDATE_TIMEOUT = Duration.ofSeconds(1);
+ // The maximum time for changes to be propagated to netdata.
+ private static final Duration NET_DATA_UPDATE_TIMEOUT = Duration.ofSeconds(1);
+
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
base16().decode(
@@ -90,6 +97,10 @@
private static final Inet6Address GROUP_ADDR_ALL_ROUTERS =
(Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
+ private static final String TEST_NO_SLAAC_PREFIX = "9101:dead:beef:cafe::/64";
+ private static final InetAddress TEST_NO_SLAAC_PREFIX_ADDRESS =
+ InetAddresses.parseNumericAddress("9101:dead:beef:cafe::");
+
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
private ExecutorService mExecutor;
@@ -253,6 +264,49 @@
}
}
+ @Test
+ public void addPrefixToNetData_routeIsAddedToTunInterface() throws Exception {
+ ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ // Ftd child doesn't have the ability to add a prefix, so let BR itself add a prefix.
+ mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
+ mOtCtl.executeCommand("netdata register");
+ waitFor(
+ () -> {
+ String netData = mOtCtl.executeCommand("netdata show");
+ return getPrefixesFromNetData(netData).contains(TEST_NO_SLAAC_PREFIX);
+ },
+ NET_DATA_UPDATE_TIMEOUT);
+
+ LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
+ assertThat(lp).isNotNull();
+ assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
+ .isTrue();
+ }
+
+ @Test
+ public void removePrefixFromNetData_routeIsRemovedFromTunInterface() throws Exception {
+ ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ mController.joinAndWait(DEFAULT_DATASET);
+ mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
+ mOtCtl.executeCommand("netdata register");
+
+ mOtCtl.executeCommand("prefix remove " + TEST_NO_SLAAC_PREFIX);
+ mOtCtl.executeCommand("netdata register");
+ waitFor(
+ () -> {
+ String netData = mOtCtl.executeCommand("netdata show");
+ return !getPrefixesFromNetData(netData).contains(TEST_NO_SLAAC_PREFIX);
+ },
+ NET_DATA_UPDATE_TIMEOUT);
+
+ LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
+ assertThat(lp).isNotNull();
+ assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
+ .isFalse();
+ }
+
// TODO (b/323300829): add more tests for integration with linux platform and
// ConnectivityService
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 4c882d4..46cf562 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -57,7 +57,9 @@
private static final int PING_SIZE = 100;
// There may not be a response for the ping command, using a short timeout to keep the tests
// short.
- private static final float PING_TIMEOUT_SECONDS = 0.1f;
+ private static final float PING_TIMEOUT_0_1_SECOND = 0.1f;
+ // 1 second timeout should be used when response is expected.
+ private static final float PING_TIMEOUT_1_SECOND = 1f;
private final Process mProcess;
private final BufferedReader mReader;
@@ -409,7 +411,7 @@
1 /* count */,
PING_INTERVAL,
HOP_LIMIT,
- PING_TIMEOUT_SECONDS);
+ PING_TIMEOUT_0_1_SECOND);
}
public void ping(Inet6Address address) {
@@ -420,7 +422,7 @@
1 /* count */,
PING_INTERVAL,
HOP_LIMIT,
- PING_TIMEOUT_SECONDS);
+ PING_TIMEOUT_0_1_SECOND);
}
/** Returns the number of ping reply packets received. */
@@ -433,7 +435,7 @@
count,
PING_INTERVAL,
HOP_LIMIT,
- PING_TIMEOUT_SECONDS);
+ PING_TIMEOUT_1_SECOND);
return getReceivedPacketsCount(output);
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
index 9be9566..78f5770 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -15,6 +15,7 @@
*/
package android.net.thread.utils;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.system.OsConstants.IPPROTO_ICMPV6;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
@@ -24,17 +25,24 @@
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import android.net.ConnectivityManager;
import android.net.InetAddresses;
import android.net.LinkAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
import android.net.TestNetworkInterface;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.net.thread.ThreadNetworkController;
+import android.os.Build;
import android.os.Handler;
import android.os.SystemClock;
import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
import com.android.net.module.util.Struct;
import com.android.net.module.util.structs.Icmpv6Header;
@@ -375,6 +383,36 @@
}
}
+ public static String getPrefixesFromNetData(String netData) {
+ int startIdx = netData.indexOf("Prefixes:");
+ int endIdx = netData.indexOf("Routes:");
+ return netData.substring(startIdx, endIdx);
+ }
+
+ public static Network getThreadNetwork(Duration timeout) throws Exception {
+ CompletableFuture<Network> networkFuture = new CompletableFuture<>();
+ ConnectivityManager cm =
+ ApplicationProvider.getApplicationContext()
+ .getSystemService(ConnectivityManager.class);
+ NetworkRequest.Builder networkRequestBuilder =
+ new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_THREAD);
+ // Before V, we need to explicitly set `NET_CAPABILITY_LOCAL_NETWORK` capability to request
+ // a Thread network.
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ networkRequestBuilder.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ }
+ NetworkRequest networkRequest = networkRequestBuilder.build();
+ ConnectivityManager.NetworkCallback networkCallback =
+ new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ networkFuture.complete(network);
+ }
+ };
+ cm.registerNetworkCallback(networkRequest, networkCallback);
+ return networkFuture.get(timeout.toSeconds(), SECONDS);
+ }
+
private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
diff --git a/thread/tests/multidevices/Android.bp b/thread/tests/multidevices/Android.bp
new file mode 100644
index 0000000..9cd4d9c
--- /dev/null
+++ b/thread/tests/multidevices/Android.bp
@@ -0,0 +1,43 @@
+//
+// Copyright (C) 2024 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_team: "trendy_team_fwk_thread_network",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+python_test_host {
+ name: "ThreadNetworkMultiDeviceTestCases",
+ main: "thread_network_multi_device_test.py",
+ srcs: ["thread_network_multi_device_test.py"],
+ test_config: "AndroidTest.xml",
+ libs: [
+ "mobly",
+ ],
+ test_options: {
+ unit_test: false,
+ tags: ["mobly"],
+ },
+ test_suites: [
+ "mts-tethering",
+ "general-tests",
+ ],
+ version: {
+ py3: {
+ embedded_launcher: true,
+ },
+ },
+}
diff --git a/thread/tests/multidevices/AndroidTest.xml b/thread/tests/multidevices/AndroidTest.xml
new file mode 100644
index 0000000..a2ea9aa
--- /dev/null
+++ b/thread/tests/multidevices/AndroidTest.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 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 Multi-device test cases">
+ <option name="config-descriptor:metadata" key="component" value="threadnetwork" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.tethering.apex" />
+
+ <object class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController"
+ type="module_controller">
+ <option name="required-feature" value="android.hardware.thread_network" />
+ </object>
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+ <!--
+ Only run tests if the device under test is SDK version 34 (Android 14) or above.
+ -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+
+ <device name="device1">
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+ </device>
+ <device name="device2">
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+ </device>
+
+ <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest">
+ <!-- The mobly-par-file-name should match the module name -->
+ <option name="mobly-par-file-name" value="ThreadMultiDeviceTestCases" />
+ <!-- Timeout limit in milliseconds for all test cases of the python binary -->
+ <option name="mobly-test-timeout" value="180000" />
+ </test>
+</configuration>
diff --git a/thread/tests/multidevices/thread_network_multi_device_test.py b/thread/tests/multidevices/thread_network_multi_device_test.py
new file mode 100644
index 0000000..652576b
--- /dev/null
+++ b/thread/tests/multidevices/thread_network_multi_device_test.py
@@ -0,0 +1,67 @@
+# Copyright (C) 2024 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.
+
+# Lint as: python3
+
+import logging
+import time
+
+from mobly import asserts
+from mobly import base_test
+from mobly import test_runner
+from mobly.controllers import android_device
+
+class ThreadNetworkMultiDeviceTest(base_test.BaseTestClass):
+ def setup_class(self):
+ self.node_a, self.node_b = self.register_controller(
+ android_device, min_number=2)
+ self.node_a.adb.shell([
+ 'ot-ctl', 'factoryreset',
+ ])
+ self.node_b.adb.shell([
+ 'ot-ctl', 'factoryreset',
+ ])
+ time.sleep(1)
+
+ def ot_ctl(self, node, cmd, expect_done=True):
+ args = cmd.split(' ')
+ args = ['ot-ctl'] + args
+ stdout = node.adb.shell(args).decode('utf-8')
+ if expect_done:
+ asserts.assert_in('Done', stdout)
+ return stdout
+
+ def test_b_should_be_able_to_discover_a(self):
+ self.ot_ctl(self.node_a, 'dataset init new')
+ self.ot_ctl(self.node_a, 'dataset commit active')
+ self.ot_ctl(self.node_a, 'ifconfig up')
+ self.ot_ctl(self.node_a, 'thread start')
+ self.ot_ctl(self.node_a, 'state leader')
+ stdout = self.ot_ctl(self.node_a, 'extaddr')
+ extaddr = stdout.splitlines()[0]
+ logging.info('node a extaddr: %s', extaddr)
+ asserts.assert_equal(len(extaddr), 16)
+
+ stdout = self.ot_ctl(self.node_b, 'scan')
+ asserts.assert_in(extaddr, stdout)
+ logging.info('discovered node a')
+
+
+if __name__ == '__main__':
+ # Take test args
+ if '--' in sys.argv:
+ index = sys.argv.index('--')
+ sys.argv = sys.argv[:1] + sys.argv[index + 1:]
+
+ test_runner.main()
diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml
index ace7c52..8442e80 100644
--- a/thread/tests/unit/AndroidManifest.xml
+++ b/thread/tests/unit/AndroidManifest.xml
@@ -19,6 +19,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="android.net.thread.unittests">
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
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 493058f..52a9dd9 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -26,6 +26,7 @@
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.google.common.io.BaseEncoding.base16;
@@ -35,6 +36,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
@@ -64,6 +66,8 @@
import android.os.RemoteException;
import android.os.UserManager;
import android.os.test.TestLooper;
+import android.provider.Settings;
+import android.util.AtomicFile;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -75,7 +79,9 @@
import com.android.server.thread.openthread.testing.FakeOtDaemon;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
@@ -133,7 +139,6 @@
@Mock private TunInterfaceController mMockTunIfController;
@Mock private ParcelFileDescriptor mMockTunFd;
@Mock private InfraInterfaceController mMockInfraIfController;
- @Mock private ThreadPersistentSettings mMockPersistentSettings;
@Mock private NsdPublisher mMockNsdPublisher;
@Mock private UserManager mMockUserManager;
@Mock private IBinder mIBinder;
@@ -143,11 +148,15 @@
private Context mContext;
private TestLooper mTestLooper;
private FakeOtDaemon mFakeOtDaemon;
+ private ThreadPersistentSettings mPersistentSettings;
private ThreadNetworkControllerService mService;
@Captor private ArgumentCaptor<ActiveOperationalDataset> mActiveDatasetCaptor;
+ @Rule(order = 1)
+ public final TemporaryFolder tempFolder = new TemporaryFolder();
+
@Before
- public void setUp() {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = spy(ApplicationProvider.getApplicationContext());
@@ -164,10 +173,12 @@
mFakeOtDaemon = new FakeOtDaemon(handler);
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
- when(mMockPersistentSettings.get(any())).thenReturn(true);
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+
when(mConnectivityResources.get()).thenReturn(mResources);
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -175,6 +186,10 @@
when(mResources.getString(eq(R.string.config_thread_model_name)))
.thenReturn(TEST_MODEL_NAME);
+ final AtomicFile storageFile = new AtomicFile(tempFolder.newFile("thread_settings.xml"));
+ mPersistentSettings = new ThreadPersistentSettings(storageFile, mConnectivityResources);
+ mPersistentSettings.initialize();
+
mService =
new ThreadNetworkControllerService(
mContext,
@@ -184,7 +199,7 @@
mMockConnectivityManager,
mMockTunIfController,
mMockInfraIfController,
- mMockPersistentSettings,
+ mPersistentSettings,
mMockNsdPublisher,
mMockUserManager,
mConnectivityResources,
@@ -343,15 +358,9 @@
@Test
public void userRestriction_userBecomesRestricted_stateIsDisabledButNotPersisted() {
- AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
- doAnswer(
- invocation -> {
- receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
- return null;
- })
- .when(mContext)
- .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
mService.initialize();
mTestLooper.dispatchAll();
@@ -360,21 +369,14 @@
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
- verify(mMockPersistentSettings, never())
- .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(false));
+ assertThat(mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED)).isTrue();
}
@Test
- public void userRestriction_userBecomesNotRestricted_stateIsEnabledButNotPersisted() {
- AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+ public void userRestriction_userBecomesNotRestricted_stateIsEnabled() {
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
- doAnswer(
- invocation -> {
- receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
- return null;
- })
- .when(mContext)
- .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
mService.initialize();
mTestLooper.dispatchAll();
@@ -383,8 +385,6 @@
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
- verify(mMockPersistentSettings, never())
- .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
}
@Test
@@ -401,6 +401,118 @@
assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
}
+ @Test
+ public void airplaneMode_initWithAirplaneModeOn_otDaemonNotStarted() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.isInitialized()).isFalse();
+ }
+
+ @Test
+ public void airplaneMode_initWithAirplaneModeOff_threadIsEnabled() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void airplaneMode_changesFromOffToOn_stateIsDisabled() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ }
+
+ @Test
+ public void airplaneMode_changesFromOnToOff_stateIsEnabled() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void airplaneMode_setEnabledWhenAirplaneModeIsOn_WillNotAutoDisableSecondTime() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+ mService.initialize();
+
+ mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isTrue();
+ }
+
+ @Test
+ public void airplaneMode_setDisabledWhenAirplaneModeIsOn_WillAutoDisableSecondTime() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+ mService.initialize();
+ mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+
+ mService.setEnabled(false, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isFalse();
+ }
+
+ private AtomicReference<BroadcastReceiver> captureBroadcastReceiver(String action) {
+ AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+
+ doAnswer(
+ invocation -> {
+ receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+ return null;
+ })
+ .when(mContext)
+ .registerReceiver(
+ any(BroadcastReceiver.class),
+ argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)),
+ any(),
+ any());
+
+ return receiverRef;
+ }
+
private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
return new IOperationReceiver.Stub() {
@Override
diff --git a/tools/aospify_device.sh b/tools/aospify_device.sh
new file mode 100755
index 0000000..f25ac9d
--- /dev/null
+++ b/tools/aospify_device.sh
@@ -0,0 +1,164 @@
+#!/bin/bash
+
+# Script to swap core networking modules in a GMS userdebug device to AOSP modules, by remounting
+# the system partition and replacing module prebuilts. This is only to be used for local testing,
+# and should only be used on userdebug devices that support "adb root" and remounting the system
+# partition using overlayfs.
+#
+# Usage: aospify_device.sh [device_serial]
+# Reset by wiping data (adb reboot bootloader && fastboot erase userdata && fastboot reboot).
+#
+# This applies to NetworkStack, CaptivePortalLogin, dnsresolver, tethering, cellbroadcast modules,
+# which generally need to be preloaded together (core networking modules + cellbroadcast which
+# shares its certificates with NetworkStack and CaptivePortalLogin)
+#
+# This allows device manufacturers to test their changes in AOSP modules, running them on their
+# own device builds, before contributing contributing the patches to AOSP. After running this script
+# once AOSP modules can be quickly built and updated on the prepared device with:
+# m NetworkStack
+# adb install --staged $ANDROID_PRODUCT_OUT/system/priv-app/NetworkStack/NetworkStack.apk \
+# adb reboot
+# or for APEX modules:
+# m com.android.tethering deapexer
+# $ANDROID_HOST_OUT/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/com.android.tethering.capex --output /tmp/decompressed.apex
+# adb install /tmp/decompressed.apex && adb reboot
+#
+# This has been tested on Android T and Android U Pixel devices. On recent (U+) devices, it requires
+# setting a released target SDK (for example target_sdk_version: "34") in
+# packages/modules/Connectivity/service/ServiceConnectivityResources/Android.bp before building.
+set -e
+
+function push_apex {
+ local original_apex_name=$1
+ local aosp_apex_name=$2
+ if $ADB_CMD shell ls /system/apex/$original_apex_name.capex 1>/dev/null 2>/dev/null; then
+ $ADB_CMD shell rm /system/apex/$original_apex_name.capex
+ $ADB_CMD push $ANDROID_PRODUCT_OUT/system/apex/$aosp_apex_name.capex /system/apex/
+ else
+ rm -f /tmp/decompressed_$aosp_apex_name.apex
+ $ANDROID_HOST_OUT/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/$aosp_apex_name.capex --output /tmp/decompressed_$aosp_apex_name.apex
+ $ADB_CMD shell rm /system/apex/$original_apex_name.apex
+ $ADB_CMD push /tmp/decompressed_$aosp_apex_name.apex /system/apex/$aosp_apex_name.apex
+ rm /tmp/decompressed_$aosp_apex_name.apex
+ fi
+}
+
+function push_apk {
+ local app_type=$1
+ local original_apk_name=$2
+ local aosp_apk_name=$3
+ $ADB_CMD shell rm /system/$app_type/$original_apk_name/$original_apk_name.apk
+ $ADB_CMD push $ANDROID_PRODUCT_OUT/system/$app_type/$aosp_apk_name/$aosp_apk_name.apk /system/$app_type/$original_apk_name/
+}
+
+NETWORKSTACK_AOSP_SEPOLICY_KEY="<signer signature=\"308205dc308203c4a003020102020900fc6cb0d8a6fdd16\
+8300d06092a864886f70d01010b0500308181310b30090603550406130255533113301106035504080c0a43616c69666f72\
+6e69613116301406035504070c0d4d6f756e7461696e20566965773110300e060355040a0c07416e64726f69643110300e0\
+60355040b0c07416e64726f69643121301f06035504030c18636f6d2e616e64726f69642e6e6574776f726b737461636b30\
+20170d3139303231323031343632305a180f34373537303130383031343632305a308181310b30090603550406130255533\
+113301106035504080c0a43616c69666f726e69613116301406035504070c0d4d6f756e7461696e20566965773110300e06\
+0355040a0c07416e64726f69643110300e060355040b0c07416e64726f69643121301f06035504030c18636f6d2e616e647\
+26f69642e6e6574776f726b737461636b30820222300d06092a864886f70d01010105000382020f003082020a0282020100\
+bb71f5137ff0b2d757acc2ca3d378e0f8de11090d5caf3d49e314d35c283b778b02d792d8eba440364ca970985441660f0b\
+c00afbc63dd611b1bf51ad28a1edd21e0048f548b80f8bd113e25682822f57dab8273afaf12c64d19a0c6be238f3e66ddc7\
+9b10fd926931e3ee60a7bf618644da3c2c4fc428139d45d27beda7fe45e30075b493ead6ec01cdd55d931c0a657e2e59742\
+ca632b6dc3842a2deb7d22443c809291d7a549203ae6ae356582a4ca23f30f0549c4ec8408a75278e95c69e8390ad5280bc\
+efaef6f1309a41bd9f3bfb5d12dca7e79ec6fd6848193fa9ab728224887b4f93e985ec7cbf6401b0e863a4b91c05d046f04\
+0fe954004b1645954fcb4114cee1e8b64b47d719a19ef4c001cb183f7f3e166e43f56d68047c3440da34fdf529d44274b8b\
+2f6afb345091ad8ad4b93bd5c55d52286a5d3c157465db8ddf62e7cdb6b10fb18888046afdd263ae6f2125d9065759c7e42\
+f8610a6746edbdc547d4301612eeec3c3cbd124dececc8d38b20e73b13f24ee7ca13a98c5f61f0c81b07d2b519749bc2bcb\
+9e0949aef6c118a3e8125e6ab57fce46bb091a66740e10b31c740b891900c0ecda9cc69ecb4f3369998b175106dd0a4ffd7\
+024eb7e75fedd1a5b131d0bb2b40c63491e3cf86b8957b21521b3a96ed1376a51a6ac697866b0256dee1bcd9ab9a188bf4c\
+ed80b59a5f24c2da9a55eb7b0e502116e30203010001a3533051301d0603551d0e041604149383c92cfbf099d5c47b0c365\
+7d8622a084b72e1301f0603551d230418301680149383c92cfbf099d5c47b0c3657d8622a084b72e1300f0603551d130101\
+ff040530030101ff300d06092a864886f70d01010b050003820201006a0501382fde2a6b8f70c60cd1b8ee4f788718c288b\
+170258ef3a96230b65005650d6a4c42a59a97b2ddec502413e7b438fbd060363d74b74a232382a7f77fd3da34e38f79fad0\
+35a8b472c5cff365818a0118d87fa1e31cc7ed4befd27628760c290980c3cc3b7ff0cfd01b75ff1fcc83e981b5b25a54d85\
+b68a80424ac26015fb3a4c754969a71174c0bc283f6c88191dced609e245f5938ffd0ad799198e2d0bf6342221c1b0a5d33\
+2ed2fffc668982cabbcb7d3b630ff8476e5c84ac0ad37adf9224035200039f95ec1fa95bf83796c0e8986135cee2dcaef19\
+0b249855a7e7397d4a0bf17ea63d978589c6b48118a381fffbd790c44d80233e2e35292a3b5533ca3f2cc173f85cf904adf\
+e2e4e2183dc1eba0ebae07b839a81ff1bc92e292550957c8599af21e9c0497b9234ce345f3f508b1cc872aa55ddb5e773c5\
+c7dd6577b9a8b6daed20ae1ff4b8206fd9f5c8f5a22ba1980bef01ae6fcb2659b97ad5b985fa81c019ffe008ddd9c8130c0\
+6fc6032b2149c2209fc438a7e8c3b20ce03650ad31c4ee48f169777a0ae182b72ca31b81540f61f167d8d7adf4f6bb2330f\
+f5c24037245000d8172c12ab5d5aa5890b8b12db0f0e7296264eb66e7f9714c31004649fb4b864005f9c43c80db3f6de52f\
+d44d6e2036bfe7f5807156ed5ab591d06fd6bb93ba4334ea2739af8b41ed2686454e60b666d10738bb7ba88001\">\
+<seinfo value=\"network_stack\"\/><\/signer>"
+
+DEVICE=$1
+ADB_CMD="adb -s $DEVICE"
+
+if [ -z "$DEVICE" ]; then
+ echo "Usage: aospify_device.sh [device_serial]"
+ exit 1
+fi
+
+if [ -z "$ANDROID_BUILD_TOP" ]; then
+ echo "Run build/envsetup.sh first to set ANDROID_BUILD_TOP"
+ exit 1
+fi
+
+if ! $ADB_CMD wait-for-device shell pm path com.google.android.networkstack; then
+ echo "This device is already not using GMS modules"
+ exit 1
+fi
+
+read -p "This script is only for test purposes and highly likely to make your device unusable. \
+Continue ? <y/N>" prompt
+if [[ $prompt != "y" ]]
+then
+ exit 0
+fi
+
+cd $ANDROID_BUILD_TOP
+source build/envsetup.sh
+lunch aosp_arm64-trunk_staging-userdebug
+m NetworkStack CaptivePortalLogin com.android.tethering com.android.cellbroadcast \
+ com.android.resolv deapexer \
+ out/target/product/generic_arm64/system/etc/selinux/plat_mac_permissions.xml \
+ out/target/product/generic_arm64/system/etc/permissions/com.android.networkstack.xml
+
+$ADB_CMD root
+$ADB_CMD remount
+$ADB_CMD reboot
+
+echo "Waiting for boot..."
+$ADB_CMD wait-for-device;
+until [[ $($ADB_CMD shell getprop sys.boot_completed) == 1 ]]; do
+ sleep 1;
+done
+
+$ADB_CMD root
+$ADB_CMD remount
+
+push_apk priv-app NetworkStackGoogle NetworkStack
+push_apk app CaptivePortalLoginGoogle CaptivePortalLogin
+push_apex com.google.android.tethering com.android.tethering
+push_apex com.google.android.cellbroadcast com.android.cellbroadcast
+push_apex com.google.android.resolv com.android.resolv
+
+# Replace the network_stack key used to set its sepolicy context
+rm -f /tmp/pulled_plat_mac_permissions.xml
+$ADB_CMD pull /system/etc/selinux/plat_mac_permissions.xml /tmp/pulled_plat_mac_permissions.xml
+sed_replace='s/<signer signature="[0-9a-fA-F]+"><seinfo value="network_stack"\/><\/signer>/'$NETWORKSTACK_AOSP_SEPOLICY_KEY'/'
+sed -E "$sed_replace" /tmp/pulled_plat_mac_permissions.xml |
+ $ADB_CMD shell 'cat > /system/etc/selinux/plat_mac_permissions.xml'
+rm /tmp/pulled_plat_mac_permissions.xml
+
+# Update the networkstack privapp-permissions allowlist
+rm -f /tmp/pulled_privapp-permissions.xml
+$ADB_CMD pull /system/etc/permissions/privapp-permissions-google.xml /tmp/pulled_privapp-permissions.xml
+
+# Remove last </permission> line, and the permissions for com.google.android.networkstack
+sed -nE '1,/<\/permissions>/p' /tmp/pulled_privapp-permissions.xml \
+ | sed -E '/com.google.android.networkstack/,/privapp-permissions/d' > /tmp/modified_privapp-permissions.xml
+# Add the AOSP permissions and re-add the </permissions> line
+sed -nE '/com.android.networkstack/,/privapp-permissions/p' $ANDROID_PRODUCT_OUT/system/etc/permissions/com.android.networkstack.xml \
+ >> /tmp/modified_privapp-permissions.xml
+echo '</permissions>' >> /tmp/modified_privapp-permissions.xml
+
+$ADB_CMD push /tmp/modified_privapp-permissions.xml /system/etc/permissions/privapp-permissions-google.xml
+
+rm /tmp/pulled_privapp-permissions.xml /tmp/modified_privapp-permissions.xml
+
+echo "Done modifying, rebooting"
+$ADB_CMD reboot
\ No newline at end of file