Merge "Add mcts and mcts-tethering to net-tests-utils-host-common for the dependency of ConnectivityTestPreparer" into android15-tests-dev am: 900fb00793
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/3381265
Change-Id: I92961a480281fc239dcde1d0cf0480cbbd496486
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Cronet/OWNERS b/Cronet/OWNERS
deleted file mode 100644
index c24680e..0000000
--- a/Cronet/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-set noparent
-file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 6d8ed4a..078ccde 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,12 +1,13 @@
jchalard@google.com
junyulai@google.com
lorenzo@google.com
-martinwu@google.com
maze@google.com
motomuman@google.com
paulhu@google.com
prohr@google.com
reminv@google.com
-satk@google.com
xiaom@google.com
yuyanghuang@google.com
+
+martinwu@google.com #{LAST_RESORT_SUGGESTION}
+satk@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file
diff --git a/TEST_MAPPING b/TEST_MAPPING
index bcf5e8b..c1bc31e 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -316,6 +316,14 @@
}
]
},
+ {
+ "name": "CtsHostsideNetworkTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
// Test with APK modules only, in cases where APEX is not supported, or the other modules
// were simply not updated
{
@@ -416,6 +424,11 @@
]
}
],
+ "automotive-mumd-presubmit": [
+ {
+ "name": "CtsNetTestCases"
+ }
+ ],
"imports": [
{
"path": "frameworks/base/core/java/android/net"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index b4426a6..5cf5528 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -32,16 +32,19 @@
java_defaults {
name: "TetheringExternalLibs",
+ defaults: [
+ "TetheringApiLevel",
+ ],
// Libraries not including Tethering's own framework-tethering (different flavors of that one
// are needed depending on the build rule)
libs: [
"connectivity-internal-api-util",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
"framework-connectivity-t.stubs.module_lib",
"framework-statsd.stubs.module_lib",
- "framework-wifi",
- "framework-bluetooth",
+ "framework-wifi.stubs.module_lib",
+ "framework-bluetooth.stubs.module_lib",
"unsupportedappusage",
],
defaults_visibility: ["//visibility:private"],
@@ -54,6 +57,7 @@
"src/**/*.java",
":framework-connectivity-shared-srcs",
":services-tethering-shared-srcs",
+ ":statslog-connectivity-java-gen",
":statslog-tethering-java-gen",
],
static_libs: [
@@ -69,12 +73,13 @@
"android.hardware.tetheroffload.control-V1.0-java",
"android.hardware.tetheroffload.control-V1.1-java",
"android.hidl.manager-V1.2-java",
- "net-utils-tethering",
+ "net-utils-connectivity-apks",
"netd-client",
"tetheringstatsprotos",
],
defaults: ["TetheringExternalLibs"],
libs: [
+ "framework-annotations-lib",
"framework-tethering.impl",
],
manifest: "AndroidManifestBase.xml",
@@ -89,7 +94,6 @@
defaults: [
"ConnectivityNextEnableDefaults",
"TetheringAndroidLibraryDefaults",
- "TetheringApiLevel",
"TetheringReleaseTargetSdk",
],
static_libs: [
@@ -105,7 +109,6 @@
name: "TetheringApiStableLib",
defaults: [
"TetheringAndroidLibraryDefaults",
- "TetheringApiLevel",
"TetheringReleaseTargetSdk",
],
static_libs: [
@@ -194,7 +197,6 @@
name: "Tethering",
defaults: [
"TetheringAppDefaults",
- "TetheringApiLevel",
],
static_libs: ["TetheringApiStableLib"],
certificate: "networkstack",
@@ -202,13 +204,13 @@
use_embedded_native_libs: true,
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
+ updatable: true,
}
android_app {
name: "TetheringNext",
defaults: [
"TetheringAppDefaults",
- "TetheringApiLevel",
"ConnectivityNextEnableDefaults",
],
static_libs: ["TetheringApiCurrentLib"],
@@ -220,6 +222,7 @@
lint: {
error_checks: ["NewApi"],
},
+ updatable: true,
}
sdk {
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index 6a363b0..32442f5 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -32,7 +32,11 @@
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
<uses-permission android:name="android.permission.MANAGE_USB" />
+ <!-- MANAGE_USERS is for accessing multi-user APIs, note that QUERY_USERS should
+ not be used since it is not a privileged permission until U. -->
+ <uses-permission android:name="android.permission.MANAGE_USERS"/>
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 8ed5ac0..0c05354 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -98,7 +98,6 @@
],
canned_fs_config: "canned_fs_config",
bpfs: [
- "block.o",
"clatd.o",
"dscpPolicy.o",
"netd.o",
@@ -114,7 +113,7 @@
"current_sdkinfo",
"netbpfload.33rc",
"netbpfload.35rc",
- "ot-daemon.init.34rc",
+ "ot-daemon.34rc",
],
manifest: "manifest.json",
key: "com.android.tethering.key",
@@ -215,6 +214,7 @@
"android.net.nsd",
"android.net.thread",
"android.net.wear",
+ "android.net.http.internal",
],
},
}
diff --git a/Tethering/apex/permissions/permissions.xml b/Tethering/apex/permissions/permissions.xml
index f26a961..4051877 100644
--- a/Tethering/apex/permissions/permissions.xml
+++ b/Tethering/apex/permissions/permissions.xml
@@ -18,7 +18,9 @@
<permissions>
<privapp-permissions package="com.android.networkstack.tethering">
<permission name="android.permission.BLUETOOTH_PRIVILEGED" />
+ <permission name="android.permission.INTERACT_ACROSS_USERS"/>
<permission name="android.permission.MANAGE_USB"/>
+ <permission name="android.permission.MANAGE_USERS"/>
<permission name="android.permission.MODIFY_PHONE_STATE"/>
<permission name="android.permission.READ_NETWORK_USAGE_HISTORY"/>
<permission name="android.permission.TETHER_PRIVILEGED"/>
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 0df9047..af061e4 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -198,4 +198,13 @@
public String toString() {
return "Netd used";
}
+
+ @Override
+ public int getLastMaxConnectionAndResetToCurrent() {
+ return 0;
+ }
+
+ @Override
+ public void clearConnectionCounters() {
+ }
}
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 4d1e7ef..b460f0d 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -19,6 +19,7 @@
import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ACTIVE_SESSIONS_METRICS;
import android.system.ErrnoException;
import android.system.Os;
@@ -108,6 +109,22 @@
// TODO: Add IPv6 rule count.
private final SparseArray<Integer> mRule4CountOnUpstream = new SparseArray<>();
+ private final boolean mSupportActiveSessionsMetrics;
+ /**
+ * Tracks the current number of tethering connections and the maximum
+ * observed since the last metrics collection. Used to provide insights
+ * into the distribution of active tethering sessions for metrics reporting.
+
+ * These variables are accessed on the handler thread, which includes:
+ * 1. ConntrackEvents signaling the addition or removal of an IPv4 rule.
+ * 2. ConntrackEvents indicating the removal of a tethering client,
+ * triggering the removal of associated rules.
+ * 3. Removal of the last IpServer, which resets counters to handle
+ * potential synchronization issues.
+ */
+ private int mLastMaxConnectionCount = 0;
+ private int mCurrentConnectionCount = 0;
+
public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
mLog = deps.getSharedLog().forSubComponent(TAG);
@@ -156,6 +173,9 @@
} catch (ErrnoException e) {
mLog.e("Could not clear mBpfDevMap: " + e);
}
+
+ mSupportActiveSessionsMetrics = deps.isFeatureEnabled(deps.getContext(),
+ TETHER_ACTIVE_SESSIONS_METRICS);
}
@Override
@@ -350,6 +370,12 @@
final int upstreamIfindex = (int) key.iif;
int count = mRule4CountOnUpstream.get(upstreamIfindex, 0 /* default */);
mRule4CountOnUpstream.put(upstreamIfindex, ++count);
+
+ if (mSupportActiveSessionsMetrics) {
+ mCurrentConnectionCount++;
+ mLastMaxConnectionCount = Math.max(mCurrentConnectionCount,
+ mLastMaxConnectionCount);
+ }
} else {
mBpfUpstream4Map.insertEntry(key, value);
}
@@ -359,6 +385,7 @@
} catch (IllegalStateException e) {
// Silent if the rule already exists. Note that the errno EEXIST was rethrown as
// IllegalStateException. See BpfMap#insertEntry.
+ return false;
}
return true;
}
@@ -384,6 +411,10 @@
} else {
mRule4CountOnUpstream.put(upstreamIfindex, count);
}
+
+ if (mSupportActiveSessionsMetrics) {
+ mCurrentConnectionCount--;
+ }
} else {
if (!mBpfUpstream4Map.deleteEntry(key)) return false; // Rule did not exist
}
@@ -464,14 +495,16 @@
@Override
public String toString() {
- return String.join(", ", new String[] {
- mapStatus(mBpfDownstream6Map, "mBpfDownstream6Map"),
- mapStatus(mBpfUpstream6Map, "mBpfUpstream6Map"),
- mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"),
- mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"),
- mapStatus(mBpfStatsMap, "mBpfStatsMap"),
- mapStatus(mBpfLimitMap, "mBpfLimitMap"),
- mapStatus(mBpfDevMap, "mBpfDevMap")
+ return String.join(", ", new String[]{
+ mapStatus(mBpfDownstream6Map, "mBpfDownstream6Map"),
+ mapStatus(mBpfUpstream6Map, "mBpfUpstream6Map"),
+ mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"),
+ mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"),
+ mapStatus(mBpfStatsMap, "mBpfStatsMap"),
+ mapStatus(mBpfLimitMap, "mBpfLimitMap"),
+ mapStatus(mBpfDevMap, "mBpfDevMap"),
+ "mCurrentConnectionCount=" + mCurrentConnectionCount,
+ "mLastMaxConnectionCount=" + mLastMaxConnectionCount
});
}
@@ -506,4 +539,17 @@
return 0;
}
+
+ /** Get last max connection count and reset to current count. */
+ public int getLastMaxConnectionAndResetToCurrent() {
+ final int ret = mLastMaxConnectionCount;
+ mLastMaxConnectionCount = mCurrentConnectionCount;
+ return ret;
+ }
+
+ /** Clear current connection count. */
+ public void clearConnectionCounters() {
+ mCurrentConnectionCount = 0;
+ mLastMaxConnectionCount = 0;
+ }
}
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index d28a397..cb8bcc9 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -140,6 +140,8 @@
/**
* Adds a tethering IPv4 offload rule to appropriate BPF map.
+ *
+ * @return true iff the map was modified, false if the key already exists or there was an error.
*/
public abstract boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
@NonNull Tether4Value value);
@@ -200,5 +202,11 @@
* Remove interface index mapping.
*/
public abstract boolean removeDevMap(int ifIndex);
+
+ /** Get last max connection count and reset to current count. */
+ public abstract int getLastMaxConnectionAndResetToCurrent();
+
+ /** Clear current connection count. */
+ public abstract void clearConnectionCounters();
}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 39a7540..e2498e4 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -29,9 +29,14 @@
"//packages/modules/Connectivity/framework-t",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service-t",
+ "//packages/modules/Connectivity/staticlibs",
// Using for test only
"//cts/tests/netlegacy22.api",
+
+ // TODO: b/374174952 Remove it when VCN CTS is moved to Connectivity/
+ "//cts/tests/tests/vcn",
+
"//external/sl4a:__subpackages__",
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
@@ -46,10 +51,14 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
+ "//packages/modules/NetworkStack",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
- stub_only_libs: ["framework-connectivity.stubs.module_lib"],
+ stub_only_libs: [
+ "framework-connectivity.stubs.module_lib",
+ "sdk_module-lib_current_framework-wifi",
+ ],
jarjar_rules: ":framework-tethering-jarjar-rules",
installable: true,
@@ -97,10 +106,17 @@
srcs: [
":framework-tethering-srcs",
],
- libs: ["framework-connectivity.stubs.module_lib"],
+ libs: [
+ "framework-connectivity.stubs.module_lib",
+ "sdk_module-lib_current_framework-wifi",
+ ],
+ static_libs: [
+ "com.android.net.flags-aconfig-java",
+ ],
aidl: {
include_dirs: [
"packages/modules/Connectivity/framework/aidl-export",
+ "packages/modules/Wifi/framework/aidl-export",
],
},
apex_available: ["com.android.tethering"],
diff --git a/Tethering/common/TetheringLib/api/module-lib-current.txt b/Tethering/common/TetheringLib/api/module-lib-current.txt
index 460c216..e893894 100644
--- a/Tethering/common/TetheringLib/api/module-lib-current.txt
+++ b/Tethering/common/TetheringLib/api/module-lib-current.txt
@@ -46,5 +46,10 @@
method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableWifiRegexs();
}
+ public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public String getPackageName();
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public int getUid();
+ }
+
}
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index cccafd5..0e85956 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -21,8 +21,10 @@
public final class TetheringInterface implements android.os.Parcelable {
ctor public TetheringInterface(int, @NonNull String);
+ ctor @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public TetheringInterface(int, @NonNull String, @Nullable android.net.wifi.SoftApConfiguration);
method public int describeContents();
method @NonNull public String getInterface();
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable @RequiresPermission(value=android.Manifest.permission.NETWORK_SETTINGS, conditional=true) public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
method public int getType();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringInterface> CREATOR;
@@ -97,15 +99,16 @@
}
public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
- method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public int describeContents();
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public int describeContents();
method @Nullable public android.net.LinkAddress getClientStaticIpv4Address();
method public int getConnectivityScope();
method @Nullable public android.net.LinkAddress getLocalIpv4Address();
method public boolean getShouldShowEntitlementUi();
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
method public int getTetheringType();
method public boolean isExemptFromEntitlementCheck();
- method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
- field @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringManager.TetheringRequest> CREATOR;
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringManager.TetheringRequest> CREATOR;
}
public static class TetheringManager.TetheringRequest.Builder {
@@ -114,6 +117,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setConnectivityScope(int);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setExemptFromEntitlementCheck(boolean);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setShouldShowEntitlementUi(boolean);
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setSoftApConfiguration(@Nullable android.net.wifi.SoftApConfiguration);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setStaticIpv4Addresses(@NonNull android.net.LinkAddress, @NonNull android.net.LinkAddress);
}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
index 84cdef1..0464fe0 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
@@ -16,13 +16,19 @@
package android.net;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.net.TetheringManager.TetheringType;
+import android.net.wifi.SoftApConfiguration;
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.net.flags.Flags;
+
import java.util.Objects;
/**
@@ -33,15 +39,21 @@
public final class TetheringInterface implements Parcelable {
private final int mType;
private final String mInterface;
+ @Nullable
+ private final SoftApConfiguration mSoftApConfig;
+ @SuppressLint("UnflaggedApi")
public TetheringInterface(@TetheringType int type, @NonNull String iface) {
+ this(type, iface, null);
+ }
+
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ public TetheringInterface(@TetheringType int type, @NonNull String iface,
+ @Nullable SoftApConfiguration softApConfig) {
Objects.requireNonNull(iface);
mType = type;
mInterface = iface;
- }
-
- private TetheringInterface(@NonNull Parcel in) {
- this(in.readInt(), in.readString());
+ mSoftApConfig = softApConfig;
}
/** Get tethering type. */
@@ -55,22 +67,36 @@
return mInterface;
}
+ /**
+ * Get the SoftApConfiguration provided for this interface, if any. This will only be populated
+ * for apps with the same uid that specified the configuration, or apps with permission
+ * {@link android.Manifest.permission.NETWORK_SETTINGS}.
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ @RequiresPermission(value = android.Manifest.permission.NETWORK_SETTINGS, conditional = true)
+ @Nullable
+ public SoftApConfiguration getSoftApConfiguration() {
+ return mSoftApConfig;
+ }
+
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mType);
dest.writeString(mInterface);
+ dest.writeParcelable(mSoftApConfig, flags);
}
@Override
public int hashCode() {
- return Objects.hash(mType, mInterface);
+ return Objects.hash(mType, mInterface, mSoftApConfig);
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof TetheringInterface)) return false;
final TetheringInterface other = (TetheringInterface) obj;
- return mType == other.mType && mInterface.equals(other.mInterface);
+ return mType == other.mType && mInterface.equals(other.mInterface)
+ && Objects.equals(mSoftApConfig, other.mSoftApConfig);
}
@Override
@@ -82,8 +108,10 @@
public static final Creator<TetheringInterface> CREATOR = new Creator<TetheringInterface>() {
@NonNull
@Override
+ @SuppressLint("UnflaggedApi")
public TetheringInterface createFromParcel(@NonNull Parcel in) {
- return new TetheringInterface(in);
+ return new TetheringInterface(in.readInt(), in.readString(),
+ in.readParcelable(SoftApConfiguration.class.getClassLoader()));
}
@NonNull
@@ -97,6 +125,8 @@
@Override
public String toString() {
return "TetheringInterface {mType=" + mType
- + ", mInterface=" + mInterface + "}";
+ + ", mInterface=" + mInterface
+ + ((mSoftApConfig == null) ? "" : ", mSoftApConfig=" + mSoftApConfig)
+ + "}";
}
}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 0f5a014..bc771da 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -26,11 +26,14 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.content.Context;
+import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.util.ArrayMap;
@@ -38,6 +41,7 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.net.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -50,6 +54,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.StringJoiner;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
@@ -62,16 +67,6 @@
*/
@SystemApi
public class TetheringManager {
- // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
- // available here
- /** @hide */
- public static class Flags {
- static final String TETHERING_REQUEST_WITH_SOFT_AP_CONFIG =
- "com.android.net.flags.tethering_request_with_soft_ap_config";
- static final String TETHERING_REQUEST_VIRTUAL =
- "com.android.net.flags.tethering_request_virtual";
- }
-
private static final String TAG = TetheringManager.class.getSimpleName();
private static final int DEFAULT_TIMEOUT_MS = 60_000;
private static final long CONNECTOR_POLL_INTERVAL_MILLIS = 200L;
@@ -204,7 +199,7 @@
* AVF(Android Virtualization Framework).
* @hide
*/
- @FlaggedApi(Flags.TETHERING_REQUEST_VIRTUAL)
+ @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_VIRTUAL)
@SystemApi
public static final int TETHERING_VIRTUAL = 7;
@@ -214,6 +209,20 @@
*/
public static final int MAX_TETHERING_TYPE = TETHERING_VIRTUAL;
+ private static String typeToString(@TetheringType int type) {
+ switch (type) {
+ case TETHERING_INVALID: return "TETHERING_INVALID";
+ case TETHERING_WIFI: return "TETHERING_WIFI";
+ case TETHERING_USB: return "TETHERING_USB";
+ case TETHERING_BLUETOOTH: return "TETHERING_BLUETOOTH";
+ case TETHERING_WIFI_P2P: return "TETHERING_WIFI_P2P";
+ case TETHERING_NCM: return "TETHERING_NCM";
+ case TETHERING_ETHERNET: return "TETHERING_ETHERNET";
+ default:
+ return "TETHERING_UNKNOWN(" + type + ")";
+ }
+ }
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
@@ -437,7 +446,7 @@
// Used to dispatch legacy ConnectivityManager methods that expect tethering to be able to
// return results and perform operations synchronously.
// TODO: remove once there are no callers of these legacy methods.
- private class RequestDispatcher {
+ private static class RequestDispatcher {
private final ConditionVariable mWaiting;
public volatile int mRemoteResult;
@@ -453,8 +462,8 @@
mWaiting = new ConditionVariable();
}
- int waitForResult(final RequestHelper request) {
- getConnector(c -> request.runRequest(c, mListener));
+ int waitForResult(final RequestHelper request, final TetheringManager mgr) {
+ mgr.getConnector(c -> request.runRequest(c, mListener));
if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) {
throw new IllegalStateException("Callback timeout");
}
@@ -610,7 +619,7 @@
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
- });
+ }, this);
}
/**
@@ -642,7 +651,7 @@
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
- });
+ }, this);
}
/**
@@ -670,7 +679,7 @@
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
- });
+ }, this);
}
/**
@@ -695,6 +704,17 @@
})
public @interface ConnectivityScope {}
+ private static String connectivityScopeToString(@ConnectivityScope int scope) {
+ switch (scope) {
+ case CONNECTIVITY_SCOPE_GLOBAL:
+ return "CONNECTIVITY_SCOPE_GLOBAL";
+ case CONNECTIVITY_SCOPE_LOCAL:
+ return "CONNECTIVITY_SCOPE_LOCAL";
+ default:
+ return "CONNECTIVITY_SCOPE_UNKNOWN(" + scope + ")";
+ }
+ }
+
/**
* Use with {@link #startTethering} to specify additional parameters when starting tethering.
*/
@@ -705,7 +725,7 @@
/**
* @hide
*/
- @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
public TetheringRequest(@NonNull final TetheringRequestParcel request) {
mRequestParcel = request;
}
@@ -714,7 +734,7 @@
mRequestParcel = in.readParcelable(TetheringRequestParcel.class.getClassLoader());
}
- @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
@NonNull
public static final Creator<TetheringRequest> CREATOR = new Creator<>() {
@Override
@@ -728,13 +748,13 @@
}
};
- @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
@Override
public int describeContents() {
return 0;
}
- @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelable(mRequestParcel, flags);
@@ -753,6 +773,8 @@
mBuilderParcel.exemptFromEntitlementCheck = false;
mBuilderParcel.showProvisioningUi = true;
mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type);
+ mBuilderParcel.uid = Process.INVALID_UID;
+ mBuilderParcel.softApConfig = null;
}
/**
@@ -812,6 +834,30 @@
return this;
}
+ /**
+ * Set the desired SoftApConfiguration for {@link #TETHERING_WIFI}. If this is null or
+ * not set, then the persistent tethering SoftApConfiguration from
+ * {@link WifiManager#getSoftApConfiguration()} will be used.
+ * </p>
+ * If TETHERING_WIFI is already enabled and a new request is made with a different
+ * SoftApConfiguration, the request will be accepted if the device can support an
+ * additional tethering Wi-Fi AP interface. Otherwise, the request will be rejected.
+ *
+ * @param softApConfig SoftApConfiguration to use.
+ * @throws IllegalArgumentException if the tethering type isn't TETHERING_WIFI.
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+ @NonNull
+ public Builder setSoftApConfiguration(@Nullable SoftApConfiguration softApConfig) {
+ if (mBuilderParcel.tetheringType != TETHERING_WIFI) {
+ throw new IllegalArgumentException(
+ "SoftApConfiguration can only be set for TETHERING_WIFI");
+ }
+ mBuilderParcel.softApConfig = softApConfig;
+ return this;
+ }
+
/** Build {@link TetheringRequest} with the currently set configuration. */
@NonNull
public TetheringRequest build() {
@@ -893,6 +939,56 @@
}
/**
+ * Get the desired SoftApConfiguration of the request, if one was specified.
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ @Nullable
+ public SoftApConfiguration getSoftApConfiguration() {
+ return mRequestParcel.softApConfig;
+ }
+
+ /**
+ * Sets the UID of the app that sent this request. This should always be overridden when
+ * receiving TetheringRequest from an external source.
+ * @hide
+ */
+ public void setUid(int uid) {
+ mRequestParcel.uid = uid;
+ }
+
+ /**
+ * Sets the package name of the app that sent this request. This should always be overridden
+ * when receiving a TetheringRequest from an external source.
+ * @hide
+ */
+ public void setPackageName(String packageName) {
+ mRequestParcel.packageName = packageName;
+ }
+
+ /**
+ * Gets the UID of the app that sent this request. This defaults to
+ * {@link Process#INVALID_UID} if unset.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ @SystemApi(client = MODULE_LIBRARIES)
+ public int getUid() {
+ return mRequestParcel.uid;
+ }
+
+ /**
+ * Gets the package name of the app that sent this request. This defaults to {@code null} if
+ * unset.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ @SystemApi(client = MODULE_LIBRARIES)
+ @Nullable
+ public String getPackageName() {
+ return mRequestParcel.packageName;
+ }
+
+ /**
* Get a TetheringRequestParcel from the configuration
* @hide
*/
@@ -902,12 +998,31 @@
/** String of TetheringRequest detail. */
public String toString() {
- return "TetheringRequest [ type= " + mRequestParcel.tetheringType
- + ", localIPv4Address= " + mRequestParcel.localIPv4Address
- + ", staticClientAddress= " + mRequestParcel.staticClientAddress
- + ", exemptFromEntitlementCheck= "
- + mRequestParcel.exemptFromEntitlementCheck + ", showProvisioningUi= "
- + mRequestParcel.showProvisioningUi + " ]";
+ StringJoiner sj = new StringJoiner(", ", "TetheringRequest[ ", " ]");
+ sj.add(typeToString(mRequestParcel.tetheringType));
+ if (mRequestParcel.localIPv4Address != null) {
+ sj.add("localIpv4Address=" + mRequestParcel.localIPv4Address);
+ }
+ if (mRequestParcel.staticClientAddress != null) {
+ sj.add("staticClientAddress=" + mRequestParcel.staticClientAddress);
+ }
+ if (mRequestParcel.exemptFromEntitlementCheck) {
+ sj.add("exemptFromEntitlementCheck");
+ }
+ if (mRequestParcel.showProvisioningUi) {
+ sj.add("showProvisioningUi");
+ }
+ sj.add(connectivityScopeToString(mRequestParcel.connectivityScope));
+ if (mRequestParcel.softApConfig != null) {
+ sj.add("softApConfig=" + mRequestParcel.softApConfig);
+ }
+ if (mRequestParcel.uid != Process.INVALID_UID) {
+ sj.add("uid=" + mRequestParcel.uid);
+ }
+ if (mRequestParcel.packageName != null) {
+ sj.add("packageName=" + mRequestParcel.packageName);
+ }
+ return sj.toString();
}
@Override
@@ -921,7 +1036,10 @@
&& Objects.equals(parcel.staticClientAddress, otherParcel.staticClientAddress)
&& parcel.exemptFromEntitlementCheck == otherParcel.exemptFromEntitlementCheck
&& parcel.showProvisioningUi == otherParcel.showProvisioningUi
- && parcel.connectivityScope == otherParcel.connectivityScope;
+ && parcel.connectivityScope == otherParcel.connectivityScope
+ && Objects.equals(parcel.softApConfig, otherParcel.softApConfig)
+ && parcel.uid == otherParcel.uid
+ && Objects.equals(parcel.packageName, otherParcel.packageName);
}
@Override
@@ -929,7 +1047,8 @@
TetheringRequestParcel parcel = getParcel();
return Objects.hash(parcel.tetheringType, parcel.localIPv4Address,
parcel.staticClientAddress, parcel.exemptFromEntitlementCheck,
- parcel.showProvisioningUi, parcel.connectivityScope);
+ parcel.showProvisioningUi, parcel.connectivityScope, parcel.softApConfig,
+ parcel.uid, parcel.packageName);
}
}
@@ -1722,7 +1841,7 @@
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
- });
+ }, this);
return ret == TETHER_ERROR_NO_ERROR;
}
@@ -1771,6 +1890,6 @@
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
- });
+ }, this);
}
}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index f13c970..789d5bb 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -17,6 +17,7 @@
package android.net;
import android.net.LinkAddress;
+import android.net.wifi.SoftApConfiguration;
/**
* Configuration details for requesting tethering.
@@ -29,4 +30,7 @@
boolean exemptFromEntitlementCheck;
boolean showProvisioningUi;
int connectivityScope;
+ SoftApConfiguration softApConfig;
+ int uid;
+ String packageName;
}
diff --git a/Tethering/res/values-fa/strings.xml b/Tethering/res/values-fa/strings.xml
index d7f2543..fdfd5c4 100644
--- a/Tethering/res/values-fa/strings.xml
+++ b/Tethering/res/values-fa/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="tethered_notification_title" msgid="5350162111436634622">"اشتراکگذاری اینترنت یا نقطه اتصال فعال است"</string>
- <string name="tethered_notification_message" msgid="2338023450330652098">"برای راهاندازی، ضربه بزنید."</string>
+ <string name="tethered_notification_message" msgid="2338023450330652098">"برای راهاندازی، تکضرب بزنید."</string>
<string name="disable_tether_notification_title" msgid="3183576627492925522">"اشتراکگذاری اینترنت غیرفعال است"</string>
<string name="disable_tether_notification_message" msgid="6655882039707534929">"برای جزئیات، با سرپرستتان تماس بگیرید"</string>
<string name="notification_channel_tethering_status" msgid="7030733422705019001">"وضعیت نقطه اتصال و اشتراکگذاری اینترنت"</string>
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index b807544..ebc9e4e 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -69,6 +69,7 @@
import com.android.internal.util.MessageUtils;
import com.android.internal.util.State;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.IIpv4PrefixRequest;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.RoutingCoordinatorManager;
@@ -76,7 +77,6 @@
import com.android.net.module.util.SyncStateMachine.StateInfo;
import com.android.net.module.util.ip.InterfaceController;
import com.android.networkstack.tethering.BpfCoordinator;
-import com.android.networkstack.tethering.PrivateAddressCoordinator;
import com.android.networkstack.tethering.TetheringConfiguration;
import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
@@ -124,6 +124,8 @@
// TODO: have PanService use some visible version of this constant
private static final String BLUETOOTH_IFACE_ADDR = "192.168.44.1/24";
+ private static final String LEGACY_WIFI_P2P_IFACE_ADDRESS = "192.168.49.1/24";
+
// TODO: have this configurable
private static final int DHCP_LEASE_TIME_SECS = 3600;
@@ -166,10 +168,10 @@
/**
* Request Tethering change.
*
- * @param tetheringType the downstream type of this IpServer.
+ * @param request the TetheringRequest this IpServer was enabled with.
* @param enabled enable or disable tethering.
*/
- public void requestEnableTethering(int tetheringType, boolean enabled) { }
+ public void requestEnableTethering(TetheringRequest request, boolean enabled) { }
}
/** Capture IpServer dependencies, for injection. */
@@ -240,15 +242,17 @@
private final BpfCoordinator mBpfCoordinator;
@NonNull
private final RoutingCoordinatorManager mRoutingCoordinator;
+ @NonNull
+ private final IIpv4PrefixRequest mIpv4PrefixRequest;
private final Callback mCallback;
private final InterfaceController mInterfaceCtrl;
- private final PrivateAddressCoordinator mPrivateAddressCoordinator;
private final String mIfaceName;
private final int mInterfaceType;
private final LinkProperties mLinkProperties;
private final boolean mUsingLegacyDhcp;
private final int mP2pLeasesSubnetPrefixLength;
+ private final boolean mIsWifiP2pDedicatedIpEnabled;
private final Dependencies mDeps;
@@ -289,6 +293,9 @@
private LinkAddress mIpv4Address;
+ @Nullable
+ private TetheringRequest mTetheringRequest;
+
private final TetheringMetrics mTetheringMetrics;
private final Handler mHandler;
@@ -298,7 +305,7 @@
String ifaceName, Handler handler, int interfaceType, SharedLog log,
INetd netd, @NonNull BpfCoordinator bpfCoordinator,
RoutingCoordinatorManager routingCoordinatorManager, Callback callback,
- TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
+ TetheringConfiguration config,
TetheringMetrics tetheringMetrics, Dependencies deps) {
super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
mHandler = handler;
@@ -306,6 +313,12 @@
mNetd = netd;
mBpfCoordinator = bpfCoordinator;
mRoutingCoordinator = routingCoordinatorManager;
+ mIpv4PrefixRequest = new IIpv4PrefixRequest.Stub() {
+ @Override
+ public void onIpv4PrefixConflict(IpPrefix ipPrefix) throws RemoteException {
+ sendMessage(CMD_NOTIFY_PREFIX_CONFLICT);
+ }
+ };
mCallback = callback;
mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
mIfaceName = ifaceName;
@@ -313,7 +326,7 @@
mLinkProperties = new LinkProperties();
mUsingLegacyDhcp = config.useLegacyDhcpServer();
mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
- mPrivateAddressCoordinator = addressCoordinator;
+ mIsWifiP2pDedicatedIpEnabled = config.shouldEnableWifiP2pDedicatedIp();
mDeps = deps;
mTetheringMetrics = tetheringMetrics;
resetLinkProperties();
@@ -391,6 +404,17 @@
return mInterfaceParams;
}
+ @VisibleForTesting
+ public IIpv4PrefixRequest getIpv4PrefixRequest() {
+ return mIpv4PrefixRequest;
+ }
+
+ /** The TetheringRequest the IpServer started with. */
+ @Nullable
+ public TetheringRequest getTetheringRequest() {
+ return mTetheringRequest;
+ }
+
/**
* Get the latest list of DHCP leases that was reported. Must be called on the IpServer looper
* thread.
@@ -639,7 +663,7 @@
// NOTE: All of configureIPv4() will be refactored out of existence
// into calls to InterfaceController, shared with startIPv4().
mInterfaceCtrl.clearIPv4Address();
- mPrivateAddressCoordinator.releaseDownstream(this);
+ mRoutingCoordinator.releaseDownstream(mIpv4PrefixRequest);
mBpfCoordinator.tetherOffloadClientClear(this);
mIpv4Address = null;
mStaticIpv4ServerAddr = null;
@@ -698,12 +722,24 @@
return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
}
+ private boolean shouldUseWifiP2pDedicatedIp() {
+ return mIsWifiP2pDedicatedIpEnabled
+ && mInterfaceType == TetheringManager.TETHERING_WIFI_P2P;
+ }
+
private LinkAddress requestIpv4Address(final int scope, final boolean useLastAddress) {
if (mStaticIpv4ServerAddr != null) return mStaticIpv4ServerAddr;
if (shouldNotConfigureBluetoothInterface()) return new LinkAddress(BLUETOOTH_IFACE_ADDR);
- return mPrivateAddressCoordinator.requestDownstreamAddress(this, scope, useLastAddress);
+ if (shouldUseWifiP2pDedicatedIp()) return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
+
+ if (useLastAddress) {
+ return mRoutingCoordinator.requestStickyDownstreamAddress(mInterfaceType, scope,
+ mIpv4PrefixRequest);
+ }
+
+ return mRoutingCoordinator.requestDownstreamAddress(mIpv4PrefixRequest);
}
private boolean startIPv6() {
@@ -1006,6 +1042,7 @@
switch (message.what) {
case CMD_TETHER_REQUESTED:
mLastError = TETHER_ERROR_NO_ERROR;
+ mTetheringRequest = (TetheringRequest) message.obj;
switch (message.arg1) {
case STATE_LOCAL_ONLY:
maybeConfigureStaticIp((TetheringRequest) message.obj);
@@ -1141,13 +1178,14 @@
handleNewPrefixRequest((IpPrefix) message.obj);
break;
case CMD_NOTIFY_PREFIX_CONFLICT:
- mLog.i("restart tethering: " + mInterfaceType);
- mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
+ mLog.i("restart tethering: " + mIfaceName);
+ mCallback.requestEnableTethering(mTetheringRequest, false /* enabled */);
transitionTo(mWaitingForRestartState);
break;
case CMD_SERVICE_FAILED_TO_START:
mLog.e("start serving fail, error: " + message.arg1);
transitionTo(mInitialState);
+ break;
default:
return false;
}
@@ -1393,8 +1431,28 @@
@Override
public void enter() {
mLastError = TETHER_ERROR_NO_ERROR;
+ // TODO: clean this up after the synchronous state machine is fully rolled out. Clean up
+ // can be directly triggered after calling IpServer.stop() inside Tethering.java.
sendInterfaceState(STATE_UNAVAILABLE);
}
+
+ @Override
+ public boolean processMessage(Message message) {
+ switch (message.what) {
+ case CMD_IPV6_TETHER_UPDATE:
+ // sendInterfaceState(STATE_UNAVAILABLE) triggers
+ // handleInterfaceServingStateInactive which in turn cleans up IPv6 tethering
+ // (and calls into IpServer one more time). At this point, this is the only
+ // message we potentially see in this state because this IpServer has already
+ // been removed from mTetherStates before transitioning to this State; however,
+ // handleInterfaceServiceStateInactive passes a reference.
+ // TODO: This can be removed once SyncStateMachine is rolled out and the
+ // teardown path is cleaned up.
+ return true;
+ default:
+ return false;
+ }
+ }
}
class WaitingForRestartState extends State {
@@ -1405,12 +1463,12 @@
case CMD_TETHER_UNREQUESTED:
transitionTo(mInitialState);
mLog.i("Untethered (unrequested) and restarting " + mIfaceName);
- mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
+ mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
break;
case CMD_INTERFACE_DOWN:
transitionTo(mUnavailableState);
mLog.i("Untethered (interface down) and restarting " + mIfaceName);
- mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
+ mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
break;
default:
return false;
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 5c853f4..75ab9ec 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -27,16 +27,19 @@
import static android.system.OsConstants.ETH_P_IP;
import static android.system.OsConstants.ETH_P_IPV6;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.net.module.util.NetworkStackConstants.IPV4_MIN_MTU;
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
import static com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ACTIVE_SESSIONS_METRICS;
import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
import static com.android.networkstack.tethering.util.TetheringUtils.getTetheringJniLibraryName;
import android.app.usage.NetworkStatsManager;
+import android.content.Context;
import android.net.INetd;
import android.net.IpPrefix;
import android.net.LinkProperties;
@@ -65,6 +68,7 @@
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetworkStackConstants;
@@ -84,6 +88,7 @@
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
import com.android.networkstack.tethering.util.TetheringUtils.ForwardedStats;
+import com.android.server.ConnectivityStatsLog;
import java.io.IOException;
import java.net.Inet4Address;
@@ -148,6 +153,13 @@
@VisibleForTesting
static final int CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS = 60_000;
+ // The interval is set to 5 minutes to strike a balance between minimizing
+ // the amount of metrics data uploaded and providing sufficient resolution
+ // to track changes in forwarding rules. This choice considers the minimum
+ // push metrics sampling interval of 5 minutes and the 3-minute timeout
+ // for forwarding rules.
+ @VisibleForTesting
+ static final int CONNTRACK_METRICS_UPDATE_INTERVAL_MS = 300_000;
@VisibleForTesting
static final int NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED = 432_000;
@VisibleForTesting
@@ -314,12 +326,22 @@
scheduleConntrackTimeoutUpdate();
};
+ private final boolean mSupportActiveSessionsMetrics;
+
+ // Runnable that used by scheduling next refreshing of conntrack metrics sampling.
+ private final Runnable mScheduledConntrackMetricsSampling = () -> {
+ uploadConntrackMetricsSample();
+ scheduleConntrackMetricsSampling();
+ };
+
// TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
- @VisibleForTesting
public abstract static class Dependencies {
/** Get handler. */
@NonNull public abstract Handler getHandler();
+ /** Get context. */
+ @NonNull public abstract Context getContext();
+
/** Get netd. */
@NonNull public abstract INetd getNetd();
@@ -472,6 +494,19 @@
return null;
}
}
+
+ /** Send a TetheringActiveSessionsReported event. */
+ public void sendTetheringActiveSessionsReported(int lastMaxSessionCount) {
+ ConnectivityStatsLog.write(ConnectivityStatsLog.TETHERING_ACTIVE_SESSIONS_REPORTED,
+ lastMaxSessionCount);
+ }
+
+ /**
+ * @see DeviceConfigUtils#isTetheringFeatureEnabled
+ */
+ public boolean isFeatureEnabled(Context context, String name) {
+ return DeviceConfigUtils.isTetheringFeatureEnabled(context, name);
+ }
}
@VisibleForTesting
@@ -508,32 +543,53 @@
if (!mBpfCoordinatorShim.isInitialized()) {
mLog.e("Bpf shim not initialized");
}
+
+ // BPF IPv4 forwarding only supports on S+.
+ mSupportActiveSessionsMetrics = mDeps.isAtLeastS()
+ && mDeps.isFeatureEnabled(mDeps.getContext(), TETHER_ACTIVE_SESSIONS_METRICS);
}
/**
- * Start BPF tethering offload stats and conntrack timeout polling.
+ * Start BPF tethering offload stats and conntrack polling.
* Note that this can be only called on handler thread.
*/
- private void startStatsAndConntrackTimeoutPolling() {
+ private void startStatsAndConntrackPolling() {
schedulePollingStats();
scheduleConntrackTimeoutUpdate();
+ if (mSupportActiveSessionsMetrics) {
+ scheduleConntrackMetricsSampling();
+ }
mLog.i("Polling started.");
}
/**
- * Stop BPF tethering offload stats and conntrack timeout polling.
+ * Stop BPF tethering offload stats and conntrack polling.
* The data limit cleanup and the tether stats maps cleanup are not implemented here.
* These cleanups rely on all IpServers calling #removeIpv6DownstreamRule. After the
* last rule is removed from the upstream, #removeIpv6DownstreamRule does the cleanup
* functionality.
* Note that this can be only called on handler thread.
*/
- private void stopStatsAndConntrackTimeoutPolling() {
+ private void stopStatsAndConntrackPolling() {
// Stop scheduled polling conntrack timeout.
if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) {
mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate);
}
+ // Stop scheduled polling conntrack metrics sampling and
+ // clear counters in case there is any counter unsync problem
+ // previously due to possible bpf failures.
+ // Normally this won't happen because all clients are cleared before
+ // reaching here. See IpServer.BaseServingState#exit().
+ if (mSupportActiveSessionsMetrics) {
+ if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) {
+ mHandler.removeCallbacks(mScheduledConntrackMetricsSampling);
+ }
+ // Avoid sending metrics when tethering is about to close.
+ // This leads to a missing final sample before disconnect
+ // but avoids possibly duplicating the last metric in the upload.
+ mBpfCoordinatorShim.clearConnectionCounters();
+ }
// Stop scheduled polling stats and poll the latest stats from BPF maps.
if (mHandler.hasCallbacks(mScheduledPollingStats)) {
mHandler.removeCallbacks(mScheduledPollingStats);
@@ -867,7 +923,7 @@
// Start monitoring and polling when the first IpServer is added.
if (mServedIpServers.isEmpty()) {
- startStatsAndConntrackTimeoutPolling();
+ startStatsAndConntrackPolling();
startConntrackMonitoring();
mIpNeighborMonitor.start();
mLog.i("Neighbor monitoring started.");
@@ -890,7 +946,7 @@
// Stop monitoring and polling when the last IpServer is removed.
if (mServedIpServers.isEmpty()) {
- stopStatsAndConntrackTimeoutPolling();
+ stopStatsAndConntrackPolling();
stopConntrackMonitoring();
mIpNeighborMonitor.stop();
mLog.i("Neighbor monitoring stopped.");
@@ -1300,6 +1356,9 @@
pw.increaseIndent();
dumpCounters(pw);
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("mSupportActiveSessionsMetrics: " + mSupportActiveSessionsMetrics);
}
private void dumpStats(@NonNull IndentingPrintWriter pw) {
@@ -2136,11 +2195,25 @@
maybeAddDevMap(upstreamIndex, tetherClient.downstreamIfindex);
maybeSetLimit(upstreamIndex);
- mBpfCoordinatorShim.tetherOffloadRuleAdd(UPSTREAM, upstream4Key, upstream4Value);
- mBpfCoordinatorShim.tetherOffloadRuleAdd(DOWNSTREAM, downstream4Key, downstream4Value);
+
+ final boolean addedUpstream = mBpfCoordinatorShim.tetherOffloadRuleAdd(
+ UPSTREAM, upstream4Key, upstream4Value);
+ final boolean addedDownstream = mBpfCoordinatorShim.tetherOffloadRuleAdd(
+ DOWNSTREAM, downstream4Key, downstream4Value);
+ if (addedUpstream != addedDownstream) {
+ Log.wtf(TAG, "The bidirectional rules should be added concurrently ("
+ + "upstream: " + addedUpstream
+ + ", downstream: " + addedDownstream + ")");
+ return;
+ }
}
}
+ @VisibleForTesting(visibility = PRIVATE)
+ public int getLastMaxConnectionAndResetToCurrent() {
+ return mBpfCoordinatorShim.getLastMaxConnectionAndResetToCurrent();
+ }
+
@VisibleForTesting
private class BpfNeighborEventConsumer implements NeighborEventConsumer {
public void accept(NeighborEvent e) {
@@ -2477,6 +2550,11 @@
});
}
+ private void uploadConntrackMetricsSample() {
+ mDeps.sendTetheringActiveSessionsReported(
+ mBpfCoordinatorShim.getLastMaxConnectionAndResetToCurrent());
+ }
+
private void schedulePollingStats() {
if (mHandler.hasCallbacks(mScheduledPollingStats)) {
mHandler.removeCallbacks(mScheduledPollingStats);
@@ -2494,6 +2572,15 @@
CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
}
+ private void scheduleConntrackMetricsSampling() {
+ if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) {
+ mHandler.removeCallbacks(mScheduledConntrackMetricsSampling);
+ }
+
+ mHandler.postDelayed(mScheduledConntrackMetricsSampling,
+ CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ }
+
// Return IPv6 downstream forwarding rule map. This is used for testing only.
// Note that this can be only called on handler thread.
@NonNull
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index b88b13b..fb16226 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -33,9 +33,12 @@
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.networkstack.apishim.ConstantsShim.ACTION_TETHER_UNSUPPORTED_CARRIER_UI;
import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_NOT_EXPORTED;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@@ -50,9 +53,13 @@
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.Settings;
import android.util.SparseIntArray;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.SharedLog;
@@ -85,7 +92,6 @@
// Indicate tethering is not supported by carrier.
private static final int TETHERING_PROVISIONING_CARRIER_UNSUPPORT = 1002;
- private final ComponentName mSilentProvisioningService;
private static final int MS_PER_HOUR = 60 * 60 * 1000;
private static final int DUMP_TIMEOUT = 10_000;
@@ -109,9 +115,120 @@
private boolean mNeedReRunProvisioningUi = false;
private OnTetherProvisioningFailedListener mListener;
private TetheringConfigurationFetcher mFetcher;
+ private final Dependencies mDeps;
+
+ @VisibleForTesting(visibility = PRIVATE)
+ static class Dependencies {
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final SharedLog mLog;
+ private final ComponentName mSilentProvisioningService;
+
+ Dependencies(@NonNull Context context, @NonNull SharedLog log) {
+ mContext = context;
+ mLog = log;
+ mSilentProvisioningService = ComponentName.unflattenFromString(
+ mContext.getResources().getString(R.string.config_wifi_tether_enable));
+ }
+
+ /**
+ * Run the UI-enabled tethering provisioning check.
+ * @param type tethering type from TetheringManager.TETHERING_{@code *}
+ * @param receiver to receive entitlement check result.
+ *
+ * @return the broadcast intent, or null if the current user is not allowed to
+ * perform entitlement check.
+ */
+ @Nullable
+ protected Intent runUiTetherProvisioning(int type, final TetheringConfiguration config,
+ ResultReceiver receiver) {
+ if (DBG) mLog.i("runUiTetherProvisioning: " + type);
+
+ Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING_UI);
+ intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+ intent.putExtra(EXTRA_TETHER_UI_PROVISIONING_APP_NAME, config.provisioningApp);
+ intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
+ intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Only launch entitlement UI for the current user if it is allowed to
+ // change tethering. This usually means the system user or the admin users in HSUM.
+ if (SdkLevel.isAtLeastT()) {
+ // Create a user context for the current foreground user as UserManager#isAdmin()
+ // operates on the context user.
+ final int currentUserId = getCurrentUser();
+ final UserHandle currentUser = UserHandle.of(currentUserId);
+ final Context userContext = mContext.createContextAsUser(currentUser, 0);
+ final UserManager userManager = userContext.getSystemService(UserManager.class);
+
+ if (userManager.isAdminUser()) {
+ mContext.startActivityAsUser(intent, currentUser);
+ } else {
+ mLog.e("Current user (" + currentUserId
+ + ") is not allowed to perform entitlement check.");
+ // If the user is not allowed to perform an entitlement check
+ // (e.g., a non-admin user), notify the receiver immediately.
+ // This is necessary because the entitlement check app cannot
+ // be launched to conduct the check and deliver the results.
+ receiver.send(TETHER_ERROR_PROVISIONING_FAILED, null);
+ return null;
+ }
+ } else {
+ // For T- devices, there is no other admin user other than the system user.
+ mContext.startActivity(intent);
+ }
+ return intent;
+ }
+
+ /**
+ * Run no UI tethering provisioning check.
+ * @param type tethering type from TetheringManager.TETHERING_{@code *}
+ */
+ protected Intent runSilentTetherProvisioning(
+ int type, final TetheringConfiguration config, ResultReceiver receiver) {
+ if (DBG) mLog.i("runSilentTetherProvisioning: " + type);
+
+ Intent intent = new Intent();
+ intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+ intent.putExtra(EXTRA_RUN_PROVISION, true);
+ intent.putExtra(EXTRA_TETHER_SILENT_PROVISIONING_ACTION, config.provisioningAppNoUi);
+ intent.putExtra(EXTRA_TETHER_PROVISIONING_RESPONSE, config.provisioningResponse);
+ intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
+ intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
+ intent.setComponent(mSilentProvisioningService);
+ // Only admin user can change tethering and SilentTetherProvisioning don't need to
+ // show UI, it is fine to always start setting's background service as system user.
+ mContext.startService(intent);
+ return intent;
+ }
+
+ /**
+ * Create a PendingIntent for the provisioning recheck alarm.
+ * @param pkgName the package name of the PendingIntent.
+ */
+ PendingIntent createRecheckAlarmIntent(final String pkgName) {
+ final Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
+ intent.setPackage(pkgName);
+ return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
+ }
+
+ /**
+ * Get the current user id.
+ */
+ int getCurrentUser() {
+ return ActivityManager.getCurrentUser();
+ }
+ }
public EntitlementManager(Context ctx, Handler h, SharedLog log,
Runnable callback) {
+ this(ctx, h, log, callback, new Dependencies(ctx, log));
+ }
+
+ @VisibleForTesting(visibility = PRIVATE)
+ EntitlementManager(Context ctx, Handler h, SharedLog log,
+ Runnable callback, @NonNull Dependencies deps) {
mContext = ctx;
mLog = log.forSubComponent(TAG);
mCurrentDownstreams = new BitSet();
@@ -120,6 +237,7 @@
mEntitlementCacheValue = new SparseIntArray();
mPermissionChangeCallback = callback;
mHandler = h;
+ mDeps = deps;
if (SdkLevel.isAtLeastU()) {
mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_PROVISIONING_ALARM),
null, mHandler, RECEIVER_NOT_EXPORTED);
@@ -127,8 +245,6 @@
mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_PROVISIONING_ALARM),
null, mHandler);
}
- mSilentProvisioningService = ComponentName.unflattenFromString(
- mContext.getResources().getString(R.string.config_wifi_tether_enable));
}
public void setOnTetherProvisioningFailedListener(
@@ -382,53 +498,6 @@
}
}
- /**
- * Run no UI tethering provisioning check.
- * @param type tethering type from TetheringManager.TETHERING_{@code *}
- * @param subId default data subscription ID.
- */
- @VisibleForTesting
- protected Intent runSilentTetherProvisioning(
- int type, final TetheringConfiguration config, ResultReceiver receiver) {
- if (DBG) mLog.i("runSilentTetherProvisioning: " + type);
-
- Intent intent = new Intent();
- intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
- intent.putExtra(EXTRA_RUN_PROVISION, true);
- intent.putExtra(EXTRA_TETHER_SILENT_PROVISIONING_ACTION, config.provisioningAppNoUi);
- intent.putExtra(EXTRA_TETHER_PROVISIONING_RESPONSE, config.provisioningResponse);
- intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
- intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
- intent.setComponent(mSilentProvisioningService);
- // Only admin user can change tethering and SilentTetherProvisioning don't need to
- // show UI, it is fine to always start setting's background service as system user.
- mContext.startService(intent);
- return intent;
- }
-
- /**
- * Run the UI-enabled tethering provisioning check.
- * @param type tethering type from TetheringManager.TETHERING_{@code *}
- * @param subId default data subscription ID.
- * @param receiver to receive entitlement check result.
- */
- @VisibleForTesting
- protected Intent runUiTetherProvisioning(int type, final TetheringConfiguration config,
- ResultReceiver receiver) {
- if (DBG) mLog.i("runUiTetherProvisioning: " + type);
-
- Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING_UI);
- intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
- intent.putExtra(EXTRA_TETHER_UI_PROVISIONING_APP_NAME, config.provisioningApp);
- intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
- intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // Only launch entitlement UI for system user. Entitlement UI should not appear for other
- // user because only admin user is allowed to change tethering.
- mContext.startActivity(intent);
- return intent;
- }
-
private void runTetheringProvisioning(
boolean showProvisioningUi, int downstreamType, final TetheringConfiguration config) {
if (!config.isCarrierSupportTethering) {
@@ -442,9 +511,9 @@
ResultReceiver receiver =
buildProxyReceiver(downstreamType, showProvisioningUi/* notifyFail */, null);
if (showProvisioningUi) {
- runUiTetherProvisioning(downstreamType, config, receiver);
+ mDeps.runUiTetherProvisioning(downstreamType, config, receiver);
} else {
- runSilentTetherProvisioning(downstreamType, config, receiver);
+ mDeps.runSilentTetherProvisioning(downstreamType, config, receiver);
}
}
@@ -458,20 +527,13 @@
mContext.startActivity(intent);
}
- @VisibleForTesting
- PendingIntent createRecheckAlarmIntent(final String pkgName) {
- final Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
- intent.setPackage(pkgName);
- return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
- }
-
// Not needed to check if this don't run on the handler thread because it's private.
private void scheduleProvisioningRecheck(final TetheringConfiguration config) {
if (mProvisioningRecheckAlarm == null) {
final int period = config.provisioningCheckPeriod;
if (period <= 0) return;
- mProvisioningRecheckAlarm = createRecheckAlarmIntent(mContext.getPackageName());
+ mProvisioningRecheckAlarm = mDeps.createRecheckAlarmIntent(mContext.getPackageName());
AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
Context.ALARM_SERVICE);
long triggerAtMillis = SystemClock.elapsedRealtime() + (period * MS_PER_HOUR);
@@ -697,7 +759,7 @@
receiver.send(cacheValue, null);
} else {
ResultReceiver proxy = buildProxyReceiver(downstream, false/* notifyFail */, receiver);
- runUiTetherProvisioning(downstream, config, proxy);
+ mDeps.runUiTetherProvisioning(downstream, config, proxy);
}
}
}
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
deleted file mode 100644
index 528991f..0000000
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ /dev/null
@@ -1,477 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.networkstack.tethering;
-
-import static android.net.NetworkCapabilities.TRANSPORT_VPN;
-import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
-import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
-import static android.net.TetheringManager.TETHERING_BLUETOOTH;
-import static android.net.TetheringManager.TETHERING_WIFI_P2P;
-
-import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
-import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
-import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTH;
-import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
-
-import static java.util.Arrays.asList;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.IpPrefix;
-import android.net.LinkAddress;
-import android.net.Network;
-import android.net.ip.IpServer;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.IndentingPrintWriter;
-
-import java.net.Inet4Address;
-import java.net.InetAddress;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Random;
-import java.util.Set;
-
-/**
- * This class coordinate IP addresses conflict problem.
- *
- * Tethering downstream IP addresses may conflict with network assigned addresses. This
- * coordinator is responsible for recording all of network assigned addresses and dispatched
- * free address to downstream interfaces.
- *
- * This class is not thread-safe and should be accessed on the same tethering internal thread.
- * @hide
- */
-public class PrivateAddressCoordinator {
- public static final int PREFIX_LENGTH = 24;
-
- // Upstream monitor would be stopped when tethering is down. When tethering restart, downstream
- // address may be requested before coordinator get current upstream notification. To ensure
- // coordinator do not select conflict downstream prefix, mUpstreamPrefixMap would not be cleared
- // when tethering is down. Instead tethering would remove all deprecated upstreams from
- // mUpstreamPrefixMap when tethering is starting. See #maybeRemoveDeprecatedUpstreams().
- private final ArrayMap<Network, List<IpPrefix>> mUpstreamPrefixMap;
- private final ArraySet<IpServer> mDownstreams;
- private static final String LEGACY_WIFI_P2P_IFACE_ADDRESS = "192.168.49.1/24";
- private static final String LEGACY_BLUETOOTH_IFACE_ADDRESS = "192.168.44.1/24";
- private final List<IpPrefix> mTetheringPrefixes;
- private final ConnectivityManager mConnectivityMgr;
- private final TetheringConfiguration mConfig;
- // keyed by downstream type(TetheringManager.TETHERING_*).
- private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
- private final Random mRandom;
-
- public PrivateAddressCoordinator(Context context, TetheringConfiguration config) {
- mDownstreams = new ArraySet<>();
- mUpstreamPrefixMap = new ArrayMap<>();
- mConnectivityMgr = (ConnectivityManager) context.getSystemService(
- Context.CONNECTIVITY_SERVICE);
- mConfig = config;
- mCachedAddresses = new ArrayMap<AddressKey, LinkAddress>();
- // Reserved static addresses for bluetooth and wifi p2p.
- mCachedAddresses.put(new AddressKey(TETHERING_BLUETOOTH, CONNECTIVITY_SCOPE_GLOBAL),
- new LinkAddress(LEGACY_BLUETOOTH_IFACE_ADDRESS));
- mCachedAddresses.put(new AddressKey(TETHERING_WIFI_P2P, CONNECTIVITY_SCOPE_LOCAL),
- new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS));
-
- mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16"),
- new IpPrefix("172.16.0.0/12"), new IpPrefix("10.0.0.0/8")));
- mRandom = new Random();
- }
-
- /**
- * Record a new upstream IpPrefix which may conflict with tethering downstreams.
- * The downstreams will be notified if a conflict is found. When updateUpstreamPrefix is called,
- * UpstreamNetworkState must have an already populated LinkProperties.
- */
- public void updateUpstreamPrefix(final UpstreamNetworkState ns) {
- // Do not support VPN as upstream. Normally, networkCapabilities is not expected to be null,
- // but just checking to be sure.
- if (ns.networkCapabilities != null && ns.networkCapabilities.hasTransport(TRANSPORT_VPN)) {
- removeUpstreamPrefix(ns.network);
- return;
- }
-
- final ArrayList<IpPrefix> ipv4Prefixes = getIpv4Prefixes(
- ns.linkProperties.getAllLinkAddresses());
- if (ipv4Prefixes.isEmpty()) {
- removeUpstreamPrefix(ns.network);
- return;
- }
-
- mUpstreamPrefixMap.put(ns.network, ipv4Prefixes);
- handleMaybePrefixConflict(ipv4Prefixes);
- }
-
- private ArrayList<IpPrefix> getIpv4Prefixes(final List<LinkAddress> linkAddresses) {
- final ArrayList<IpPrefix> list = new ArrayList<>();
- for (LinkAddress address : linkAddresses) {
- if (!address.isIpv4()) continue;
-
- list.add(asIpPrefix(address));
- }
-
- return list;
- }
-
- private void handleMaybePrefixConflict(final List<IpPrefix> prefixes) {
- for (IpServer downstream : mDownstreams) {
- final IpPrefix target = getDownstreamPrefix(downstream);
-
- for (IpPrefix source : prefixes) {
- if (isConflictPrefix(source, target)) {
- downstream.sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- break;
- }
- }
- }
- }
-
- /** Remove IpPrefix records corresponding to input network. */
- public void removeUpstreamPrefix(final Network network) {
- mUpstreamPrefixMap.remove(network);
- }
-
- /**
- * Maybe remove deprecated upstream records, this would be called once tethering started without
- * any exiting tethered downstream.
- */
- public void maybeRemoveDeprecatedUpstreams() {
- if (mUpstreamPrefixMap.isEmpty()) return;
-
- // Remove all upstreams that are no longer valid networks
- final Set<Network> toBeRemoved = new HashSet<>(mUpstreamPrefixMap.keySet());
- toBeRemoved.removeAll(asList(mConnectivityMgr.getAllNetworks()));
-
- mUpstreamPrefixMap.removeAll(toBeRemoved);
- }
-
- /**
- * Pick a random available address and mark its prefix as in use for the provided IpServer,
- * returns null if there is no available address.
- */
- @Nullable
- public LinkAddress requestDownstreamAddress(final IpServer ipServer, final int scope,
- boolean useLastAddress) {
- if (mConfig.shouldEnableWifiP2pDedicatedIp()
- && ipServer.interfaceType() == TETHERING_WIFI_P2P) {
- return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
- }
-
- final AddressKey addrKey = new AddressKey(ipServer.interfaceType(), scope);
- // This ensures that tethering isn't started on 2 different interfaces with the same type.
- // Once tethering could support multiple interface with the same type,
- // TetheringSoftApCallback would need to handle it among others.
- final LinkAddress cachedAddress = mCachedAddresses.get(addrKey);
- if (useLastAddress && cachedAddress != null
- && !isConflictWithUpstream(asIpPrefix(cachedAddress))) {
- mDownstreams.add(ipServer);
- return cachedAddress;
- }
-
- final int prefixIndex = getStartedPrefixIndex();
- for (int i = 0; i < mTetheringPrefixes.size(); i++) {
- final IpPrefix prefixRange = mTetheringPrefixes.get(
- (prefixIndex + i) % mTetheringPrefixes.size());
- final LinkAddress newAddress = chooseDownstreamAddress(prefixRange);
- if (newAddress != null) {
- mDownstreams.add(ipServer);
- mCachedAddresses.put(addrKey, newAddress);
- return newAddress;
- }
- }
-
- // No available address.
- return null;
- }
-
- private int getStartedPrefixIndex() {
- if (!mConfig.isRandomPrefixBaseEnabled()) return 0;
-
- final int random = getRandomInt() & 0xffffff;
- // This is to select the starting prefix range (/8, /12, or /16) instead of the actual
- // LinkAddress. To avoid complex operations in the selection logic and make the selected
- // rate approximate consistency with that /8 is around 2^4 times of /12 and /12 is around
- // 2^4 times of /16, we simply define a map between the value and the prefix value like
- // this:
- //
- // Value 0 ~ 0xffff (65536/16777216 = 0.39%) -> 192.168.0.0/16
- // Value 0x10000 ~ 0xfffff (983040/16777216 = 5.86%) -> 172.16.0.0/12
- // Value 0x100000 ~ 0xffffff (15728640/16777216 = 93.7%) -> 10.0.0.0/8
- if (random > 0xfffff) {
- return 2;
- } else if (random > 0xffff) {
- return 1;
- } else {
- return 0;
- }
- }
-
- private int getPrefixBaseAddress(final IpPrefix prefix) {
- return inet4AddressToIntHTH((Inet4Address) prefix.getAddress());
- }
-
- /**
- * Check whether input prefix conflict with upstream prefixes or in-use downstream prefixes.
- * If yes, return one of them.
- */
- private IpPrefix getConflictPrefix(final IpPrefix prefix) {
- final IpPrefix upstream = getConflictWithUpstream(prefix);
- if (upstream != null) return upstream;
-
- return getInUseDownstreamPrefix(prefix);
- }
-
- // Get the next non-conflict sub prefix. E.g: To get next sub prefix from 10.0.0.0/8, if the
- // previously selected prefix is 10.20.42.0/24(subPrefix: 0.20.42.0) and the conflicting prefix
- // is 10.16.0.0/20 (10.16.0.0 ~ 10.16.15.255), then the max address under subPrefix is
- // 0.16.15.255 and the next subPrefix is 0.16.16.255/24 (0.16.15.255 + 0.0.1.0).
- // Note: the sub address 0.0.0.255 here is fine to be any value that it will be replaced as
- // selected random sub address later.
- private int getNextSubPrefix(final IpPrefix conflictPrefix, final int prefixRangeMask) {
- final int suffixMask = ~prefixLengthToV4NetmaskIntHTH(conflictPrefix.getPrefixLength());
- // The largest offset within the prefix assignment block that still conflicts with
- // conflictPrefix.
- final int maxConflict =
- (getPrefixBaseAddress(conflictPrefix) | suffixMask) & ~prefixRangeMask;
-
- final int prefixMask = prefixLengthToV4NetmaskIntHTH(PREFIX_LENGTH);
- // Pick a sub prefix a full prefix (1 << (32 - PREFIX_LENGTH) addresses) greater than
- // maxConflict. This ensures that the selected prefix never overlaps with conflictPrefix.
- // There is no need to mask the result with PREFIX_LENGTH bits because this is done by
- // findAvailablePrefixFromRange when it constructs the prefix.
- return maxConflict + (1 << (32 - PREFIX_LENGTH));
- }
-
- private LinkAddress chooseDownstreamAddress(final IpPrefix prefixRange) {
- // The netmask of the prefix assignment block (e.g., 0xfff00000 for 172.16.0.0/12).
- final int prefixRangeMask = prefixLengthToV4NetmaskIntHTH(prefixRange.getPrefixLength());
-
- // The zero address in the block (e.g., 0xac100000 for 172.16.0.0/12).
- final int baseAddress = getPrefixBaseAddress(prefixRange);
-
- // The subnet mask corresponding to PREFIX_LENGTH.
- final int prefixMask = prefixLengthToV4NetmaskIntHTH(PREFIX_LENGTH);
-
- // The offset within prefixRange of a randomly-selected prefix of length PREFIX_LENGTH.
- // This may not be the prefix of the address returned by this method:
- // - If it is already in use, the method will return an address in another prefix.
- // - If all prefixes within prefixRange are in use, the method will return null. For
- // example, for a /24 prefix within 172.26.0.0/12, this will be a multiple of 256 in
- // [0, 1048576). In other words, a random 32-bit number with mask 0x000fff00.
- //
- // prefixRangeMask is required to ensure no wrapping. For example, consider:
- // - prefixRange 127.0.0.0/8
- // - randomPrefixStart 127.255.255.0
- // - A conflicting prefix of 127.255.254.0/23
- // In this case without prefixRangeMask, getNextSubPrefix would return 128.0.0.0, which
- // means the "start < end" check in findAvailablePrefixFromRange would not reject the prefix
- // because Java doesn't have unsigned integers, so 128.0.0.0 = 0x80000000 = -2147483648
- // is less than 127.0.0.0 = 0x7f000000 = 2130706432.
- //
- // Additionally, it makes debug output easier to read by making the numbers smaller.
- final int randomInt = getRandomInt();
- final int randomPrefixStart = randomInt & ~prefixRangeMask & prefixMask;
-
- // A random offset within the prefix. Used to determine the local address once the prefix
- // is selected. It does not result in an IPv4 address ending in .0, .1, or .255
- // For a PREFIX_LENGTH of 24, this is a number between 2 and 254.
- final int subAddress = getSanitizedSubAddr(randomInt, ~prefixMask);
-
- // Find a prefix length PREFIX_LENGTH between randomPrefixStart and the end of the block,
- // such that the prefix does not conflict with any upstream.
- IpPrefix downstreamPrefix = findAvailablePrefixFromRange(
- randomPrefixStart, (~prefixRangeMask) + 1, baseAddress, prefixRangeMask);
- if (downstreamPrefix != null) return getLinkAddress(downstreamPrefix, subAddress);
-
- // If that failed, do the same, but between 0 and randomPrefixStart.
- downstreamPrefix = findAvailablePrefixFromRange(
- 0, randomPrefixStart, baseAddress, prefixRangeMask);
-
- return getLinkAddress(downstreamPrefix, subAddress);
- }
-
- private LinkAddress getLinkAddress(final IpPrefix prefix, final int subAddress) {
- if (prefix == null) return null;
-
- final InetAddress address = intToInet4AddressHTH(getPrefixBaseAddress(prefix) | subAddress);
- return new LinkAddress(address, PREFIX_LENGTH);
- }
-
- private IpPrefix findAvailablePrefixFromRange(final int start, final int end,
- final int baseAddress, final int prefixRangeMask) {
- int newSubPrefix = start;
- while (newSubPrefix < end) {
- final InetAddress address = intToInet4AddressHTH(baseAddress | newSubPrefix);
- final IpPrefix prefix = new IpPrefix(address, PREFIX_LENGTH);
-
- final IpPrefix conflictPrefix = getConflictPrefix(prefix);
-
- if (conflictPrefix == null) return prefix;
-
- newSubPrefix = getNextSubPrefix(conflictPrefix, prefixRangeMask);
- }
-
- return null;
- }
-
- /** Get random int which could be used to generate random address. */
- @VisibleForTesting
- public int getRandomInt() {
- return mRandom.nextInt();
- }
-
- /** Get random subAddress and avoid selecting x.x.x.0, x.x.x.1 and x.x.x.255 address. */
- private int getSanitizedSubAddr(final int randomInt, final int subAddrMask) {
- final int randomSubAddr = randomInt & subAddrMask;
- // If prefix length > 30, the selecting speace would be less than 4 which may be hard to
- // avoid 3 consecutive address.
- if (PREFIX_LENGTH > 30) return randomSubAddr;
-
- // TODO: maybe it is not necessary to avoid .0, .1 and .255 address because tethering
- // address would not be conflicted. This code only works because PREFIX_LENGTH is not longer
- // than 24
- final int candidate = randomSubAddr & 0xff;
- if (candidate == 0 || candidate == 1 || candidate == 255) {
- return (randomSubAddr & 0xfffffffc) + 2;
- }
-
- return randomSubAddr;
- }
-
- /** Release downstream record for IpServer. */
- public void releaseDownstream(final IpServer ipServer) {
- mDownstreams.remove(ipServer);
- }
-
- /** Clear current upstream prefixes records. */
- public void clearUpstreamPrefixes() {
- mUpstreamPrefixMap.clear();
- }
-
- private IpPrefix getConflictWithUpstream(final IpPrefix prefix) {
- for (int i = 0; i < mUpstreamPrefixMap.size(); i++) {
- final List<IpPrefix> list = mUpstreamPrefixMap.valueAt(i);
- for (IpPrefix upstream : list) {
- if (isConflictPrefix(prefix, upstream)) return upstream;
- }
- }
- return null;
- }
-
- private boolean isConflictWithUpstream(final IpPrefix prefix) {
- return getConflictWithUpstream(prefix) != null;
- }
-
- private boolean isConflictPrefix(final IpPrefix prefix1, final IpPrefix prefix2) {
- if (prefix2.getPrefixLength() < prefix1.getPrefixLength()) {
- return prefix2.contains(prefix1.getAddress());
- }
-
- return prefix1.contains(prefix2.getAddress());
- }
-
- // InUse Prefixes are prefixes of mCachedAddresses which are active downstream addresses, last
- // downstream addresses(reserved for next time) and static addresses(e.g. bluetooth, wifi p2p).
- private IpPrefix getInUseDownstreamPrefix(final IpPrefix prefix) {
- for (int i = 0; i < mCachedAddresses.size(); i++) {
- final IpPrefix downstream = asIpPrefix(mCachedAddresses.valueAt(i));
- if (isConflictPrefix(prefix, downstream)) return downstream;
- }
-
- // IpServer may use manually-defined address (mStaticIpv4ServerAddr) which does not include
- // in mCachedAddresses.
- for (IpServer downstream : mDownstreams) {
- final IpPrefix target = getDownstreamPrefix(downstream);
-
- if (isConflictPrefix(prefix, target)) return target;
- }
-
- return null;
- }
-
- @NonNull
- private IpPrefix getDownstreamPrefix(final IpServer downstream) {
- final LinkAddress address = downstream.getAddress();
-
- return asIpPrefix(address);
- }
-
- private static class AddressKey {
- private final int mTetheringType;
- private final int mScope;
-
- private AddressKey(int type, int scope) {
- mTetheringType = type;
- mScope = scope;
- }
-
- @Override
- public int hashCode() {
- return (mTetheringType << 16) + mScope;
- }
-
- @Override
- public boolean equals(@Nullable Object obj) {
- if (!(obj instanceof AddressKey)) return false;
- final AddressKey other = (AddressKey) obj;
-
- return mTetheringType == other.mTetheringType && mScope == other.mScope;
- }
-
- @Override
- public String toString() {
- return "AddressKey(" + mTetheringType + ", " + mScope + ")";
- }
- }
-
- void dump(final IndentingPrintWriter pw) {
- pw.println("mTetheringPrefixes:");
- pw.increaseIndent();
- for (IpPrefix prefix : mTetheringPrefixes) {
- pw.println(prefix);
- }
- pw.decreaseIndent();
-
- pw.println("mUpstreamPrefixMap:");
- pw.increaseIndent();
- for (int i = 0; i < mUpstreamPrefixMap.size(); i++) {
- pw.println(mUpstreamPrefixMap.keyAt(i) + " - " + mUpstreamPrefixMap.valueAt(i));
- }
- pw.decreaseIndent();
-
- pw.println("mDownstreams:");
- pw.increaseIndent();
- for (IpServer ipServer : mDownstreams) {
- pw.println(ipServer.interfaceType() + " - " + ipServer.getAddress());
- }
- pw.decreaseIndent();
-
- pw.println("mCachedAddresses:");
- pw.increaseIndent();
- for (int i = 0; i < mCachedAddresses.size(); i++) {
- pw.println(mCachedAddresses.keyAt(i) + " - " + mCachedAddresses.valueAt(i));
- }
- pw.decreaseIndent();
- }
-}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index d62f18f..f33ef37 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -109,6 +109,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
@@ -216,10 +217,12 @@
* Cookie added when registering {@link android.net.TetheringManager.TetheringEventCallback}.
*/
private static class CallbackCookie {
- public final boolean hasListClientsPermission;
+ public final int uid;
+ public final boolean hasSystemPrivilege;
- private CallbackCookie(boolean hasListClientsPermission) {
- this.hasListClientsPermission = hasListClientsPermission;
+ private CallbackCookie(int uid, boolean hasSystemPrivilege) {
+ this.uid = uid;
+ this.hasSystemPrivilege = hasSystemPrivilege;
}
}
@@ -253,7 +256,6 @@
private final TetheringNotificationUpdater mNotificationUpdater;
private final UserManager mUserManager;
private final BpfCoordinator mBpfCoordinator;
- private final PrivateAddressCoordinator mPrivateAddressCoordinator;
private final TetheringMetrics mTetheringMetrics;
private final WearableConnectionManager mWearableConnectionManager;
private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
@@ -264,7 +266,6 @@
private boolean mRndisEnabled; // track the RNDIS function enabled state
private boolean mNcmEnabled; // track the NCM function enabled state
private Network mTetherUpstream;
- private TetherStatesParcel mTetherStatesParcel;
private boolean mDataSaverEnabled = false;
private String mWifiP2pTetherInterface = null;
private int mOffloadStatus = TETHER_HARDWARE_OFFLOAD_STOPPED;
@@ -359,10 +360,6 @@
// Load tethering configuration.
updateConfiguration();
mConfig.readEnableSyncSM(mContext);
- // It is OK for the configuration to be passed to the PrivateAddressCoordinator at
- // construction time because the only part of the configuration it uses is
- // shouldEnableWifiP2pDedicatedIp(), and currently do not support changing that.
- mPrivateAddressCoordinator = mDeps.makePrivateAddressCoordinator(mContext, mConfig);
// Must be initialized after tethering configuration is loaded because BpfCoordinator
// constructor needs to use the configuration.
@@ -374,6 +371,11 @@
}
@NonNull
+ public Context getContext() {
+ return mContext;
+ }
+
+ @NonNull
public INetd getNetd() {
return mNetd;
}
@@ -1091,21 +1093,55 @@
}
// TODO: Figure out how to update for local hotspot mode interfaces.
- private void sendTetherStateChangedBroadcast() {
+ private void notifyTetherStatesChanged() {
if (!isTetheringSupported()) return;
+ sendTetherStatesChangedCallback();
+ sendTetherStatesChangedBroadcast();
+
+ int downstreamTypesMask = DOWNSTREAM_NONE;
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ final TetherState tetherState = mTetherStates.valueAt(i);
+ final int type = tetherState.ipServer.interfaceType();
+ if (tetherState.lastState != IpServer.STATE_TETHERED) continue;
+ switch (type) {
+ case TETHERING_USB:
+ case TETHERING_WIFI:
+ case TETHERING_BLUETOOTH:
+ downstreamTypesMask |= (1 << type);
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ }
+ mNotificationUpdater.onDownstreamChanged(downstreamTypesMask);
+ }
+
+ /**
+ * Builds a TetherStatesParcel for the specified CallbackCookie. SoftApConfiguration will only
+ * be included if the cookie has the same uid as the app that specified the configuration, or
+ * if the cookie has system privilege.
+ *
+ * @param cookie CallbackCookie of the receiving app.
+ * @return TetherStatesParcel with information redacted for the specified cookie.
+ */
+ private TetherStatesParcel buildTetherStatesParcel(CallbackCookie cookie) {
final ArrayList<TetheringInterface> available = new ArrayList<>();
final ArrayList<TetheringInterface> tethered = new ArrayList<>();
final ArrayList<TetheringInterface> localOnly = new ArrayList<>();
final ArrayList<TetheringInterface> errored = new ArrayList<>();
final ArrayList<Integer> lastErrors = new ArrayList<>();
- int downstreamTypesMask = DOWNSTREAM_NONE;
for (int i = 0; i < mTetherStates.size(); i++) {
final TetherState tetherState = mTetherStates.valueAt(i);
final int type = tetherState.ipServer.interfaceType();
final String iface = mTetherStates.keyAt(i);
- final TetheringInterface tetheringIface = new TetheringInterface(type, iface);
+ final TetheringRequest request = tetherState.ipServer.getTetheringRequest();
+ final boolean includeSoftApConfig = request != null && cookie != null
+ && (cookie.uid == request.getUid() || cookie.hasSystemPrivilege);
+ final TetheringInterface tetheringIface = new TetheringInterface(type, iface,
+ includeSoftApConfig ? request.getSoftApConfiguration() : null);
if (tetherState.lastError != TETHER_ERROR_NO_ERROR) {
errored.add(tetheringIface);
lastErrors.add(tetherState.lastError);
@@ -1118,41 +1154,16 @@
case TETHERING_USB:
case TETHERING_WIFI:
case TETHERING_BLUETOOTH:
- downstreamTypesMask |= (1 << type);
break;
default:
// Do nothing.
+ break;
}
tethered.add(tetheringIface);
}
}
- mTetherStatesParcel = buildTetherStatesParcel(available, localOnly, tethered, errored,
- lastErrors);
- reportTetherStateChanged(mTetherStatesParcel);
-
- mContext.sendStickyBroadcastAsUser(buildStateChangeIntent(available, localOnly, tethered,
- errored), UserHandle.ALL);
- if (DBG) {
- Log.d(TAG, String.format(
- "reportTetherStateChanged %s=[%s] %s=[%s] %s=[%s] %s=[%s]",
- "avail", TextUtils.join(",", available),
- "local_only", TextUtils.join(",", localOnly),
- "tether", TextUtils.join(",", tethered),
- "error", TextUtils.join(",", errored)));
- }
-
- mNotificationUpdater.onDownstreamChanged(downstreamTypesMask);
- }
-
- private TetherStatesParcel buildTetherStatesParcel(
- final ArrayList<TetheringInterface> available,
- final ArrayList<TetheringInterface> localOnly,
- final ArrayList<TetheringInterface> tethered,
- final ArrayList<TetheringInterface> errored,
- final ArrayList<Integer> lastErrors) {
final TetherStatesParcel parcel = new TetherStatesParcel();
-
parcel.availableList = available.toArray(new TetheringInterface[0]);
parcel.tetheredList = tethered.toArray(new TetheringInterface[0]);
parcel.localOnlyList = localOnly.toArray(new TetheringInterface[0]);
@@ -1161,23 +1172,23 @@
for (int i = 0; i < lastErrors.size(); i++) {
parcel.lastErrorList[i] = lastErrors.get(i);
}
-
return parcel;
}
- private Intent buildStateChangeIntent(final ArrayList<TetheringInterface> available,
- final ArrayList<TetheringInterface> localOnly,
- final ArrayList<TetheringInterface> tethered,
- final ArrayList<TetheringInterface> errored) {
+ private void sendTetherStatesChangedBroadcast() {
final Intent bcast = new Intent(ACTION_TETHER_STATE_CHANGED);
bcast.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
- bcast.putStringArrayListExtra(EXTRA_AVAILABLE_TETHER, toIfaces(available));
- bcast.putStringArrayListExtra(EXTRA_ACTIVE_LOCAL_ONLY, toIfaces(localOnly));
- bcast.putStringArrayListExtra(EXTRA_ACTIVE_TETHER, toIfaces(tethered));
- bcast.putStringArrayListExtra(EXTRA_ERRORED_TETHER, toIfaces(errored));
-
- return bcast;
+ TetherStatesParcel parcel = buildTetherStatesParcel(null /* cookie */);
+ bcast.putStringArrayListExtra(
+ EXTRA_AVAILABLE_TETHER, toIfaces(Arrays.asList(parcel.availableList)));
+ bcast.putStringArrayListExtra(
+ EXTRA_ACTIVE_LOCAL_ONLY, toIfaces(Arrays.asList(parcel.localOnlyList)));
+ bcast.putStringArrayListExtra(
+ EXTRA_ACTIVE_TETHER, toIfaces(Arrays.asList(parcel.tetheredList)));
+ bcast.putStringArrayListExtra(
+ EXTRA_ERRORED_TETHER, toIfaces(Arrays.asList(parcel.erroredIfaceList)));
+ mContext.sendStickyBroadcastAsUser(bcast, UserHandle.ALL);
}
private class StateReceiver extends BroadcastReceiver {
@@ -1999,10 +2010,11 @@
final UpstreamNetworkState ns = (UpstreamNetworkState) o;
switch (arg1) {
case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
- mPrivateAddressCoordinator.updateUpstreamPrefix(ns);
+ mRoutingCoordinator.updateUpstreamPrefix(
+ ns.linkProperties, ns.networkCapabilities, ns.network);
break;
case UpstreamNetworkMonitor.EVENT_ON_LOST:
- mPrivateAddressCoordinator.removeUpstreamPrefix(ns.network);
+ mRoutingCoordinator.removeUpstreamPrefix(ns.network);
break;
}
@@ -2072,7 +2084,7 @@
return;
}
- mPrivateAddressCoordinator.maybeRemoveDeprecatedUpstreams();
+ mRoutingCoordinator.maybeRemoveDeprecatedUpstreams();
mUpstreamNetworkMonitor.startObserveAllNetworks();
// TODO: De-duplicate with updateUpstreamWanted() below.
@@ -2082,6 +2094,7 @@
chooseUpstreamType(true);
mTryCell = false;
}
+ mTetheringMetrics.initUpstreamUsageBaseline();
}
@Override
@@ -2190,9 +2203,9 @@
break;
}
case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
- final int tetheringType = message.arg1;
- final Boolean enabled = (Boolean) message.obj;
- enableTetheringInternal(tetheringType, enabled, null);
+ final boolean enabled = message.arg1 == 1;
+ final TetheringRequest request = (TetheringRequest) message.obj;
+ enableTetheringInternal(request.getTetheringType(), enabled, null);
break;
}
default:
@@ -2389,19 +2402,19 @@
/** Register tethering event callback */
void registerTetheringEventCallback(ITetheringEventCallback callback) {
- final boolean hasListPermission =
- hasCallingPermission(NETWORK_SETTINGS)
- || hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK)
- || hasCallingPermission(NETWORK_STACK);
+ final int uid = mDeps.getBinderCallingUid();
+ final boolean hasSystemPrivilege = hasCallingPermission(NETWORK_SETTINGS)
+ || hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK)
+ || hasCallingPermission(NETWORK_STACK);
mHandler.post(() -> {
- mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
+ CallbackCookie cookie = new CallbackCookie(uid, hasSystemPrivilege);
+ mTetheringEventCallbacks.register(callback, cookie);
final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
parcel.supportedTypes = mSupportedTypeBitmap;
parcel.upstreamNetwork = mTetherUpstream;
parcel.config = mConfig.toStableParcelable();
- parcel.states =
- mTetherStatesParcel != null ? mTetherStatesParcel : emptyTetherStatesParcel();
- parcel.tetheredClients = hasListPermission
+ parcel.states = buildTetherStatesParcel(cookie);
+ parcel.tetheredClients = hasSystemPrivilege
? mConnectedClientsTracker.getLastTetheredClients()
: Collections.emptyList();
parcel.offloadStatus = mOffloadStatus;
@@ -2413,17 +2426,6 @@
});
}
- private TetherStatesParcel emptyTetherStatesParcel() {
- final TetherStatesParcel parcel = new TetherStatesParcel();
- parcel.availableList = new TetheringInterface[0];
- parcel.tetheredList = new TetheringInterface[0];
- parcel.localOnlyList = new TetheringInterface[0];
- parcel.erroredIfaceList = new TetheringInterface[0];
- parcel.lastErrorList = new int[0];
-
- return parcel;
- }
-
private boolean hasCallingPermission(@NonNull String permission) {
return mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED;
}
@@ -2482,12 +2484,14 @@
}
}
- private void reportTetherStateChanged(TetherStatesParcel states) {
+ private void sendTetherStatesChangedCallback() {
final int length = mTetheringEventCallbacks.beginBroadcast();
try {
for (int i = 0; i < length; i++) {
try {
- mTetheringEventCallbacks.getBroadcastItem(i).onTetherStatesChanged(states);
+ TetherStatesParcel parcel = buildTetherStatesParcel(
+ (CallbackCookie) mTetheringEventCallbacks.getBroadcastCookie(i));
+ mTetheringEventCallbacks.getBroadcastItem(i).onTetherStatesChanged(parcel);
} catch (RemoteException e) {
// Not really very much to do here.
}
@@ -2495,6 +2499,18 @@
} finally {
mTetheringEventCallbacks.finishBroadcast();
}
+
+ if (DBG) {
+ // Use a CallbackCookie with system privilege so nothing is redacted.
+ TetherStatesParcel parcel = buildTetherStatesParcel(
+ new CallbackCookie(Process.SYSTEM_UID, true /* hasSystemPrivilege */));
+ Log.d(TAG, String.format(
+ "sendTetherStatesChangedCallback %s=[%s] %s=[%s] %s=[%s] %s=[%s]",
+ "avail", TextUtils.join(",", Arrays.asList(parcel.availableList)),
+ "local_only", TextUtils.join(",", Arrays.asList(parcel.localOnlyList)),
+ "tether", TextUtils.join(",", Arrays.asList(parcel.tetheredList)),
+ "error", TextUtils.join(",", Arrays.asList(parcel.erroredIfaceList))));
+ }
}
private void reportTetherClientsChanged(List<TetheredClient> clients) {
@@ -2504,7 +2520,7 @@
try {
final CallbackCookie cookie =
(CallbackCookie) mTetheringEventCallbacks.getBroadcastCookie(i);
- if (!cookie.hasListClientsPermission) continue;
+ if (!cookie.hasSystemPrivilege) continue;
mTetheringEventCallbacks.getBroadcastItem(i).onTetherClientsChanged(clients);
} catch (RemoteException e) {
// Not really very much to do here.
@@ -2659,11 +2675,6 @@
dumpBpf(pw);
- pw.println("Private address coordinator:");
- pw.increaseIndent();
- mPrivateAddressCoordinator.dump(pw);
- pw.decreaseIndent();
-
if (mWearableConnectionManager != null) {
pw.println("WearableConnectionManager:");
pw.increaseIndent();
@@ -2744,7 +2755,7 @@
return;
}
mTetherMainSM.sendMessage(which, state, 0, who);
- sendTetherStateChangedBroadcast();
+ notifyTetherStatesChanged();
}
@Override
@@ -2772,9 +2783,9 @@
}
@Override
- public void requestEnableTethering(int tetheringType, boolean enabled) {
+ public void requestEnableTethering(TetheringRequest request, boolean enabled) {
mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
- tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
+ enabled ? 1 : 0, 0, request);
}
}
@@ -2817,8 +2828,7 @@
mLog.i("adding IpServer for: " + iface);
final TetherState tetherState = new TetherState(
new IpServer(iface, mHandler, interfaceType, mLog, mNetd, mBpfCoordinator,
- mRoutingCoordinator, new ControlCallback(), mConfig,
- mPrivateAddressCoordinator, mTetheringMetrics,
+ mRoutingCoordinator, new ControlCallback(), mConfig, mTetheringMetrics,
mDeps.makeIpServerDependencies()), isNcm);
mTetherStates.put(iface, tetherState);
tetherState.ipServer.start();
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 298940e..b3e9c1b 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -144,6 +144,12 @@
/** A flag for using synchronous or asynchronous state machine. */
public static boolean USE_SYNC_SM = false;
+ /**
+ * A feature flag to control whether the active sessions metrics should be enabled.
+ * Disabled by default.
+ */
+ public static final String TETHER_ACTIVE_SESSIONS_METRICS = "tether_active_sessions_metrics";
+
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWigigRegexs;
@@ -176,7 +182,6 @@
private final int mP2pLeasesSubnetPrefixLength;
private final boolean mEnableWearTethering;
- private final boolean mRandomPrefixBase;
private final int mUsbTetheringFunction;
protected final ContentResolver mContentResolver;
@@ -294,8 +299,6 @@
mEnableWearTethering = shouldEnableWearTethering(ctx);
- mRandomPrefixBase = mDeps.isFeatureEnabled(ctx, TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION);
-
configLog.log(toString());
}
@@ -384,10 +387,6 @@
return mEnableWearTethering;
}
- public boolean isRandomPrefixBaseEnabled() {
- return mRandomPrefixBase;
- }
-
/**
* Check whether sync SM is enabled then set it to USE_SYNC_SM. This should be called once
* when tethering is created. Otherwise if the flag is pushed while tethering is enabled,
@@ -449,9 +448,6 @@
pw.print("mUsbTetheringFunction: ");
pw.println(isUsingNcm() ? "NCM" : "RNDIS");
- pw.print("mRandomPrefixBase: ");
- pw.println(mRandomPrefixBase);
-
pw.print("USE_SYNC_SM: ");
pw.println(USE_SYNC_SM);
}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 5d9d349..d89bf4d 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -21,9 +21,11 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothPan;
import android.content.Context;
+import android.net.ConnectivityManager;
import android.net.INetd;
import android.net.connectivity.ConnectivityInternalApiUtil;
import android.net.ip.IpServer;
+import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
@@ -135,7 +137,10 @@
public RoutingCoordinatorManager getRoutingCoordinator(Context context, SharedLog log) {
IBinder binder;
if (!SdkLevel.isAtLeastS()) {
- binder = new RoutingCoordinatorService(getINetd(context, log));
+ final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
+ binder =
+ new RoutingCoordinatorService(
+ getINetd(context, log), cm::getAllNetworks, context);
} else {
binder = ConnectivityInternalApiUtil.getRoutingCoordinator(context);
}
@@ -174,14 +179,6 @@
}
/**
- * Make PrivateAddressCoordinator to be used by Tethering.
- */
- public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx,
- TetheringConfiguration cfg) {
- return new PrivateAddressCoordinator(ctx, cfg);
- }
-
- /**
* Make BluetoothPanShim object to enable/disable bluetooth tethering.
*
* TODO: use BluetoothPan directly when mainline module is built with API 32.
@@ -204,4 +201,11 @@
public WearableConnectionManager makeWearableConnectionManager(Context ctx) {
return new WearableConnectionManager(ctx);
}
+
+ /**
+ * Wrapper to get the binder calling uid for unit testing.
+ */
+ public int getBinderCallingUid() {
+ return Binder.getCallingUid();
+ }
}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
index a0198cc..a29f0c2 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
@@ -85,7 +85,6 @@
static final int ROAMING_NOTIFICATION_ID = 1003;
@VisibleForTesting
static final int NO_ICON_ID = 0;
- @VisibleForTesting
static final int DOWNSTREAM_NONE = 0;
// Refer to TelephonyManager#getSimCarrierId for more details about carrier id.
@VisibleForTesting
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 454cbf1..3cb5f99 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -28,6 +28,7 @@
import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR;
+import android.app.AppOpsManager;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
@@ -138,8 +139,10 @@
listener)) {
return;
}
- // TODO(b/216524590): Add UID/packageName of caller to TetheringRequest here
- mTethering.startTethering(new TetheringRequest(request), callerPkg, listener);
+ TetheringRequest external = new TetheringRequest(request);
+ external.setUid(getBinderCallingUid());
+ external.setPackageName(callerPkg);
+ mTethering.startTethering(external, callerPkg, listener);
}
@Override
@@ -238,6 +241,12 @@
final String callingAttributionTag, final boolean onlyAllowPrivileged,
final IIntResultListener listener) {
try {
+ if (!checkPackageNameMatchesUid(getBinderCallingUid(), callerPkg)) {
+ Log.e(TAG, "Package name " + callerPkg + " does not match UID "
+ + getBinderCallingUid());
+ listener.onResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ return true;
+ }
if (!hasTetherChangePermission(callerPkg, callingAttributionTag,
onlyAllowPrivileged)) {
listener.onResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
@@ -256,6 +265,12 @@
private boolean checkAndNotifyCommonError(final String callerPkg,
final String callingAttributionTag, final ResultReceiver receiver) {
+ if (!checkPackageNameMatchesUid(getBinderCallingUid(), callerPkg)) {
+ Log.e(TAG, "Package name " + callerPkg + " does not match UID "
+ + getBinderCallingUid());
+ receiver.send(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, null);
+ return true;
+ }
if (!hasTetherChangePermission(callerPkg, callingAttributionTag,
false /* onlyAllowPrivileged */)) {
receiver.send(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, null);
@@ -290,9 +305,9 @@
if (mTethering.isTetherProvisioningRequired()) return false;
- int uid = Binder.getCallingUid();
+ int uid = getBinderCallingUid();
- // If callerPkg's uid is not same as Binder.getCallingUid(),
+ // If callerPkg's uid is not same as getBinderCallingUid(),
// checkAndNoteWriteSettingsOperation will return false and the operation will be
// denied.
return mService.checkAndNoteWriteSettingsOperation(mService, uid, callerPkg,
@@ -305,6 +320,14 @@
return mService.checkCallingOrSelfPermission(
ACCESS_NETWORK_STATE) == PERMISSION_GRANTED;
}
+
+ private int getBinderCallingUid() {
+ return mService.getBinderCallingUid();
+ }
+
+ private boolean checkPackageNameMatchesUid(final int uid, final String callerPkg) {
+ return mService.checkPackageNameMatchesUid(mService, uid, callerPkg);
+ }
}
/**
@@ -322,6 +345,32 @@
}
/**
+ * Check if the package name matches the uid.
+ */
+ @VisibleForTesting
+ boolean checkPackageNameMatchesUid(@NonNull Context context, int uid,
+ @NonNull String callingPackage) {
+ try {
+ final AppOpsManager mAppOps = context.getSystemService(AppOpsManager.class);
+ if (mAppOps == null) {
+ return false;
+ }
+ mAppOps.checkPackage(uid, callingPackage);
+ } catch (SecurityException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Wrapper for the Binder calling UID, used for mocks.
+ */
+ @VisibleForTesting
+ int getBinderCallingUid() {
+ return Binder.getCallingUid();
+ }
+
+ /**
* An injection method for testing.
*/
@VisibleForTesting
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index 136dfb1..087ce44 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -16,12 +16,21 @@
package com.android.networkstack.tethering.metrics;
+import static android.app.usage.NetworkStats.Bucket.STATE_ALL;
+import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
+import static android.net.NetworkTemplate.MATCH_ETHERNET;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringManager.TETHERING_NCM;
@@ -46,18 +55,27 @@
import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
import android.annotation.Nullable;
+import android.app.usage.NetworkStats;
+import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.net.NetworkCapabilities;
+import android.net.NetworkTemplate;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.stats.connectivity.DownstreamType;
import android.stats.connectivity.ErrorCode;
import android.stats.connectivity.UpstreamType;
import android.stats.connectivity.UserType;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.networkstack.tethering.UpstreamNetworkState;
import java.util.ArrayList;
@@ -66,6 +84,10 @@
/**
* Collection of utilities for tethering metrics.
*
+ * <p>This class is thread-safe. All accesses to this class will be either posting to the internal
+ * handler thread for processing or checking whether the access is from the internal handler
+ * thread. However, the constructor is an exception, as it is called on another thread.
+ *
* To see if the logs are properly sent to statsd, execute following commands
*
* $ adb shell cmd stats print-logs
@@ -79,11 +101,25 @@
private static final String SETTINGS_PKG_NAME = "com.android.settings";
private static final String SYSTEMUI_PKG_NAME = "com.android.systemui";
private static final String GMS_PKG_NAME = "com.google.android.gms";
+ /**
+ * A feature flag to control whether upstream data usage metrics should be enabled.
+ */
+ private static final String TETHER_UPSTREAM_DATA_USAGE_METRICS =
+ "tether_upstream_data_usage_metrics";
+ @VisibleForTesting
+ static final DataUsage EMPTY = new DataUsage(0L /* txBytes */, 0L /* rxBytes */);
private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
private final SparseArray<Long> mDownstreamStartTime = new SparseArray<Long>();
private final ArrayList<RecordUpstreamEvent> mUpstreamEventList = new ArrayList<>();
+ // Store the last reported data usage for each upstream type to be used for calculating the
+ // usage delta. The keys are the upstream types, and the values are the tethering UID data
+ // usage for the corresponding types. Retrieve the baseline data usage when tethering is
+ // enabled, update it when the upstream changes, and clear it when tethering is disabled.
+ private final ArrayMap<UpstreamType, DataUsage> mLastReportedUpstreamUsage = new ArrayMap<>();
private final Context mContext;
private final Dependencies mDependencies;
+ private final NetworkStatsManager mNetworkStatsManager;
+ private final Handler mHandler;
private UpstreamType mCurrentUpstream = null;
private Long mCurrentUpStreamStartTime = 0L;
@@ -112,6 +148,29 @@
public long timeNow() {
return System.currentTimeMillis();
}
+
+ /**
+ * Indicates whether {@link #TETHER_UPSTREAM_DATA_USAGE_METRICS} is enabled.
+ */
+ public boolean isUpstreamDataUsageMetricsEnabled(Context context) {
+ // Getting data usage requires building a NetworkTemplate. However, the
+ // NetworkTemplate#Builder API was introduced in Android T.
+ return SdkLevel.isAtLeastT() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+ context, TETHER_UPSTREAM_DATA_USAGE_METRICS);
+ }
+
+ /**
+ * @see Handler
+ *
+ * Note: This should only be called once, within the constructor, as it creates a new
+ * thread. Calling it multiple times could lead to a thread leak.
+ */
+ @NonNull
+ public Handler createHandler() {
+ final HandlerThread thread = new HandlerThread(TAG);
+ thread.start();
+ return new Handler(thread.getLooper());
+ }
}
/**
@@ -126,18 +185,61 @@
TetheringMetrics(Context context, Dependencies dependencies) {
mContext = context;
mDependencies = dependencies;
+ mNetworkStatsManager = mContext.getSystemService(NetworkStatsManager.class);
+ mHandler = dependencies.createHandler();
+ }
+
+ @VisibleForTesting
+ static class DataUsage {
+ public final long txBytes;
+ public final long rxBytes;
+
+ DataUsage(long txBytes, long rxBytes) {
+ this.txBytes = txBytes;
+ this.rxBytes = rxBytes;
+ }
+
+ /*** Calculate the data usage delta from give new and old usage */
+ public static DataUsage subtract(DataUsage newUsage, DataUsage oldUsage) {
+ return new DataUsage(
+ newUsage.txBytes - oldUsage.txBytes,
+ newUsage.rxBytes - oldUsage.rxBytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (txBytes & 0xFFFFFFFF)
+ + ((int) (txBytes >> 32) * 3)
+ + ((int) (rxBytes & 0xFFFFFFFF) * 5)
+ + ((int) (rxBytes >> 32) * 7);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof DataUsage)) {
+ return false;
+ }
+ return txBytes == ((DataUsage) other).txBytes
+ && rxBytes == ((DataUsage) other).rxBytes;
+ }
+
}
private static class RecordUpstreamEvent {
- public final long mStartTime;
- public final long mStopTime;
- public final UpstreamType mUpstreamType;
+ final long mStartTime;
+ final long mStopTime;
+ final UpstreamType mUpstreamType;
+ final DataUsage mDataUsage;
RecordUpstreamEvent(final long startTime, final long stopTime,
- final UpstreamType upstream) {
+ final UpstreamType upstream, final DataUsage dataUsage) {
mStartTime = startTime;
mStopTime = stopTime;
mUpstreamType = upstream;
+ mDataUsage = dataUsage;
}
}
@@ -150,6 +252,10 @@
* @param callerPkg The package name of the caller.
*/
public void createBuilder(final int downstreamType, final String callerPkg) {
+ mHandler.post(() -> handleCreateBuilder(downstreamType, callerPkg));
+ }
+
+ private void handleCreateBuilder(final int downstreamType, final String callerPkg) {
NetworkTetheringReported.Builder statsBuilder = NetworkTetheringReported.newBuilder()
.setDownstreamType(downstreamTypeToEnum(downstreamType))
.setUserType(userTypeToEnum(callerPkg))
@@ -167,6 +273,10 @@
* @param errCode The error code to set.
*/
public void updateErrorCode(final int downstreamType, final int errCode) {
+ mHandler.post(() -> handleUpdateErrorCode(downstreamType, errCode));
+ }
+
+ private void handleUpdateErrorCode(final int downstreamType, final int errCode) {
NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
if (statsBuilder == null) {
Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
@@ -176,18 +286,56 @@
}
/**
+ * Calculates the data usage difference between the current and previous usage for the
+ * specified upstream type.
+ *
+ * Note: This must be called before updating mCurrentUpstream when changing the upstream.
+ *
+ * @return A DataUsage object containing the calculated difference in transmitted (tx) and
+ * received (rx) bytes.
+ */
+ private DataUsage calculateDataUsageDelta(@Nullable UpstreamType upstream) {
+ if (!mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)) {
+ return EMPTY;
+ }
+
+ if (upstream == null || !isUsageSupportedForUpstreamType(upstream)) {
+ return EMPTY;
+ }
+
+ final DataUsage oldUsage = mLastReportedUpstreamUsage.getOrDefault(upstream, EMPTY);
+ if (oldUsage.equals(EMPTY)) {
+ Log.d(TAG, "No usage baseline for the upstream=" + upstream);
+ return EMPTY;
+ }
+ // TODO(b/370724247): Fix data usage which might be incorrect if the device uses
+ // tethering with the same upstream for over 15 days.
+ // Need to refresh the baseline usage data. If the network switches back to Wi-Fi after
+ // using cellular data (Wi-Fi -> Cellular -> Wi-Fi), the old baseline might be
+ // inaccurate, leading to incorrect delta calculations.
+ final DataUsage newUsage = getCurrentDataUsageForUpstreamType(upstream);
+ mLastReportedUpstreamUsage.put(upstream, newUsage);
+ return DataUsage.subtract(newUsage, oldUsage);
+ }
+
+ /**
* Update the list of upstream types and their duration whenever the current upstream type
* changes.
* @param ns The UpstreamNetworkState object representing the current upstream network state.
*/
public void maybeUpdateUpstreamType(@Nullable final UpstreamNetworkState ns) {
+ mHandler.post(() -> handleMaybeUpdateUpstreamType(ns));
+ }
+
+ private void handleMaybeUpdateUpstreamType(@Nullable final UpstreamNetworkState ns) {
UpstreamType upstream = transportTypeToUpstreamTypeEnum(ns);
if (upstream.equals(mCurrentUpstream)) return;
final long newTime = mDependencies.timeNow();
if (mCurrentUpstream != null) {
+ final DataUsage dataUsage = calculateDataUsageDelta(mCurrentUpstream);
mUpstreamEventList.add(new RecordUpstreamEvent(mCurrentUpStreamStartTime, newTime,
- mCurrentUpstream));
+ mCurrentUpstream, dataUsage));
}
mCurrentUpstream = upstream;
mCurrentUpStreamStartTime = newTime;
@@ -238,13 +386,14 @@
final long startTime = Math.max(downstreamStartTime, event.mStartTime);
// Handle completed upstream events.
addUpstreamEvent(upstreamEventsBuilder, startTime, event.mStopTime,
- event.mUpstreamType, 0L /* txBytes */, 0L /* rxBytes */);
+ event.mUpstreamType, event.mDataUsage.txBytes, event.mDataUsage.rxBytes);
}
final long startTime = Math.max(downstreamStartTime, mCurrentUpStreamStartTime);
final long stopTime = mDependencies.timeNow();
// Handle the last upstream event.
+ final DataUsage dataUsage = calculateDataUsageDelta(mCurrentUpstream);
addUpstreamEvent(upstreamEventsBuilder, startTime, stopTime, mCurrentUpstream,
- 0L /* txBytes */, 0L /* rxBytes */);
+ dataUsage.txBytes, dataUsage.rxBytes);
statsBuilder.setUpstreamEvents(upstreamEventsBuilder);
statsBuilder.setDurationMillis(stopTime - downstreamStartTime);
}
@@ -260,6 +409,10 @@
* @param downstreamType the type of downstream event to remove statistics for
*/
public void sendReport(final int downstreamType) {
+ mHandler.post(() -> handleSendReport(downstreamType));
+ }
+
+ private void handleSendReport(final int downstreamType) {
final NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
if (statsBuilder == null) {
Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
@@ -280,9 +433,7 @@
*
* @param reported a NetworkTetheringReported object containing statistics to write
*/
- @VisibleForTesting
- public void write(@NonNull final NetworkTetheringReported reported) {
- final byte[] upstreamEvents = reported.getUpstreamEvents().toByteArray();
+ private void write(@NonNull final NetworkTetheringReported reported) {
mDependencies.write(reported);
if (DBG) {
Log.d(
@@ -296,19 +447,75 @@
+ ", userType: "
+ reported.getUserType().getNumber()
+ ", upstreamTypes: "
- + Arrays.toString(upstreamEvents)
+ + Arrays.toString(reported.getUpstreamEvents().toByteArray())
+ ", durationMillis: "
+ reported.getDurationMillis());
}
}
/**
+ * Initialize the upstream data usage baseline when tethering is turned on.
+ */
+ public void initUpstreamUsageBaseline() {
+ mHandler.post(() -> handleInitUpstreamUsageBaseline());
+ }
+
+ private void handleInitUpstreamUsageBaseline() {
+ if (!mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)) {
+ return;
+ }
+
+ if (!mLastReportedUpstreamUsage.isEmpty()) {
+ Log.wtf(TAG, "The upstream usage baseline has been initialed.");
+ return;
+ }
+
+ for (UpstreamType type : UpstreamType.values()) {
+ if (!isUsageSupportedForUpstreamType(type)) continue;
+ mLastReportedUpstreamUsage.put(type, getCurrentDataUsageForUpstreamType(type));
+ }
+ }
+
+ @VisibleForTesting
+ @NonNull
+ DataUsage getLastReportedUsageFromUpstreamType(@NonNull UpstreamType type) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ return mLastReportedUpstreamUsage.getOrDefault(type, EMPTY);
+ }
+
+
+ /**
+ * Get the current usage for given upstream type.
+ */
+ @NonNull
+ private DataUsage getCurrentDataUsageForUpstreamType(@NonNull UpstreamType type) {
+ final NetworkStats stats = mNetworkStatsManager.queryDetailsForUidTagState(
+ buildNetworkTemplateForUpstreamType(type), Long.MIN_VALUE, Long.MAX_VALUE,
+ UID_TETHERING, TAG_NONE, STATE_ALL);
+
+ final NetworkStats.Bucket bucket = new NetworkStats.Bucket();
+ Long totalTxBytes = 0L;
+ Long totalRxBytes = 0L;
+ while (stats.hasNextBucket()) {
+ stats.getNextBucket(bucket);
+ totalTxBytes += bucket.getTxBytes();
+ totalRxBytes += bucket.getRxBytes();
+ }
+ return new DataUsage(totalTxBytes, totalRxBytes);
+ }
+
+ /**
* Cleans up the variables related to upstream events when tethering is turned off.
*/
public void cleanup() {
+ mHandler.post(() -> handleCleanup());
+ }
+
+ private void handleCleanup() {
mUpstreamEventList.clear();
mCurrentUpstream = null;
mCurrentUpStreamStartTime = 0L;
+ mLastReportedUpstreamUsage.clear();
}
private DownstreamType downstreamTypeToEnum(final int ifaceType) {
@@ -419,4 +626,46 @@
}
return false;
}
+
+ /**
+ * Build NetworkTemplate for the given upstream type.
+ *
+ * <p> NetworkTemplate.Builder API was introduced in Android T.
+ *
+ * @param type the upstream type
+ * @return A NetworkTemplate object with a corresponding match rule or null if tethering
+ * metrics' data usage cannot be collected for a given upstream type.
+ */
+ @Nullable
+ public static NetworkTemplate buildNetworkTemplateForUpstreamType(@NonNull UpstreamType type) {
+ if (!isUsageSupportedForUpstreamType(type)) return null;
+
+ switch (type) {
+ case UT_CELLULAR:
+ // TODO: Handle the DUN connection, which is not a default network.
+ return new NetworkTemplate.Builder(MATCH_MOBILE)
+ .setMeteredness(METERED_YES)
+ .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+ .build();
+ case UT_WIFI:
+ return new NetworkTemplate.Builder(MATCH_WIFI)
+ .setMeteredness(METERED_YES)
+ .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+ .build();
+ case UT_BLUETOOTH:
+ return new NetworkTemplate.Builder(MATCH_BLUETOOTH)
+ .setMeteredness(METERED_YES)
+ .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+ .build();
+ case UT_ETHERNET:
+ return new NetworkTemplate.Builder(MATCH_ETHERNET)
+ .setMeteredness(METERED_YES)
+ .setDefaultNetworkStatus(DEFAULT_NETWORK_YES)
+ .build();
+ default:
+ Log.e(TAG, "Unsupported UpstreamType: " + type.name());
+ break;
+ }
+ return null;
+ }
}
diff --git a/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java b/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
index c9e75c0..5eb1551 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
@@ -62,8 +62,8 @@
if (DBG) Log.d(mTag, "startListening");
if (mReceiver != null) return;
- mReceiver = new Receiver(mTag, mGenerationNumber, mCallback);
- mContext.registerReceiver(mReceiver, mFilter, null, mHandler);
+ mReceiver = new Receiver(mTag, mGenerationNumber, mCallback, mHandler);
+ mContext.registerReceiver(mReceiver, mFilter);
}
/** Stop listening to intent broadcast. */
@@ -77,30 +77,35 @@
}
private static class Receiver extends BroadcastReceiver {
- public final String tag;
- public final AtomicInteger atomicGenerationNumber;
- public final Consumer<Intent> callback;
+ final String mTag;
+ final AtomicInteger mAtomicGenerationNumber;
+ final Consumer<Intent> mCallback;
// Used to verify this receiver is still current.
- public final int generationNumber;
+ final int mGenerationNumber;
+ private final Handler mHandler;
- Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback) {
- this.tag = tag;
- this.atomicGenerationNumber = atomicGenerationNumber;
- this.callback = callback;
- generationNumber = atomicGenerationNumber.incrementAndGet();
+ Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback,
+ Handler handler) {
+ mTag = tag;
+ mAtomicGenerationNumber = atomicGenerationNumber;
+ mCallback = callback;
+ mGenerationNumber = atomicGenerationNumber.incrementAndGet();
+ mHandler = handler;
}
@Override
public void onReceive(Context context, Intent intent) {
- final int currentGenerationNumber = atomicGenerationNumber.get();
+ mHandler.post(() -> {
+ final int currentGenerationNumber = mAtomicGenerationNumber.get();
- if (DBG) {
- Log.d(tag, "receiver generationNumber=" + generationNumber
- + ", current generationNumber=" + currentGenerationNumber);
- }
- if (generationNumber != currentGenerationNumber) return;
+ if (DBG) {
+ Log.d(mTag, "receiver generationNumber=" + mGenerationNumber
+ + ", current generationNumber=" + currentGenerationNumber);
+ }
+ if (mGenerationNumber != currentGenerationNumber) return;
- callback.accept(intent);
+ mCallback.accept(intent);
+ });
}
}
}
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 337d408..2211546 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -38,9 +38,9 @@
"connectivity-net-module-utils-bpf",
],
libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
],
}
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 3944a8a..01f3af9 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -26,7 +26,9 @@
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringTester.buildTcpPacket;
import static android.net.TetheringTester.buildUdpPacket;
+import static android.net.TetheringTester.buildUdpPackets;
import static android.net.TetheringTester.isAddressIpv4;
+import static android.net.TetheringTester.isExpectedFragmentIpPacket;
import static android.net.TetheringTester.isExpectedIcmpPacket;
import static android.net.TetheringTester.isExpectedTcpPacket;
import static android.net.TetheringTester.isExpectedUdpPacket;
@@ -58,15 +60,17 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
+import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.FragmentHeader;
import com.android.net.module.util.structs.Ipv6Header;
import com.android.testutils.HandlerUtils;
-import com.android.testutils.TapPacketReader;
+import com.android.testutils.PollPacketReader;
import com.android.testutils.TestNetworkTracker;
import org.junit.After;
@@ -154,10 +158,10 @@
protected TetheredInterfaceRequester mTetheredInterfaceRequester;
// Late initialization in initTetheringTester().
- private TapPacketReader mUpstreamReader;
+ private PollPacketReader mUpstreamReader;
private TestNetworkTracker mUpstreamTracker;
private TestNetworkInterface mDownstreamIface;
- private TapPacketReader mDownstreamReader;
+ private PollPacketReader mDownstreamReader;
private MyTetheringEventCallback mTetheringEventCallback;
public Context getContext() {
@@ -183,10 +187,10 @@
return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> sTm.isTetheringSupported());
}
- protected void maybeStopTapPacketReader(final TapPacketReader tapPacketReader)
+ protected void maybeStopTapPacketReader(final PollPacketReader tapPacketReader)
throws Exception {
if (tapPacketReader != null) {
- TapPacketReader reader = tapPacketReader;
+ PollPacketReader reader = tapPacketReader;
mHandler.post(() -> reader.stop());
}
}
@@ -224,7 +228,7 @@
});
}
if (mUpstreamReader != null) {
- TapPacketReader reader = mUpstreamReader;
+ PollPacketReader reader = mUpstreamReader;
mHandler.post(() -> reader.stop());
mUpstreamReader = null;
}
@@ -287,7 +291,7 @@
});
}
- protected static void waitForRouterAdvertisement(TapPacketReader reader, String iface,
+ protected static void waitForRouterAdvertisement(PollPacketReader reader, String iface,
long timeoutMs) {
final long deadline = SystemClock.uptimeMillis() + timeoutMs;
do {
@@ -564,13 +568,19 @@
return nif.getMTU();
}
- protected TapPacketReader makePacketReader(final TestNetworkInterface iface) throws Exception {
+ protected int getIndexByName(String ifaceName) throws SocketException {
+ NetworkInterface nif = NetworkInterface.getByName(ifaceName);
+ assertNotNull("Can't get NetworkInterface object for " + ifaceName, nif);
+ return nif.getIndex();
+ }
+
+ protected PollPacketReader makePacketReader(final TestNetworkInterface iface) throws Exception {
FileDescriptor fd = iface.getFileDescriptor().getFileDescriptor();
return makePacketReader(fd, getMTU(iface));
}
- protected TapPacketReader makePacketReader(FileDescriptor fd, int mtu) {
- final TapPacketReader reader = new TapPacketReader(mHandler, fd, mtu);
+ protected PollPacketReader makePacketReader(FileDescriptor fd, int mtu) {
+ final PollPacketReader reader = new PollPacketReader(mHandler, fd, mtu);
mHandler.post(() -> reader.start());
HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
return reader;
@@ -678,6 +688,57 @@
});
}
+ protected void sendDownloadFragmentedUdpPackets(@NonNull final Inet6Address srcIp,
+ @NonNull final Inet6Address dstIp, @NonNull final TetheringTester tester,
+ @NonNull final ByteBuffer payload, int l2mtu) throws Exception {
+ final List<ByteBuffer> testPackets = buildUdpPackets(null /* srcMac */, null /* dstMac */,
+ srcIp, dstIp, REMOTE_PORT, LOCAL_PORT, payload, l2mtu);
+ assertTrue("No packet fragmentation occurs", testPackets.size() > 1);
+
+ short id = 0;
+ final ArrayMap<Short, ByteBuffer> fragmentPayloads = new ArrayMap<>();
+ for (ByteBuffer testPacket : testPackets) {
+ Struct.parse(Ipv6Header.class, testPacket);
+ final FragmentHeader fragmentHeader = Struct.parse(FragmentHeader.class, testPacket);
+ // Conversion of IPv6's fragmentOffset field to IPv4's flagsAndFragmentOffset field.
+ // IPv6 Fragment Header:
+ // '13 bits of offset in multiples of 8' + 2 zero bits + more fragment bit
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Next Header | Reserved | Fragment Offset |Res|M|
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Identification |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // IPv4 Header:
+ // zero bit + don't frag bit + more frag bit + '13 bits of offset in multiples of 8'
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // |Version| IHL |Type of Service| Total Length |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Identification |Flags| Fragment Offset |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // + . . . +
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ short offset = (short) (((fragmentHeader.fragmentOffset & 0x1) << 13)
+ | (fragmentHeader.fragmentOffset >> 3));
+ // RFC6145: for fragment id, copied from the low-order 16 bits in the identification
+ // field in the Fragment Header.
+ id = (short) (fragmentHeader.identification & 0xffff);
+ final byte[] fragmentPayload = new byte[testPacket.remaining()];
+ testPacket.get(fragmentPayload);
+ testPacket.flip();
+ fragmentPayloads.put(offset, ByteBuffer.wrap(fragmentPayload));
+ }
+
+ final short fragId = id;
+ tester.verifyDownloadBatch(testPackets, p -> {
+ Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+ return isExpectedFragmentIpPacket(p, fragId, fragmentPayloads);
+ });
+ }
+
protected void sendDownloadPacketTcp(@NonNull final InetAddress srcIp,
@NonNull final InetAddress dstIp, short seq, short ack, byte tcpFlags,
@NonNull final ByteBuffer payload, @NonNull final TetheringTester tester,
@@ -913,6 +974,11 @@
return Struct.parse(Ipv6Header.class, ByteBuffer.wrap(expectedPacket)).srcIp;
}
+ protected String getUpstreamInterfaceName() {
+ if (mUpstreamTracker == null) return null;
+ return mUpstreamTracker.getTestIface().getInterfaceName();
+ }
+
protected <T> List<T> toList(T... array) {
return Arrays.asList(array);
}
diff --git a/Tethering/tests/integration/base/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
index ae4ae55..fb94eed 100644
--- a/Tethering/tests/integration/base/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/base/android/net/TetheringTester.java
@@ -27,6 +27,7 @@
import static android.system.OsConstants.IPPROTO_IPV6;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
+
import static com.android.net.module.util.DnsPacket.ANSECTION;
import static com.android.net.module.util.DnsPacket.DnsHeader;
import static com.android.net.module.util.DnsPacket.DnsRecord;
@@ -53,6 +54,7 @@
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
+
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
@@ -82,7 +84,7 @@
import com.android.net.module.util.structs.RaHeader;
import com.android.net.module.util.structs.TcpHeader;
import com.android.net.module.util.structs.UdpHeader;
-import com.android.testutils.TapPacketReader;
+import com.android.testutils.PollPacketReader;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -91,6 +93,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
@@ -154,14 +157,14 @@
public static final String DHCP_HOSTNAME = "testhostname";
private final ArrayMap<MacAddress, TetheredDevice> mTetheredDevices;
- private final TapPacketReader mDownstreamReader;
- private final TapPacketReader mUpstreamReader;
+ private final PollPacketReader mDownstreamReader;
+ private final PollPacketReader mUpstreamReader;
- public TetheringTester(TapPacketReader downstream) {
+ public TetheringTester(PollPacketReader downstream) {
this(downstream, null);
}
- public TetheringTester(TapPacketReader downstream, TapPacketReader upstream) {
+ public TetheringTester(PollPacketReader downstream, PollPacketReader upstream) {
if (downstream == null) fail("Downstream reader could not be NULL");
mDownstreamReader = downstream;
@@ -497,6 +500,51 @@
});
}
+ /**
+ * Checks if the given raw packet data represents an expected fragmented IP packet.
+ *
+ * @param rawPacket the raw packet data to check.
+ * @param id the identification field of the fragmented IP packet.
+ * @param expectedPayloads a map of fragment offsets to their corresponding payload data.
+ * @return true if the packet is a valid fragmented IP packet with matching payload fragments;
+ * false otherwise.
+ */
+ public static boolean isExpectedFragmentIpPacket(@NonNull final byte[] rawPacket, int id,
+ @NonNull final Map<Short, ByteBuffer> expectedPayloads) {
+ final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
+ try {
+ // Validate Ethernet header and IPv4 header.
+ if (!hasExpectedEtherHeader(buf, true /* isIpv4 */)) return false;
+ final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, buf);
+ if (ipv4Header.protocol != (byte) IPPROTO_UDP) return false;
+ if (ipv4Header.id != id) return false;
+ // Validate payload data which expected at a specific fragment offset.
+ final ByteBuffer expectedPayload =
+ expectedPayloads.get(ipv4Header.flagsAndFragmentOffset);
+ if (expectedPayload == null) return false;
+ if (buf.remaining() != expectedPayload.limit()) return false;
+ // Validate UDP header (which located in the 1st fragment).
+ // TODO: Validate the checksum field in UDP header. Currently, it'll be altered by NAT.
+ if ((ipv4Header.flagsAndFragmentOffset & 0x1FFF) == 0) {
+ final UdpHeader receivedUdpHeader = Struct.parse(UdpHeader.class, buf);
+ final UdpHeader expectedUdpHeader = Struct.parse(UdpHeader.class, expectedPayload);
+ if (receivedUdpHeader == null || expectedUdpHeader == null) return false;
+ if (receivedUdpHeader.srcPort != expectedUdpHeader.srcPort
+ || receivedUdpHeader.dstPort != expectedUdpHeader.dstPort
+ || receivedUdpHeader.length != expectedUdpHeader.length) {
+ return false;
+ }
+ return true;
+ }
+ // Check the contents of the remaining payload.
+ return Arrays.equals(getRemaining(buf),
+ getRemaining(expectedPayload.asReadOnlyBuffer()));
+ } catch (Exception e) {
+ // A failed packet parsing indicates that the packet is not a fragmented IPv4 packet.
+ return false;
+ }
+ }
+
// |expectedPayload| is copied as read-only because the caller may reuse it.
// See hasExpectedDnsMessage.
public static boolean isExpectedUdpDnsPacket(@NonNull final byte[] rawPacket, boolean hasEth,
@@ -683,10 +731,10 @@
}
@NonNull
- public static ByteBuffer buildUdpPacket(
+ public static List<ByteBuffer> buildUdpPackets(
@Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
@NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
- short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+ short srcPort, short dstPort, @Nullable final ByteBuffer payload, int l2mtu)
throws Exception {
final int ipProto = getIpProto(srcIp, dstIp);
final boolean hasEther = (srcMac != null && dstMac != null);
@@ -720,7 +768,30 @@
payload.clear();
}
- return packetBuilder.finalizePacket();
+ return l2mtu == 0
+ ? Arrays.asList(packetBuilder.finalizePacket())
+ : packetBuilder.finalizePacket(l2mtu);
+ }
+
+ /**
+ * Builds a UDP packet.
+ *
+ * @param srcMac the source MAC address.
+ * @param dstMac the destination MAC address.
+ * @param srcIp the source IP address.
+ * @param dstIp the destination IP address.
+ * @param srcPort the source port number.
+ * @param dstPort the destination port number.
+ * @param payload the optional payload data to be included in the packet.
+ * @return a ByteBuffer containing the constructed UDP packet.
+ */
+ @NonNull
+ public static ByteBuffer buildUdpPacket(
+ @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+ @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
+ short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+ throws Exception {
+ return buildUdpPackets(srcMac, dstMac, srcIp, dstIp, srcPort, dstPort, payload, 0).get(0);
}
@NonNull
@@ -994,6 +1065,28 @@
return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
}
+ /**
+ * Sends a batch of download packets and verifies against a specified filtering condition.
+ *
+ * This method is designed for testing fragmented packets. All packets are sent before
+ * verification because the kernel buffers fragments until the last one is received.
+ * Captured packets are then verified against the provided filter.
+ *
+ * @param packets the list of ByteBuffers containing the packets to send.
+ * @param filter a Predicate that defines the filtering condition to apply to each received
+ * packet. If the filter returns true for a packet's data, it is considered to
+ * meet the verification criteria.
+ */
+ public void verifyDownloadBatch(final List<ByteBuffer> packets, final Predicate<byte[]> filter)
+ throws Exception {
+ for (ByteBuffer packet : packets) {
+ sendDownloadPacket(packet);
+ }
+ for (int i = 0; i < packets.size(); ++i) {
+ verifyPacketNotNull("Download fail", getDownloadPacket(filter));
+ }
+ }
+
// Send DHCPDISCOVER to DHCP server to see if DHCP server is still alive to handle
// the upcoming DHCP packets. This method should be only used when we know the DHCP
// server has been created successfully before.
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 9cdba2f..1bbea94 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -33,6 +33,9 @@
import static com.android.net.module.util.HexDump.dumpHexString;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
import static com.android.testutils.DeviceInfoUtils.KVersion;
import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -62,6 +65,10 @@
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.Ipv6Utils;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.bpf.ClatEgress4Key;
+import com.android.net.module.util.bpf.ClatEgress4Value;
+import com.android.net.module.util.bpf.ClatIngress6Key;
+import com.android.net.module.util.bpf.ClatIngress6Value;
import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
import com.android.net.module.util.bpf.TetherStatsKey;
@@ -73,7 +80,7 @@
import com.android.testutils.DeviceInfoUtils;
import com.android.testutils.DumpTestUtils;
import com.android.testutils.NetworkStackModuleTest;
-import com.android.testutils.TapPacketReader;
+import com.android.testutils.PollPacketReader;
import org.junit.After;
import org.junit.Rule;
@@ -122,6 +129,8 @@
private static final int TX_UDP_PACKET_SIZE = 30;
private static final int TX_UDP_PACKET_COUNT = 123;
+ private static final String DUMPSYS_CLAT_RAWMAP_EGRESS4_ARG = "clatEgress4RawBpfMap";
+ private static final String DUMPSYS_CLAT_RAWMAP_INGRESS6_ARG = "clatIngress6RawBpfMap";
private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
@@ -204,7 +213,7 @@
TestNetworkInterface downstreamIface = null;
MyTetheringEventCallback tetheringEventCallback = null;
- TapPacketReader downstreamReader = null;
+ PollPacketReader downstreamReader = null;
try {
downstreamIface = createTestInterface();
@@ -244,7 +253,7 @@
TestNetworkInterface downstreamIface = null;
MyTetheringEventCallback tetheringEventCallback = null;
- TapPacketReader downstreamReader = null;
+ PollPacketReader downstreamReader = null;
try {
downstreamIface = createTestInterface();
@@ -274,7 +283,7 @@
TestNetworkInterface downstreamIface = null;
MyTetheringEventCallback tetheringEventCallback = null;
- TapPacketReader downstreamReader = null;
+ PollPacketReader downstreamReader = null;
try {
downstreamIface = createTestInterface();
@@ -348,7 +357,7 @@
TestNetworkInterface downstreamIface = null;
MyTetheringEventCallback tetheringEventCallback = null;
- TapPacketReader downstreamReader = null;
+ PollPacketReader downstreamReader = null;
try {
downstreamIface = createTestInterface();
@@ -414,7 +423,7 @@
// client, which is not possible in this test.
}
- private void checkTetheredClientCallbacks(final TapPacketReader packetReader,
+ private void checkTetheredClientCallbacks(final PollPacketReader packetReader,
final MyTetheringEventCallback tetheringEventCallback) throws Exception {
// Create a fake client.
byte[] clientMacAddr = new byte[6];
@@ -901,11 +910,10 @@
@NonNull
private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
- Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+ Class<K> keyClass, Class<V> valueClass, @NonNull String service, @NonNull String[] args)
throws Exception {
- final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
final String rawMapStr = runAsShell(DUMP, () ->
- DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args));
+ DumpTestUtils.dumpService(service, args));
final HashMap<K, V> map = new HashMap<>();
for (final String line : rawMapStr.split(LINE_DELIMITER)) {
@@ -918,10 +926,10 @@
@Nullable
private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
- Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+ Class<K> keyClass, Class<V> valueClass, @NonNull String service, @NonNull String[] args)
throws Exception {
for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
- final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
+ final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, service, args);
if (!map.isEmpty()) return map;
Thread.sleep(DUMP_POLLING_INTERVAL_MS);
@@ -977,8 +985,10 @@
Thread.sleep(UDP_STREAM_SLACK_MS);
// [1] Verify IPv4 upstream rule map.
+ final String[] upstreamArgs = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG,
+ DUMPSYS_RAWMAP_ARG_UPSTREAM4};
final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
- Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
+ Tether4Key.class, Tether4Value.class, Context.TETHERING_SERVICE, upstreamArgs);
assertNotNull(upstreamMap);
assertEquals(1, upstreamMap.size());
@@ -1017,8 +1027,10 @@
}
// Dump stats map to verify.
+ final String[] statsArgs = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG,
+ DUMPSYS_RAWMAP_ARG_STATS};
final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
- TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
+ TetherStatsKey.class, TetherStatsValue.class, Context.TETHERING_SERVICE, statsArgs);
assertNotNull(statsMap);
assertEquals(1, statsMap.size());
@@ -1053,4 +1065,119 @@
runUdp4Test();
}
+
+ private ClatEgress4Value getClatEgress4Value(int clatIfaceIndex) throws Exception {
+ // Command: dumpsys connectivity clatEgress4RawBpfMap
+ final String[] args = new String[] {DUMPSYS_CLAT_RAWMAP_EGRESS4_ARG};
+ final HashMap<ClatEgress4Key, ClatEgress4Value> egress4Map = pollRawMapFromDump(
+ ClatEgress4Key.class, ClatEgress4Value.class, Context.CONNECTIVITY_SERVICE, args);
+ assertNotNull(egress4Map);
+ for (Map.Entry<ClatEgress4Key, ClatEgress4Value> entry : egress4Map.entrySet()) {
+ ClatEgress4Key key = entry.getKey();
+ if (key.iif == clatIfaceIndex) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ private ClatIngress6Value getClatIngress6Value(int ifaceIndex) throws Exception {
+ // Command: dumpsys connectivity clatIngress6RawBpfMap
+ final String[] args = new String[] {DUMPSYS_CLAT_RAWMAP_INGRESS6_ARG};
+ final HashMap<ClatIngress6Key, ClatIngress6Value> ingress6Map = pollRawMapFromDump(
+ ClatIngress6Key.class, ClatIngress6Value.class, Context.CONNECTIVITY_SERVICE, args);
+ assertNotNull(ingress6Map);
+ for (Map.Entry<ClatIngress6Key, ClatIngress6Value> entry : ingress6Map.entrySet()) {
+ ClatIngress6Key key = entry.getKey();
+ if (key.iif == ifaceIndex) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Test network topology:
+ *
+ * public network (rawip) private network
+ * | UE (CLAT support) |
+ * +---------------+ V +------------+------------+ V +------------+
+ * | NAT64 Gateway +---------+ Upstream | Downstream +---------+ Client |
+ * +---------------+ +------------+------------+ +------------+
+ * remote ip public ip private ip
+ * [64:ff9b::808:808]:443 [clat ipv6]:9876 [TetheredDevice ipv4]:9876
+ *
+ * Note that CLAT IPv6 address is generated by ClatCoordinator. Get the CLAT IPv6 address by
+ * sending out an IPv4 packet and extracting the source address from CLAT translated IPv6
+ * packet.
+ */
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testTetherClatBpfOffloadUdp() throws Exception {
+ assumeKernelSupportBpfOffloadUdpV4();
+
+ // CLAT only starts on IPv6 only network.
+ final TetheringTester tester = initTetheringTester(toList(TEST_IP6_ADDR),
+ toList(TEST_IP6_DNS));
+ final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, true /* hasIpv6 */);
+
+ // Get CLAT IPv6 address.
+ final Inet6Address clatIp6 = getClatIpv6Address(tester, tethered);
+
+ // Get current values before sending packets.
+ final String ifaceName = getUpstreamInterfaceName();
+ final int ifaceIndex = getIndexByName(ifaceName);
+ final int clatIfaceIndex = getIndexByName("v4-" + ifaceName);
+ final ClatEgress4Value oldEgress4 = getClatEgress4Value(clatIfaceIndex);
+ final ClatIngress6Value oldIngress6 = getClatIngress6Value(ifaceIndex);
+ assertNotNull(oldEgress4);
+ assertNotNull(oldIngress6);
+
+ // Send an IPv4 UDP packet in original direction.
+ // IPv4 packet -- CLAT translation --> IPv6 packet
+ for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+ sendUploadPacketUdp(tethered.macAddr, tethered.routerMacAddr, tethered.ipv4Addr,
+ REMOTE_IP4_ADDR, tester, true /* is4To6 */);
+ }
+
+ // Send an IPv6 UDP packet in reply direction.
+ // IPv6 packet -- CLAT translation --> IPv4 packet
+ for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+ sendDownloadPacketUdp(REMOTE_NAT64_ADDR, clatIp6, tester, true /* is6To4 */);
+ }
+
+ // Send fragmented IPv6 UDP packets in the reply direction.
+ // IPv6 frament packet -- CLAT translation --> IPv4 fragment packet
+ final int payloadLen = 1500;
+ final int l2mtu = 1000;
+ final int fragPktCnt = 2; // 1500 bytes of UDP payload were fragmented into two packets.
+ final long fragRxBytes = payloadLen + UDP_HEADER_LEN + fragPktCnt * IPV4_HEADER_MIN_LEN;
+ final byte[] payload = new byte[payloadLen];
+ // Initialize the payload with random bytes.
+ Random random = new Random();
+ random.nextBytes(payload);
+ sendDownloadFragmentedUdpPackets(REMOTE_NAT64_ADDR, clatIp6, tester,
+ ByteBuffer.wrap(payload), l2mtu);
+
+ // After sending test packets, get stats again to verify their differences.
+ final ClatEgress4Value newEgress4 = getClatEgress4Value(clatIfaceIndex);
+ final ClatIngress6Value newIngress6 = getClatIngress6Value(ifaceIndex);
+ assertNotNull(newEgress4);
+ assertNotNull(newIngress6);
+
+ assertEquals(RX_UDP_PACKET_COUNT + fragPktCnt, newIngress6.packets - oldIngress6.packets);
+ assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE + fragRxBytes,
+ newIngress6.bytes - oldIngress6.bytes);
+ assertEquals(TX_UDP_PACKET_COUNT, newEgress4.packets - oldEgress4.packets);
+ // The increase in egress traffic equals the expected size of the translated UDP packets.
+ // Calculation:
+ // - Original UDP packet was TX_UDP_PACKET_SIZE bytes (IPv4 header + UDP header + payload).
+ // - After CLAT translation, each packet is now:
+ // IPv6 header + unchanged UDP header + unchanged payload
+ // Therefore, the total size of the translated UDP packet should be:
+ // TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN
+ assertEquals(
+ TX_UDP_PACKET_COUNT * (TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN),
+ newEgress4.bytes - oldEgress4.bytes);
+ }
}
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index c4d5636..1f1929c 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -26,7 +26,7 @@
target_sdk_version: "33",
libs: [
- "android.test.base",
+ "android.test.base.stubs",
],
srcs: [
diff --git a/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
index ebf09ed..0f3f5bb 100644
--- a/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
@@ -43,7 +43,7 @@
import com.android.networkstack.tethering.util.TetheringUtils;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
-import com.android.testutils.TapPacketReader;
+import com.android.testutils.PollPacketReader;
import com.android.testutils.TapPacketReaderRule;
import org.junit.After;
@@ -75,7 +75,7 @@
private InterfaceParams mUpstreamParams, mTetheredParams;
private HandlerThread mHandlerThread;
private Handler mHandler;
- private TapPacketReader mUpstreamPacketReader, mTetheredPacketReader;
+ private PollPacketReader mUpstreamPacketReader, mTetheredPacketReader;
private static INetd sNetd;
@@ -219,7 +219,7 @@
}
// TODO: change to assert.
- private boolean waitForPacket(ByteBuffer packet, TapPacketReader reader) {
+ private boolean waitForPacket(ByteBuffer packet, PollPacketReader reader) {
byte[] p;
while ((p = reader.popPacket(PACKET_TIMEOUT_MS)) != null) {
@@ -247,7 +247,7 @@
}
private void receivePacketAndMaybeExpectForwarded(boolean expectForwarded,
- ByteBuffer in, TapPacketReader inReader, ByteBuffer out, TapPacketReader outReader)
+ ByteBuffer in, PollPacketReader inReader, ByteBuffer out, PollPacketReader outReader)
throws IOException {
inReader.sendResponse(in);
@@ -271,13 +271,13 @@
assertEquals(msg, expectForwarded, waitForPacket(out, outReader));
}
- private void receivePacketAndExpectForwarded(ByteBuffer in, TapPacketReader inReader,
- ByteBuffer out, TapPacketReader outReader) throws IOException {
+ private void receivePacketAndExpectForwarded(ByteBuffer in, PollPacketReader inReader,
+ ByteBuffer out, PollPacketReader outReader) throws IOException {
receivePacketAndMaybeExpectForwarded(true, in, inReader, out, outReader);
}
- private void receivePacketAndExpectNotForwarded(ByteBuffer in, TapPacketReader inReader,
- ByteBuffer out, TapPacketReader outReader) throws IOException {
+ private void receivePacketAndExpectNotForwarded(ByteBuffer in, PollPacketReader inReader,
+ ByteBuffer out, PollPacketReader outReader) throws IOException {
receivePacketAndMaybeExpectForwarded(false, in, inReader, out, outReader);
}
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
index 90ceaa1..7cc8c74 100644
--- a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -64,7 +64,7 @@
import com.android.net.module.util.structs.PrefixInformationOption;
import com.android.net.module.util.structs.RaHeader;
import com.android.net.module.util.structs.RdnssOption;
-import com.android.testutils.TapPacketReader;
+import com.android.testutils.PollPacketReader;
import com.android.testutils.TapPacketReaderRule;
import org.junit.After;
@@ -93,7 +93,7 @@
private InterfaceParams mTetheredParams;
private HandlerThread mHandlerThread;
private Handler mHandler;
- private TapPacketReader mTetheredPacketReader;
+ private PollPacketReader mTetheredPacketReader;
private RouterAdvertisementDaemon mRaDaemon;
private static INetd sNetd;
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 24407ca..d0d23ac 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -62,9 +62,9 @@
// remove framework-minus-apex, ext, and framework-res
sdk_version: "core_platform",
libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
"ext",
"framework-minus-apex",
"framework-res",
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 177296a..680e81d 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -89,13 +89,10 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.RoutingCoordinatorManager;
-import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.tethering.BpfCoordinator;
-import com.android.networkstack.tethering.PrivateAddressCoordinator;
import com.android.networkstack.tethering.TetheringConfiguration;
import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
@@ -139,6 +136,7 @@
private static final boolean DEFAULT_USING_BPF_OFFLOAD = true;
private static final int DEFAULT_SUBNET_PREFIX_LENGTH = 0;
private static final int P2P_SUBNET_PREFIX_LENGTH = 25;
+ private static final String LEGACY_WIFI_P2P_IFACE_ADDRESS = "192.168.49.1/24";
private static final InterfaceParams TEST_IFACE_PARAMS = new InterfaceParams(
IFACE_NAME, 42 /* index */, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
@@ -174,7 +172,6 @@
@Mock private DadProxy mDadProxy;
@Mock private RouterAdvertisementDaemon mRaDaemon;
@Mock private IpServer.Dependencies mDependencies;
- @Mock private PrivateAddressCoordinator mAddressCoordinator;
@Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
@Mock private NetworkStatsManager mStatsManager;
@Mock private TetheringConfiguration mTetherConfig;
@@ -196,6 +193,12 @@
private void initStateMachine(int interfaceType, boolean usingLegacyDhcp,
boolean usingBpfOffload) throws Exception {
+ initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload,
+ false /* shouldEnableWifiP2pDedicatedIp */);
+ }
+
+ private void initStateMachine(int interfaceType, boolean usingLegacyDhcp,
+ boolean usingBpfOffload, boolean shouldEnableWifiP2pDedicatedIp) throws Exception {
when(mDependencies.getDadProxy(any(), any())).thenReturn(mDadProxy);
when(mDependencies.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon);
when(mDependencies.getInterfaceParams(IFACE_NAME)).thenReturn(TEST_IFACE_PARAMS);
@@ -213,6 +216,8 @@
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
+ when(mTetherConfig.shouldEnableWifiP2pDedicatedIp())
+ .thenReturn(shouldEnableWifiP2pDedicatedIp);
when(mBpfCoordinator.isUsingBpfOffload()).thenReturn(usingBpfOffload);
mIpServer = createIpServer(interfaceType);
mIpServer.start();
@@ -252,9 +257,9 @@
verify(mBpfCoordinator).updateIpv6UpstreamInterface(
mIpServer, interfaceParams.index, upstreamPrefixes);
}
- reset(mNetd, mBpfCoordinator, mCallback, mAddressCoordinator);
- when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
- anyBoolean())).thenReturn(mTestAddress);
+ reset(mNetd, mBpfCoordinator, mCallback, mRoutingCoordinatorManager);
+ when(mRoutingCoordinatorManager.requestStickyDownstreamAddress(anyInt(), anyInt(),
+ any())).thenReturn(mTestAddress);
}
@SuppressWarnings("DoNotCall") // Ignore warning for synchronous to call to Thread.run()
@@ -275,8 +280,9 @@
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mSharedLog.forSubComponent(anyString())).thenReturn(mSharedLog);
- when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
- anyBoolean())).thenReturn(mTestAddress);
+ when(mRoutingCoordinatorManager.requestStickyDownstreamAddress(anyInt(), anyInt(),
+ any())).thenReturn(mTestAddress);
+ when(mRoutingCoordinatorManager.requestDownstreamAddress(any())).thenReturn(mTestAddress);
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
@@ -288,7 +294,7 @@
mLooper = new TestLooper();
mHandler = new Handler(mLooper.getLooper());
return new IpServer(IFACE_NAME, mHandler, interfaceType, mSharedLog, mNetd, mBpfCoordinator,
- mRoutingCoordinatorManager, mCallback, mTetherConfig, mAddressCoordinator,
+ mRoutingCoordinatorManager, mCallback, mTetherConfig,
mTetheringMetrics, mDependencies);
}
@@ -340,10 +346,14 @@
initStateMachine(TETHERING_BLUETOOTH);
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
- InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
+ InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
if (isAtLeastT()) {
- inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
- eq(CONNECTIVITY_SCOPE_GLOBAL), eq(true));
+ inOrder.verify(mRoutingCoordinatorManager)
+ .requestStickyDownstreamAddress(
+ eq(TETHERING_BLUETOOTH),
+ eq(CONNECTIVITY_SCOPE_GLOBAL),
+ any());
+ inOrder.verify(mRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
}
@@ -364,7 +374,7 @@
initTetheredStateMachine(TETHERING_BLUETOOTH, null);
dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
- InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
+ InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
inOrder.verify(mNetd).tetherApplyDnsInterfaces();
inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
@@ -375,7 +385,7 @@
argThat(cfg -> assertContainsFlag(cfg.flags, IF_STATE_DOWN)));
}
inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> cfg.flags.length == 0));
- inOrder.verify(mAddressCoordinator).releaseDownstream(any());
+ inOrder.verify(mRoutingCoordinatorManager).releaseDownstream(any());
inOrder.verify(mCallback).updateInterfaceState(
mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
inOrder.verify(mCallback).updateLinkProperties(
@@ -383,7 +393,7 @@
verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_BLUETOOTH),
eq(TETHER_ERROR_NO_ERROR));
verify(mTetheringMetrics).sendReport(eq(TETHERING_BLUETOOTH));
- verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
+ verifyNoMoreInteractions(mNetd, mCallback, mRoutingCoordinatorManager);
}
@Test
@@ -391,9 +401,10 @@
initStateMachine(TETHERING_USB);
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
- InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
- inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
- eq(CONNECTIVITY_SCOPE_GLOBAL), eq(true));
+ InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
+ inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
+ eq(CONNECTIVITY_SCOPE_GLOBAL), any());
+ inOrder.verify(mRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
@@ -405,17 +416,18 @@
inOrder.verify(mCallback).updateLinkProperties(
eq(mIpServer), mLinkPropertiesCaptor.capture());
assertIPv4AddressAndDirectlyConnectedRoute(mLinkPropertiesCaptor.getValue());
- verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
+ verifyNoMoreInteractions(mNetd, mCallback, mRoutingCoordinatorManager);
}
@Test
- public void canBeTetheredAsWifiP2p() throws Exception {
+ public void canBeTetheredAsWifiP2p_NotUsingDedicatedIp() throws Exception {
initStateMachine(TETHERING_WIFI_P2P);
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
- InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
- inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
- eq(CONNECTIVITY_SCOPE_LOCAL), eq(true));
+ InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
+ inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
+ eq(CONNECTIVITY_SCOPE_LOCAL), any());
+ inOrder.verify(mRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
IFACE_NAME.equals(cfg.ifName) && assertNotContainsFlag(cfg.flags, IF_STATE_UP)));
inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
@@ -427,7 +439,35 @@
inOrder.verify(mCallback).updateLinkProperties(
eq(mIpServer), mLinkPropertiesCaptor.capture());
assertIPv4AddressAndDirectlyConnectedRoute(mLinkPropertiesCaptor.getValue());
- verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
+ verifyNoMoreInteractions(mNetd, mCallback, mRoutingCoordinatorManager);
+ }
+
+ @Test
+ public void canBeTetheredAsWifiP2p_UsingDedicatedIp() throws Exception {
+ initStateMachine(TETHERING_WIFI_P2P, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD,
+ true /* shouldEnableWifiP2pDedicatedIp */);
+
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
+ // When using WiFi P2p dedicated IP, the IpServer just picks the IP address without
+ // requesting for it at RoutingCoordinatorManager.
+ inOrder.verify(mRoutingCoordinatorManager, never())
+ .requestStickyDownstreamAddress(anyInt(), anyInt(), any());
+ inOrder.verify(mRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+ IFACE_NAME.equals(cfg.ifName) && assertNotContainsFlag(cfg.flags, IF_STATE_UP)));
+ inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
+ inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+ inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
+ any(), any());
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertIPv4AddressAndDirectlyConnectedRoute(mLinkPropertiesCaptor.getValue());
+ assertEquals(List.of(new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS)),
+ mLinkPropertiesCaptor.getValue().getLinkAddresses());
+ verifyNoMoreInteractions(mNetd, mCallback, mRoutingCoordinatorManager);
}
@Test
@@ -533,15 +573,9 @@
initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
clearInvocations(
- mNetd, mCallback, mAddressCoordinator, mBpfCoordinator, mRoutingCoordinatorManager);
+ mNetd, mCallback, mBpfCoordinator, mRoutingCoordinatorManager);
dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
- InOrder inOrder =
- inOrder(
- mNetd,
- mCallback,
- mAddressCoordinator,
- mBpfCoordinator,
- mRoutingCoordinatorManager);
+ InOrder inOrder = inOrder(mNetd, mCallback, mBpfCoordinator, mRoutingCoordinatorManager);
inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mRoutingCoordinatorManager)
.removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
@@ -556,15 +590,14 @@
inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
inOrder.verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg(
argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
- inOrder.verify(mAddressCoordinator).releaseDownstream(any());
+ inOrder.verify(mRoutingCoordinatorManager).releaseDownstream(any());
inOrder.verify(mBpfCoordinator).tetherOffloadClientClear(mIpServer);
inOrder.verify(mBpfCoordinator).removeIpServer(mIpServer);
inOrder.verify(mCallback).updateInterfaceState(
mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
inOrder.verify(mCallback).updateLinkProperties(
eq(mIpServer), any(LinkProperties.class));
- verifyNoMoreInteractions(
- mNetd, mCallback, mAddressCoordinator, mBpfCoordinator, mRoutingCoordinatorManager);
+ verifyNoMoreInteractions(mNetd, mCallback, mRoutingCoordinatorManager, mBpfCoordinator);
}
@Test
@@ -701,9 +734,10 @@
final ArgumentCaptor<LinkProperties> lpCaptor =
ArgumentCaptor.forClass(LinkProperties.class);
- InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator);
- inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
- eq(CONNECTIVITY_SCOPE_LOCAL), eq(true));
+ InOrder inOrder = inOrder(mNetd, mCallback, mRoutingCoordinatorManager);
+ inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
+ eq(CONNECTIVITY_SCOPE_LOCAL), any());
+ inOrder.verify(mRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
// One for ipv4 route, one for ipv6 link local route.
inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
@@ -711,18 +745,18 @@
inOrder.verify(mCallback).updateInterfaceState(
mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR);
inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture());
- verifyNoMoreInteractions(mCallback, mAddressCoordinator);
+ verifyNoMoreInteractions(mCallback, mRoutingCoordinatorManager);
// Simulate the DHCP server receives DHCPDECLINE on MirrorLink and then signals
// onNewPrefixRequest callback.
final LinkAddress newAddress = new LinkAddress("192.168.100.125/24");
- when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
- anyBoolean())).thenReturn(newAddress);
+ when(mRoutingCoordinatorManager.requestDownstreamAddress(any())).thenReturn(newAddress);
eventCallbacks.onNewPrefixRequest(new IpPrefix("192.168.42.0/24"));
mLooper.dispatchAll();
- inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
- eq(CONNECTIVITY_SCOPE_LOCAL), eq(false));
+ inOrder.verify(mRoutingCoordinatorManager, never())
+ .requestStickyDownstreamAddress(anyInt(), anyInt(), any());
+ inOrder.verify(mRoutingCoordinatorManager).requestDownstreamAddress(any());
inOrder.verify(mNetd).tetherApplyDnsInterfaces();
inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture());
verifyNoMoreInteractions(mCallback);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index e54a7e0..dd10cc3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -48,6 +48,7 @@
import static com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED;
import static com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE;
import static com.android.net.module.util.netlink.StructNdMsg.NUD_STALE;
+import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_METRICS_UPDATE_INTERVAL_MS;
import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS;
import static com.android.networkstack.tethering.BpfCoordinator.INVALID_MTU;
import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED;
@@ -60,6 +61,7 @@
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ACTIVE_SESSIONS_METRICS;
import static com.android.testutils.MiscAsserts.assertSameElements;
import static org.junit.Assert.assertArrayEquals;
@@ -87,6 +89,7 @@
import static org.mockito.Mockito.when;
import android.app.usage.NetworkStatsManager;
+import android.content.Context;
import android.net.INetd;
import android.net.InetAddresses;
import android.net.IpPrefix;
@@ -140,6 +143,8 @@
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.TestBpfMap;
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag;
import org.junit.Before;
import org.junit.Rule;
@@ -171,6 +176,16 @@
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+ final HashMap<String, Boolean> mFeatureFlags = new HashMap<>();
+ // This will set feature flags from @FeatureFlag annotations
+ // into the map before setUp() runs.
+ @Rule
+ public final SetFeatureFlagsRule mSetFeatureFlagsRule =
+ new SetFeatureFlagsRule((name, enabled) -> {
+ mFeatureFlags.put(name, enabled);
+ return null;
+ }, (name) -> mFeatureFlags.getOrDefault(name, false));
+
private static final boolean IPV4 = true;
private static final boolean IPV6 = false;
@@ -406,6 +421,11 @@
return this;
}
+ public Builder setPrivateAddress(Inet4Address privateAddr) {
+ mPrivateAddr = privateAddr;
+ return this;
+ }
+
public Builder setRemotePort(int remotePort) {
mRemotePort = (short) remotePort;
return this;
@@ -429,6 +449,7 @@
@Mock private NetworkStatsManager mStatsManager;
@Mock private INetd mNetd;
+ @Mock private Context mMockContext;
@Mock private IpServer mIpServer;
@Mock private IpServer mIpServer2;
@Mock private TetheringConfiguration mTetherConfig;
@@ -475,6 +496,11 @@
}
@NonNull
+ public Context getContext() {
+ return mMockContext;
+ }
+
+ @NonNull
public INetd getNetd() {
return mNetd;
}
@@ -546,6 +572,16 @@
public IBpfMap<S32, S32> getBpfErrorMap() {
return mBpfErrorMap;
}
+
+ @Override
+ public void sendTetheringActiveSessionsReported(int lastMaxSessionCount) {
+ // No-op.
+ }
+
+ @Override
+ public boolean isFeatureEnabled(Context context, String name) {
+ return mFeatureFlags.getOrDefault(name, false);
+ }
});
@Before public void setUp() {
@@ -1977,6 +2013,217 @@
verify(mBpfDevMap, never()).updateEntry(any(), any());
}
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
+ // BPF IPv4 forwarding only supports on S+.
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testMaxConnectionCount_metricsEnabled() throws Exception {
+ doTestMaxConnectionCount(true);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false)
+ @Test
+ public void testMaxConnectionCount_metricsDisabled() throws Exception {
+ doTestMaxConnectionCount(false);
+ }
+
+ private void doTestMaxConnectionCount(final boolean supportActiveSessionsMetrics)
+ throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+ resetNetdAndBpfMaps();
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Prepare add/delete rule events.
+ final ArrayList<ConntrackEvent> addRuleEvents = new ArrayList<>();
+ final ArrayList<ConntrackEvent> delRuleEvents = new ArrayList<>();
+ for (int i = 0; i < 5; i++) {
+ final ConntrackEvent addEvent = new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_NEW).setProto(IPPROTO_TCP).setRemotePort(i).build();
+ addRuleEvents.add(addEvent);
+ final ConntrackEvent delEvent = new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build();
+ delRuleEvents.add(delEvent);
+ }
+
+ // Add rules, verify counter increases.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(addRuleEvents.get(i));
+ assertEquals(supportActiveSessionsMetrics ? i + 1 : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ }
+
+ // Add the same events again should not increase the counter because
+ // all events are already exist.
+ for (final ConntrackEvent event : addRuleEvents) {
+ mConsumer.accept(event);
+ assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ }
+
+ // Verify removing non-existent items won't change the counters.
+ for (int i = 5; i < 8; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ }
+
+ // Verify remove the rules decrease the counter.
+ // Note the max counter returns the max, so it returns the count before deleting.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(delRuleEvents.get(i));
+ }
+ // The maximum number of rules observed is still 5.
+ assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ // After the reset, the maximum number of rules observed is 0.
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Verify remove these rules again doesn't decrease the counter.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(delRuleEvents.get(i));
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+ }
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
+ // BPF IPv4 forwarding only supports on S+.
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void doTestMaxConnectionCount_removeClient_metricsEnabled() throws Exception {
+ doTestMaxConnectionCount_removeClient(true);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false)
+ @Test
+ public void doTestMaxConnectionCount_removeClient_metricsDisabled() throws Exception {
+ doTestMaxConnectionCount_removeClient(false);
+ }
+
+ private void doTestMaxConnectionCount_removeClient(final boolean supportActiveSessionsMetrics)
+ throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+ resetNetdAndBpfMaps();
+
+ // Add client information A and B on on the same downstream.
+ final ClientInfo clientA = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+ PRIVATE_ADDR, MAC_A);
+ final ClientInfo clientB = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+ PRIVATE_ADDR2, MAC_B);
+ coordinator.tetherOffloadClientAdd(mIpServer, clientA);
+ coordinator.tetherOffloadClientAdd(mIpServer, clientB);
+ assertClientInfoExists(mIpServer, clientA);
+ assertClientInfoExists(mIpServer, clientB);
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Add some rules for both clients.
+ final int addr1RuleCount = 5;
+ final int addr2RuleCount = 3;
+
+ for (int i = 0; i < addr1RuleCount; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder()
+ .setMsgType(IPCTNL_MSG_CT_NEW)
+ .setProto(IPPROTO_TCP)
+ .setRemotePort(i)
+ .setPrivateAddress(PRIVATE_ADDR)
+ .build());
+ }
+
+ for (int i = addr1RuleCount; i < addr1RuleCount + addr2RuleCount; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder()
+ .setMsgType(IPCTNL_MSG_CT_NEW)
+ .setProto(IPPROTO_TCP)
+ .setRemotePort(i)
+ .setPrivateAddress(PRIVATE_ADDR2)
+ .build());
+ }
+
+ assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Remove 1 client. Since the 1st poll will return the LastMaxCounter and
+ // update it to the current, the max counter will be kept at 1st poll.
+ coordinator.tetherOffloadClientRemove(mIpServer, clientA);
+ assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ // And the counter be updated at 2nd poll.
+ assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Remove other client.
+ coordinator.tetherOffloadClientRemove(mIpServer, clientB);
+ assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
+ coordinator.getLastMaxConnectionAndResetToCurrent());
+ // Verify the counter reach zero at 2nd poll.
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
+ // BPF IPv4 forwarding only supports on S+.
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testSendActiveSessionsReported_metricsEnabled() throws Exception {
+ doTestSendActiveSessionsReported(true);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false)
+ @Test
+ public void testSendActiveSessionsReported_metricsDisabled() throws Exception {
+ doTestSendActiveSessionsReported(false);
+ }
+
+ private void doTestSendActiveSessionsReported(final boolean supportActiveSessionsMetrics)
+ throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+ resetNetdAndBpfMaps();
+ assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
+
+ // Prepare the counter value.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_NEW).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ }
+
+ // Then delete some 3 rules, 2 rules remaining.
+ // The max count is 5 while current rules count is 2.
+ for (int i = 0; i < 3; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ }
+
+ // Verify the method is not invoked when timer is not expired.
+ waitForIdle();
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+
+ // Verify metrics will be sent upon timer expiry.
+ mTestLooper.moveTimeForward(CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ if (supportActiveSessionsMetrics) {
+ verify(mDeps).sendTetheringActiveSessionsReported(5);
+ } else {
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
+ // Verify next uploaded metrics will reflect the decreased rules count.
+ mTestLooper.moveTimeForward(CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ if (supportActiveSessionsMetrics) {
+ verify(mDeps).sendTetheringActiveSessionsReported(2);
+ } else {
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
+ // Verify no metrics uploaded if polling stopped.
+ clearInvocations(mDeps);
+ coordinator.removeIpServer(mIpServer);
+ mTestLooper.moveTimeForward(CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
private void setElapsedRealtimeNanos(long nanoSec) {
mElapsedRealtimeNanos = nanoSec;
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index c2e1617..51c2d56 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -38,6 +38,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -71,16 +72,19 @@
import android.os.ResultReceiver;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.UserManager;
import android.os.test.TestLooper;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
+import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.ArrayTrackRecord;
import com.android.net.module.util.SharedLog;
import com.android.testutils.DevSdkIgnoreRule;
@@ -114,6 +118,7 @@
@Mock private EntitlementManager
.OnTetherProvisioningFailedListener mTetherProvisioningFailedListener;
@Mock private AlarmManager mAlarmManager;
+ @Mock private UserManager mUserManager;
@Mock private PendingIntent mAlarmIntent;
@Rule
@@ -126,9 +131,10 @@
private MockContext mMockContext;
private Runnable mPermissionChangeCallback;
- private WrappedEntitlementManager mEnMgr;
+ private EntitlementManager mEnMgr;
private TetheringConfiguration mConfig;
private MockitoSession mMockingSession;
+ private TestDependencies mDeps;
private class MockContext extends BroadcastInterceptingContext {
MockContext(Context base) {
@@ -143,19 +149,30 @@
@Override
public Object getSystemService(String name) {
if (Context.ALARM_SERVICE.equals(name)) return mAlarmManager;
+ if (Context.USER_SERVICE.equals(name)) return mUserManager;
return super.getSystemService(name);
}
+
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ if (UserManager.class.equals(serviceClass)) return Context.USER_SERVICE;
+ return super.getSystemServiceName(serviceClass);
+ }
+
+ @Override
+ public Context createContextAsUser(UserHandle user, int flags) {
+ return mMockContext; // Return self for easier test injection.
+ }
}
- public class WrappedEntitlementManager extends EntitlementManager {
+ class TestDependencies extends EntitlementManager.Dependencies {
public int fakeEntitlementResult = TETHER_ERROR_ENTITLEMENT_UNKNOWN;
public int uiProvisionCount = 0;
public int silentProvisionCount = 0;
-
- public WrappedEntitlementManager(Context ctx, Handler h, SharedLog log,
- Runnable callback) {
- super(ctx, h, log, callback);
+ TestDependencies(@NonNull Context context,
+ @NonNull SharedLog log) {
+ super(context, log);
}
public void reset() {
@@ -168,9 +185,12 @@
protected Intent runUiTetherProvisioning(int type,
final TetheringConfiguration config, final ResultReceiver receiver) {
Intent intent = super.runUiTetherProvisioning(type, config, receiver);
- assertUiTetherProvisioningIntent(type, config, receiver, intent);
- uiProvisionCount++;
- receiver.send(fakeEntitlementResult, null);
+ if (intent != null) {
+ assertUiTetherProvisioningIntent(type, config, receiver, intent);
+ uiProvisionCount++;
+ // If the intent is null, the result is sent by the underlying method.
+ receiver.send(fakeEntitlementResult, null);
+ }
return intent;
}
@@ -195,7 +215,7 @@
Intent intent = super.runSilentTetherProvisioning(type, config, receiver);
assertSilentTetherProvisioning(type, config, intent);
silentProvisionCount++;
- addDownstreamMapping(type, fakeEntitlementResult);
+ mEnMgr.addDownstreamMapping(type, fakeEntitlementResult);
return intent;
}
@@ -217,6 +237,13 @@
assertEquals(TEST_PACKAGE_NAME, pkgName);
return mAlarmIntent;
}
+
+ @Override
+ int getCurrentUser() {
+ // The result is not used, just override to bypass the need of accessing
+ // the static method.
+ return 0;
+ }
}
@Before
@@ -253,11 +280,13 @@
false);
when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
when(mLog.forSubComponent(anyString())).thenReturn(mLog);
+ doReturn(true).when(mUserManager).isAdminUser();
mMockContext = new MockContext(mContext);
+ mDeps = new TestDependencies(mMockContext, mLog);
mPermissionChangeCallback = spy(() -> { });
- mEnMgr = new WrappedEntitlementManager(mMockContext, new Handler(mLooper.getLooper()), mLog,
- mPermissionChangeCallback);
+ mEnMgr = new EntitlementManager(mMockContext, new Handler(mLooper.getLooper()), mLog,
+ mPermissionChangeCallback, mDeps);
mEnMgr.setOnTetherProvisioningFailedListener(mTetherProvisioningFailedListener);
mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
mEnMgr.setTetheringConfigurationFetcher(() -> {
@@ -320,102 +349,46 @@
@Test
public void testRequestLastEntitlementCacheValue() throws Exception {
// 1. Entitlement check is not required.
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- ResultReceiver receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
- mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- mEnMgr.reset();
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_NO_ERROR, true);
+ assertEquals(0, mDeps.uiProvisionCount);
+ mDeps.reset();
setupForRequiredProvisioning();
// 2. No cache value and don't need to run entitlement check.
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
- mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- mEnMgr.reset();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_ENTITLEMENT_UNKNOWN, false);
+ assertEquals(0, mDeps.uiProvisionCount);
+ mDeps.reset();
// 3. No cache value and ui entitlement check is needed.
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
- mLooper.dispatchAll();
- assertEquals(1, mEnMgr.uiProvisionCount);
- mEnMgr.reset();
+ mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_PROVISIONING_FAILED, true);
+ assertEquals(1, mDeps.uiProvisionCount);
+ mDeps.reset();
// 4. Cache value is TETHER_ERROR_PROVISIONING_FAILED and don't need to run entitlement
// check.
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
- mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- mEnMgr.reset();
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_PROVISIONING_FAILED, false);
+ assertEquals(0, mDeps.uiProvisionCount);
+ mDeps.reset();
// 5. Cache value is TETHER_ERROR_PROVISIONING_FAILED and ui entitlement check is needed.
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
- mLooper.dispatchAll();
- assertEquals(1, mEnMgr.uiProvisionCount);
- mEnMgr.reset();
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_NO_ERROR, true);
+ assertEquals(1, mDeps.uiProvisionCount);
+ mDeps.reset();
// 6. Cache value is TETHER_ERROR_NO_ERROR.
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
- mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- mEnMgr.reset();
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_NO_ERROR, true);
+ assertEquals(0, mDeps.uiProvisionCount);
+ mDeps.reset();
// 7. Test get value for other downstream type.
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
- mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- mEnMgr.reset();
+ assertLatestEntitlementResult(TETHERING_USB, TETHER_ERROR_ENTITLEMENT_UNKNOWN, false);
+ assertEquals(0, mDeps.uiProvisionCount);
+ mDeps.reset();
// 8. Test get value for invalid downstream type.
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI_P2P, receiver, true);
- mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- mEnMgr.reset();
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ assertLatestEntitlementResult(TETHERING_WIFI_P2P, TETHER_ERROR_ENTITLEMENT_UNKNOWN, true);
+ assertEquals(0, mDeps.uiProvisionCount);
+ mDeps.reset();
}
private void assertPermissionChangeCallback(InOrder inOrder) {
@@ -431,7 +404,7 @@
final InOrder inOrder = inOrder(mPermissionChangeCallback);
setupForRequiredProvisioning();
mEnMgr.notifyUpstream(true);
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
mLooper.dispatchAll();
// Permitted: true -> false
@@ -443,7 +416,7 @@
// Permitted: false -> false
assertNoPermissionChange(inOrder);
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
mLooper.dispatchAll();
// Permitted: false -> true
@@ -456,21 +429,21 @@
final InOrder inOrder = inOrder(mPermissionChangeCallback);
setupForRequiredProvisioning();
mEnMgr.notifyUpstream(true);
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
mLooper.dispatchAll();
// Permitted: true -> false
assertPermissionChangeCallback(inOrder);
assertFalse(mEnMgr.isCellularUpstreamPermitted());
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
mLooper.dispatchAll();
// Permitted: false -> false
assertNoPermissionChange(inOrder);
assertFalse(mEnMgr.isCellularUpstreamPermitted());
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true);
mLooper.dispatchAll();
// Permitted: false -> false
@@ -483,14 +456,14 @@
final InOrder inOrder = inOrder(mPermissionChangeCallback);
setupForRequiredProvisioning();
mEnMgr.notifyUpstream(true);
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
mLooper.dispatchAll();
// Permitted: true -> true
assertNoPermissionChange(inOrder);
assertTrue(mEnMgr.isCellularUpstreamPermitted());
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
mLooper.dispatchAll();
// Permitted: true -> true
@@ -519,89 +492,89 @@
final InOrder inOrder = inOrder(mPermissionChangeCallback);
setupForRequiredProvisioning();
// 1. start ui provisioning, upstream is mobile
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
mLooper.dispatchAll();
- assertEquals(1, mEnMgr.uiProvisionCount);
- assertEquals(0, mEnMgr.silentProvisionCount);
+ assertEquals(1, mDeps.uiProvisionCount);
+ assertEquals(0, mDeps.silentProvisionCount);
// Permitted: true -> true
assertNoPermissionChange(inOrder);
assertTrue(mEnMgr.isCellularUpstreamPermitted());
- mEnMgr.reset();
+ mDeps.reset();
// 2. start no-ui provisioning
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, false);
mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- assertEquals(1, mEnMgr.silentProvisionCount);
+ assertEquals(0, mDeps.uiProvisionCount);
+ assertEquals(1, mDeps.silentProvisionCount);
// Permitted: true -> true
assertNoPermissionChange(inOrder);
assertTrue(mEnMgr.isCellularUpstreamPermitted());
- mEnMgr.reset();
+ mDeps.reset();
// 3. tear down mobile, then start ui provisioning
mEnMgr.notifyUpstream(false);
mLooper.dispatchAll();
mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true);
mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- assertEquals(0, mEnMgr.silentProvisionCount);
+ assertEquals(0, mDeps.uiProvisionCount);
+ assertEquals(0, mDeps.silentProvisionCount);
assertNoPermissionChange(inOrder);
- mEnMgr.reset();
+ mDeps.reset();
// 4. switch upstream back to mobile
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
- assertEquals(1, mEnMgr.uiProvisionCount);
- assertEquals(0, mEnMgr.silentProvisionCount);
+ assertEquals(1, mDeps.uiProvisionCount);
+ assertEquals(0, mDeps.silentProvisionCount);
// Permitted: true -> true
assertNoPermissionChange(inOrder);
assertTrue(mEnMgr.isCellularUpstreamPermitted());
- mEnMgr.reset();
+ mDeps.reset();
// 5. tear down mobile, then switch SIM
mEnMgr.notifyUpstream(false);
mLooper.dispatchAll();
mEnMgr.reevaluateSimCardProvisioning(mConfig);
- assertEquals(0, mEnMgr.uiProvisionCount);
- assertEquals(0, mEnMgr.silentProvisionCount);
+ assertEquals(0, mDeps.uiProvisionCount);
+ assertEquals(0, mDeps.silentProvisionCount);
assertNoPermissionChange(inOrder);
- mEnMgr.reset();
+ mDeps.reset();
// 6. switch upstream back to mobile again
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- assertEquals(3, mEnMgr.silentProvisionCount);
+ assertEquals(0, mDeps.uiProvisionCount);
+ assertEquals(3, mDeps.silentProvisionCount);
// Permitted: true -> false
assertPermissionChangeCallback(inOrder);
assertFalse(mEnMgr.isCellularUpstreamPermitted());
- mEnMgr.reset();
+ mDeps.reset();
// 7. start ui provisioning, upstream is mobile, downstream is ethernet
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.startProvisioningIfNeeded(TETHERING_ETHERNET, true);
mLooper.dispatchAll();
- assertEquals(1, mEnMgr.uiProvisionCount);
- assertEquals(0, mEnMgr.silentProvisionCount);
+ assertEquals(1, mDeps.uiProvisionCount);
+ assertEquals(0, mDeps.silentProvisionCount);
// Permitted: false -> true
assertPermissionChangeCallback(inOrder);
assertTrue(mEnMgr.isCellularUpstreamPermitted());
- mEnMgr.reset();
+ mDeps.reset();
// 8. downstream is invalid
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI_P2P, true);
mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- assertEquals(0, mEnMgr.silentProvisionCount);
+ assertEquals(0, mDeps.uiProvisionCount);
+ assertEquals(0, mDeps.silentProvisionCount);
assertNoPermissionChange(inOrder);
- mEnMgr.reset();
+ mDeps.reset();
}
@Test
@@ -609,18 +582,82 @@
setupForRequiredProvisioning();
verify(mTetherProvisioningFailedListener, times(0))
.onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
mLooper.dispatchAll();
- assertEquals(1, mEnMgr.uiProvisionCount);
+ assertEquals(1, mDeps.uiProvisionCount);
verify(mTetherProvisioningFailedListener, times(1))
.onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
}
+ @IgnoreUpTo(SC_V2)
@Test
- public void testsetExemptedDownstreamType() throws Exception {
+ public void testUiProvisioningMultiUser_aboveT() {
+ doTestUiProvisioningMultiUser(true, 1);
+ doTestUiProvisioningMultiUser(false, 0);
+ }
+
+ @IgnoreAfter(SC_V2)
+ @Test
+ public void testUiProvisioningMultiUser_belowT() {
+ doTestUiProvisioningMultiUser(true, 1);
+ doTestUiProvisioningMultiUser(false, 1);
+ }
+
+ private static class TestableResultReceiver extends ResultReceiver {
+ private static final long DEFAULT_TIMEOUT_MS = 200L;
+ private final ArrayTrackRecord<Integer>.ReadHead mHistory =
+ new ArrayTrackRecord<Integer>().newReadHead();
+
+ TestableResultReceiver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ mHistory.add(resultCode);
+ }
+
+ void expectResult(int resultCode) {
+ final int event = mHistory.poll(DEFAULT_TIMEOUT_MS, it -> true);
+ assertEquals(resultCode, event);
+ }
+ }
+
+ void assertLatestEntitlementResult(int downstreamType, int expectedCode,
+ boolean showEntitlementUi) {
+ final TestableResultReceiver receiver = new TestableResultReceiver(null);
+ mEnMgr.requestLatestTetheringEntitlementResult(downstreamType, receiver, showEntitlementUi);
+ mLooper.dispatchAll();
+ receiver.expectResult(expectedCode);
+ }
+
+ private void doTestUiProvisioningMultiUser(boolean isAdminUser, int expectedUiProvisionCount) {
+ setupForRequiredProvisioning();
+ doReturn(isAdminUser).when(mUserManager).isAdminUser();
+
+ mDeps.reset();
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mEnMgr.notifyUpstream(true);
+ mLooper.dispatchAll();
+ mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+ mLooper.dispatchAll();
+ assertEquals(expectedUiProvisionCount, mDeps.uiProvisionCount);
+ if (expectedUiProvisionCount == 0) { // Failed to launch entitlement UI.
+ assertLatestEntitlementResult(TETHERING_USB, TETHER_ERROR_PROVISIONING_FAILED, false);
+ verify(mTetherProvisioningFailedListener).onTetherProvisioningFailed(TETHERING_USB,
+ FAILED_TETHERING_REASON);
+ } else {
+ assertLatestEntitlementResult(TETHERING_USB, TETHER_ERROR_NO_ERROR, false);
+ verify(mTetherProvisioningFailedListener, never()).onTetherProvisioningFailed(anyInt(),
+ anyString());
+ }
+ }
+
+ @Test
+ public void testSetExemptedDownstreamType() {
setupForRequiredProvisioning();
// Cellular upstream is not permitted when no entitlement result.
assertFalse(mEnMgr.isCellularUpstreamPermitted());
@@ -631,7 +668,7 @@
assertTrue(mEnMgr.isCellularUpstreamPermitted());
// If second downstream run entitlement check fail, cellular upstream is not permitted.
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
@@ -639,7 +676,7 @@
assertFalse(mEnMgr.isCellularUpstreamPermitted());
// When second downstream is down, exempted downstream can use cellular upstream.
- assertEquals(1, mEnMgr.uiProvisionCount);
+ assertEquals(1, mDeps.uiProvisionCount);
verify(mTetherProvisioningFailedListener).onTetherProvisioningFailed(TETHERING_USB,
FAILED_TETHERING_REASON);
mEnMgr.stopProvisioningIfNeeded(TETHERING_USB);
@@ -660,7 +697,7 @@
setupForRequiredProvisioning();
assertFalse(mEnMgr.isCellularUpstreamPermitted());
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
@@ -682,17 +719,10 @@
throws Exception {
setupCarrierConfig(false);
setupForRequiredProvisioning();
- mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- ResultReceiver receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
- mLooper.dispatchAll();
- assertEquals(0, mEnMgr.uiProvisionCount);
- mEnMgr.reset();
+ mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_PROVISIONING_FAILED, false);
+ assertEquals(0, mDeps.uiProvisionCount);
+ mDeps.reset();
}
@Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
index 3c07580..7fcc5f1 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
@@ -32,6 +32,8 @@
public class MockTetheringService extends TetheringService {
private final Tethering mTethering = mock(Tethering.class);
private final ArrayMap<String, Integer> mMockedPermissions = new ArrayMap<>();
+ private final ArrayMap<String, Integer> mMockedPackageUids = new ArrayMap<>();
+ private int mMockCallingUid;
@Override
public IBinder onBind(Intent intent) {
@@ -61,6 +63,17 @@
return super.checkCallingOrSelfPermission(permission);
}
+ @Override
+ boolean checkPackageNameMatchesUid(@NonNull Context context, int uid,
+ @NonNull String callingPackage) {
+ return mMockedPackageUids.getOrDefault(callingPackage, 0) == uid;
+ }
+
+ @Override
+ int getBinderCallingUid() {
+ return mMockCallingUid;
+ }
+
public Tethering getTethering() {
return mTethering;
}
@@ -91,5 +104,19 @@
mMockedPermissions.put(permission, granted);
}
}
+
+ /**
+ * Mock a package name matching a uid.
+ */
+ public void setPackageNameUid(String packageName, int uid) {
+ mMockedPackageUids.put(packageName, uid);
+ }
+
+ /**
+ * Mock a package name matching a uid.
+ */
+ public void setCallingUid(int uid) {
+ mMockCallingUid = uid;
+ }
}
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 2298a1a..1608e1a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -16,7 +16,6 @@
package com.android.networkstack.tethering;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
@@ -25,13 +24,20 @@
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.ip.IpServer.CMD_NOTIFY_PREFIX_CONFLICT;
+import static com.android.net.module.util.PrivateAddressCoordinator.TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION;
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.never;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -45,10 +51,14 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.ip.IpServer;
+import android.os.IBinder;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.IIpv4PrefixRequest;
+import com.android.net.module.util.PrivateAddressCoordinator;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -70,7 +80,7 @@
@Mock private IpServer mWifiP2pIpServer;
@Mock private Context mContext;
@Mock private ConnectivityManager mConnectivityMgr;
- @Mock private TetheringConfiguration mConfig;
+ @Mock private PrivateAddressCoordinator.Dependencies mDeps;
private PrivateAddressCoordinator mPrivateAddressCoordinator;
private final LinkAddress mBluetoothAddress = new LinkAddress("192.168.44.1/24");
@@ -90,12 +100,26 @@
new IpPrefix("172.16.0.0/12"),
new IpPrefix("10.0.0.0/8")));
+ private void setUpIpServer(IpServer ipServer, int interfaceType) throws Exception {
+ when(ipServer.interfaceType()).thenReturn(interfaceType);
+ final IIpv4PrefixRequest request = mock(IIpv4PrefixRequest.class);
+ when(ipServer.getIpv4PrefixRequest()).thenReturn(request);
+ when(request.asBinder()).thenReturn(mock(IBinder.class));
+ doAnswer(
+ invocation -> {
+ ipServer.sendMessage(CMD_NOTIFY_PREFIX_CONFLICT);
+ return null;
+ })
+ .when(request)
+ .onIpv4PrefixConflict(any());
+ }
+
private void setUpIpServers() throws Exception {
- when(mUsbIpServer.interfaceType()).thenReturn(TETHERING_USB);
- when(mEthernetIpServer.interfaceType()).thenReturn(TETHERING_ETHERNET);
- when(mHotspotIpServer.interfaceType()).thenReturn(TETHERING_WIFI);
- when(mLocalHotspotIpServer.interfaceType()).thenReturn(TETHERING_WIFI);
- when(mWifiP2pIpServer.interfaceType()).thenReturn(TETHERING_WIFI_P2P);
+ setUpIpServer(mUsbIpServer, TETHERING_USB);
+ setUpIpServer(mEthernetIpServer, TETHERING_ETHERNET);
+ setUpIpServer(mHotspotIpServer, TETHERING_WIFI);
+ setUpIpServer(mLocalHotspotIpServer, TETHERING_WIFI);
+ setUpIpServer(mWifiP2pIpServer, TETHERING_WIFI_P2P);
}
@Before
@@ -103,74 +127,58 @@
MockitoAnnotations.initMocks(this);
when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mConnectivityMgr);
+ when(mContext.getSystemService(ConnectivityManager.class)).thenReturn(mConnectivityMgr);
when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks);
- when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(false);
- when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(false);
setUpIpServers();
- mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
+ mPrivateAddressCoordinator =
+ spy(new PrivateAddressCoordinator(mConnectivityMgr::getAllNetworks, mDeps));
}
- private LinkAddress requestDownstreamAddress(final IpServer ipServer, int scope,
- boolean useLastAddress) {
- final LinkAddress address = mPrivateAddressCoordinator.requestDownstreamAddress(
- ipServer, scope, useLastAddress);
+ private LinkAddress requestStickyDownstreamAddress(final IpServer ipServer, int scope)
+ throws Exception {
+ final LinkAddress address =
+ mPrivateAddressCoordinator.requestStickyDownstreamAddress(
+ ipServer.interfaceType(), scope, ipServer.getIpv4PrefixRequest());
when(ipServer.getAddress()).thenReturn(address);
return address;
}
+ private LinkAddress requestDownstreamAddress(final IpServer ipServer) throws Exception {
+ final LinkAddress address =
+ mPrivateAddressCoordinator.requestDownstreamAddress(
+ ipServer.getIpv4PrefixRequest());
+ when(ipServer.getAddress()).thenReturn(address);
+ return address;
+ }
+
+ private void releaseDownstream(final IpServer ipServer) {
+ mPrivateAddressCoordinator.releaseDownstream(ipServer.getIpv4PrefixRequest());
+ }
+
+ private void updateUpstreamPrefix(UpstreamNetworkState ns) {
+ mPrivateAddressCoordinator.updateUpstreamPrefix(
+ ns.linkProperties, ns.networkCapabilities, ns.network);
+ }
+
@Test
public void testRequestDownstreamAddressWithoutUsingLastAddress() throws Exception {
final IpPrefix bluetoothPrefix = asIpPrefix(mBluetoothAddress);
- final LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
+ final LinkAddress address = requestDownstreamAddress(mHotspotIpServer);
final IpPrefix hotspotPrefix = asIpPrefix(address);
assertNotEquals(hotspotPrefix, bluetoothPrefix);
- final LinkAddress newAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
+ final LinkAddress newAddress = requestDownstreamAddress(mHotspotIpServer);
final IpPrefix newHotspotPrefix = asIpPrefix(newAddress);
assertNotEquals(hotspotPrefix, newHotspotPrefix);
assertNotEquals(bluetoothPrefix, newHotspotPrefix);
- final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
+ final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer);
final IpPrefix usbPrefix = asIpPrefix(usbAddress);
assertNotEquals(usbPrefix, bluetoothPrefix);
assertNotEquals(usbPrefix, newHotspotPrefix);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
- mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
- }
-
- @Test
- public void testSanitizedAddress() throws Exception {
- int fakeSubAddr = 0x2b00; // 43.0.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- LinkAddress actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.43.2/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
- fakeSubAddr = 0x2d01; // 45.1.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.45.2/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
- fakeSubAddr = 0x2eff; // 46.255.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.46.254/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
- fakeSubAddr = 0x2f05; // 47.5.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- assertEquals(new LinkAddress("192.168.47.5/24"), actualAddress);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
+ releaseDownstream(mHotspotIpServer);
+ releaseDownstream(mUsbIpServer);
}
@Test
@@ -178,63 +186,53 @@
// - Test bluetooth prefix is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
getSubAddress(mBluetoothAddress.getAddress().getAddress()));
- final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
+ final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer);
final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
+ releaseDownstream(mHotspotIpServer);
// - Test previous enabled hotspot prefix(cached prefix) is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
getSubAddress(hotspotAddress.getAddress().getAddress()));
- final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
+ final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer);
final IpPrefix usbPrefix = asIpPrefix(usbAddress);
assertNotEquals(asIpPrefix(mBluetoothAddress), usbPrefix);
assertNotEquals(hotspotPrefix, usbPrefix);
- mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
+ releaseDownstream(mUsbIpServer);
// - Test wifi p2p prefix is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
getSubAddress(mLegacyWifiP2pAddress.getAddress().getAddress()));
- final LinkAddress etherAddress = requestDownstreamAddress(mEthernetIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
+ final LinkAddress etherAddress = requestDownstreamAddress(mEthernetIpServer);
final IpPrefix etherPrefix = asIpPrefix(etherAddress);
assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix);
assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix);
assertNotEquals(hotspotPrefix, etherPrefix);
- mPrivateAddressCoordinator.releaseDownstream(mEthernetIpServer);
+ releaseDownstream(mEthernetIpServer);
}
@Test
public void testRequestLastDownstreamAddress() throws Exception {
- final int fakeHotspotSubAddr = 0x2b05; // 43.5
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
- final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
+ final LinkAddress hotspotAddress =
+ requestStickyDownstreamAddress(mHotspotIpServer, CONNECTIVITY_SCOPE_GLOBAL);
- final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.45.5/24"), usbAddress);
+ final LinkAddress usbAddress =
+ requestStickyDownstreamAddress(mUsbIpServer, CONNECTIVITY_SCOPE_GLOBAL);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
- mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
+ releaseDownstream(mHotspotIpServer);
+ releaseDownstream(mUsbIpServer);
- final int newFakeSubAddr = 0x3c05;
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
-
- final LinkAddress newHotspotAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
+ final LinkAddress newHotspotAddress =
+ requestStickyDownstreamAddress(mHotspotIpServer, CONNECTIVITY_SCOPE_GLOBAL);
assertEquals(hotspotAddress, newHotspotAddress);
- final LinkAddress newUsbAddress = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
+ final LinkAddress newUsbAddress =
+ requestStickyDownstreamAddress(mUsbIpServer, CONNECTIVITY_SCOPE_GLOBAL);
assertEquals(usbAddress, newUsbAddress);
final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
new LinkAddress("192.168.88.23/16"), null,
makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream);
+ updateUpstreamPrefix(wifiUpstream);
verify(mHotspotIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
verify(mUsbIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
}
@@ -261,269 +259,35 @@
}
@Test
- public void testNoConflictUpstreamPrefix() throws Exception {
- final int fakeHotspotSubAddr = 0x2b05; // 43.5
- final IpPrefix predefinedPrefix = new IpPrefix("192.168.43.0/24");
- // Force always get subAddress "43.5" for conflict testing.
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
- // - Enable hotspot with prefix 192.168.43.0/24
- final LinkAddress hotspotAddr = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddr);
- assertEquals("Wrong wifi prefix: ", predefinedPrefix, hotspotPrefix);
- // - test mobile network with null NetworkCapabilities. Ideally this should not happen
- // because NetworkCapabilities update should always happen before LinkProperties update
- // and the UpstreamNetworkState update, just make sure no crash in this case.
- final UpstreamNetworkState noCapUpstream = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("10.0.0.8/24"), null, null);
- mPrivateAddressCoordinator.updateUpstreamPrefix(noCapUpstream);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - test mobile upstream with no address.
- final UpstreamNetworkState noAddress = buildUpstreamNetworkState(mMobileNetwork,
- null, null, makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(noCapUpstream);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v6 only mobile network, hotspot prefix should not be removed.
- final UpstreamNetworkState v6OnlyMobile = buildUpstreamNetworkState(mMobileNetwork,
- null, new LinkAddress("2001:db8::/64"),
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v6OnlyMobile);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- mPrivateAddressCoordinator.removeUpstreamPrefix(mMobileNetwork);
- // - Update v4 only mobile network, hotspot prefix should not be removed.
- final UpstreamNetworkState v4OnlyMobile = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("10.0.0.8/24"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyMobile);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v4v6 mobile network, hotspot prefix should not be removed.
- final UpstreamNetworkState v4v6Mobile = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("10.0.0.8/24"), new LinkAddress("2001:db8::/64"),
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v4v6Mobile);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v6 only wifi network, hotspot prefix should not be removed.
- final UpstreamNetworkState v6OnlyWifi = buildUpstreamNetworkState(mWifiNetwork,
- null, new LinkAddress("2001:db8::/64"), makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v6OnlyWifi);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork);
- // - Update vpn network, it conflict with hotspot prefix but VPN networks are ignored.
- final UpstreamNetworkState v4OnlyVpn = buildUpstreamNetworkState(mVpnNetwork,
- new LinkAddress("192.168.43.5/24"), null, makeNetworkCapabilities(TRANSPORT_VPN));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyVpn);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Update v4 only wifi network, it conflict with hotspot prefix.
- final UpstreamNetworkState v4OnlyWifi = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.43.5/24"), null, makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyWifi);
- verify(mHotspotIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- reset(mHotspotIpServer);
- // - Restart hotspot again and its prefix is different previous.
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
- final LinkAddress hotspotAddr2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix hotspotPrefix2 = asIpPrefix(hotspotAddr2);
- assertNotEquals(hotspotPrefix, hotspotPrefix2);
- mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyWifi);
- verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- // - Usb tethering can be enabled and its prefix is different with conflict one.
- final LinkAddress usbAddr = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix usbPrefix = asIpPrefix(usbAddr);
- assertNotEquals(predefinedPrefix, usbPrefix);
- assertNotEquals(hotspotPrefix2, usbPrefix);
- // - Disable wifi upstream, then wifi's prefix can be selected again.
- mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork);
- final LinkAddress ethAddr = requestDownstreamAddress(mEthernetIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- final IpPrefix ethPrefix = asIpPrefix(ethAddr);
- assertEquals(predefinedPrefix, ethPrefix);
+ public void testChooseDownstreamAddress_noUpstreamConflicts() throws Exception {
+ LinkAddress address = new LinkAddress("192.168.42.42/24");
+ UpstreamNetworkState ns = buildUpstreamNetworkState(mMobileNetwork, address, null, null);
+ updateUpstreamPrefix(ns);
+ // try to look for a /24 in upstream that does not conflict with upstream -> impossible.
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(asIpPrefix(address)));
+
+ IpPrefix prefix = new IpPrefix("192.168.0.0/16");
+ LinkAddress chosenAddress = mPrivateAddressCoordinator.chooseDownstreamAddress(prefix);
+ assertNotNull(chosenAddress);
+ assertTrue(prefix.containsPrefix(asIpPrefix(chosenAddress)));
}
@Test
- public void testChooseAvailablePrefix() throws Exception {
- final int randomAddress = 0x8605; // 134.5
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
- final LinkAddress addr0 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.134.5.
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.134.5/24"), addr0);
- final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.134.13/26"), null,
- makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream);
-
- // Check whether return address is next prefix of 192.168.134.0/24.
- final LinkAddress addr1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.135.5/24"), addr1);
- final UpstreamNetworkState wifiUpstream2 = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.149.16/19"), null,
- makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream2);
-
-
- // The conflict range is 128 ~ 159, so the address is 192.168.160.5/24.
- final LinkAddress addr2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.160.5/24"), addr2);
- final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("192.168.129.53/18"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- // Update another conflict upstream which is covered by the previous one (but not the first
- // one) and verify whether this would affect the result.
- final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2,
- new LinkAddress("192.168.170.7/19"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream);
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream2);
-
- // The conflict range are 128 ~ 159 and 159 ~ 191, so the address is 192.168.192.5/24.
- final LinkAddress addr3 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.192.5/24"), addr3);
- final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
- new LinkAddress("192.168.188.133/17"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream3);
-
- // Conflict range: 128 ~ 255. The next available address is 192.168.0.5 because
- // 192.168.134/24 ~ 192.168.255.255/24 is not available.
- final LinkAddress addr4 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.0.5/24"), addr4);
- final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
- new LinkAddress("192.168.3.59/21"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream4);
-
- // Conflict ranges: 128 ~ 255 and 0 ~ 7, so the address is 192.168.8.5/24.
- final LinkAddress addr5 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr5);
- final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
- new LinkAddress("192.168.68.43/21"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream5);
-
- // Update an upstream that does *not* conflict, check whether return the same address
- // 192.168.5/24.
- final LinkAddress addr6 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr6);
- final UpstreamNetworkState mobileUpstream6 = buildUpstreamNetworkState(mMobileNetwork6,
- new LinkAddress("192.168.10.97/21"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream6);
-
- // Conflict ranges: 0 ~ 15 and 128 ~ 255, so the address is 192.168.16.5/24.
- final LinkAddress addr7 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.16.5/24"), addr7);
- final UpstreamNetworkState mobileUpstream7 = buildUpstreamNetworkState(mMobileNetwork6,
- new LinkAddress("192.168.0.0/17"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream7);
-
- // Choose prefix from next range(172.16.0.0/12) when no available prefix in 192.168.0.0/16.
- final LinkAddress addr8 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.134.5/24"), addr8);
- }
-
- @Test
- public void testChoosePrefixFromDifferentRanges() throws Exception {
- final int randomAddress = 0x1f2b2a; // 31.43.42
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
- final LinkAddress classC1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.43.42.
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.43.42/24"), classC1);
- final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
- new LinkAddress("192.168.88.23/17"), null,
- makeNetworkCapabilities(TRANSPORT_WIFI));
- mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
-
- // Check whether return address is next address of prefix 192.168.128.0/17.
- final LinkAddress classC2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("192.168.128.42/24"), classC2);
- final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
- new LinkAddress("192.1.2.3/8"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
-
- // Check whether return address is under prefix 172.16.0.0/12.
- final LinkAddress classB1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.31.43.42/24"), classB1);
- final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2,
- new LinkAddress("172.28.123.100/14"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream2);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
-
- // 172.28.0.0 ~ 172.31.255.255 is not available.
- // Check whether return address is next address of prefix 172.16.0.0/14.
- final LinkAddress classB2 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.0.42/24"), classB2);
-
- // Check whether new downstream is next address of address 172.16.0.42/24.
- final LinkAddress classB3 = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.1.42/24"), classB3);
- final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
- new LinkAddress("172.16.0.1/24"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream3);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
- verify(mUsbIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-
- // Check whether return address is next address of prefix 172.16.1.42/24.
- final LinkAddress classB4 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.16.2.42/24"), classB4);
- final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
- new LinkAddress("172.16.0.1/13"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream4);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
- verifyNotifyConflictAndRelease(mUsbIpServer);
-
- // Check whether return address is next address of prefix 172.16.0.1/13.
- final LinkAddress classB5 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.24.0.42/24"), classB5);
- // Check whether return address is next address of prefix 172.24.0.42/24.
- final LinkAddress classB6 = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("172.24.1.42/24"), classB6);
- final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
- new LinkAddress("172.24.0.1/12"), null,
- makeNetworkCapabilities(TRANSPORT_CELLULAR));
- mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream5);
- verifyNotifyConflictAndRelease(mHotspotIpServer);
- verifyNotifyConflictAndRelease(mUsbIpServer);
-
- // Check whether return address is prefix 10.0.0.0/8 + subAddress 0.31.43.42.
- final LinkAddress classA1 = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("10.31.43.42/24"), classA1);
- // Check whether new downstream is next address of address 10.31.43.42/24.
- final LinkAddress classA2 = requestDownstreamAddress(mUsbIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong prefix: ", new LinkAddress("10.31.44.42/24"), classA2);
+ public void testChooseDownstreamAddress_excludesWellKnownPrefixes() throws Exception {
+ IpPrefix prefix = new IpPrefix("192.168.0.0/24");
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
+ prefix = new IpPrefix("192.168.100.0/24");
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
+ prefix = new IpPrefix("10.3.0.0/16");
+ assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
}
private void verifyNotifyConflictAndRelease(final IpServer ipServer) throws Exception {
- verify(ipServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
- mPrivateAddressCoordinator.releaseDownstream(ipServer);
+ verify(ipServer).sendMessage(CMD_NOTIFY_PREFIX_CONFLICT);
+ releaseDownstream(ipServer);
+ final int interfaceType = ipServer.interfaceType();
reset(ipServer);
- setUpIpServers();
+ setUpIpServer(ipServer, interfaceType);
}
private int getSubAddress(final byte... ipv4Address) {
@@ -534,50 +298,33 @@
}
private void assertReseveredWifiP2pPrefix() throws Exception {
- LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
+ LinkAddress address =
+ requestStickyDownstreamAddress(mHotspotIpServer, CONNECTIVITY_SCOPE_GLOBAL);
final IpPrefix hotspotPrefix = asIpPrefix(address);
final IpPrefix legacyWifiP2pPrefix = asIpPrefix(mLegacyWifiP2pAddress);
assertNotEquals(legacyWifiP2pPrefix, hotspotPrefix);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
- }
-
- @Test
- public void testEnableLegacyWifiP2PAddress() throws Exception {
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
- getSubAddress(mLegacyWifiP2pAddress.getAddress().getAddress()));
- // No matter #shouldEnableWifiP2pDedicatedIp() is enabled or not, legacy wifi p2p prefix
- // is resevered.
- assertReseveredWifiP2pPrefix();
-
- when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(true);
- assertReseveredWifiP2pPrefix();
-
- // If #shouldEnableWifiP2pDedicatedIp() is enabled, wifi P2P gets the configured address.
- LinkAddress address = requestDownstreamAddress(mWifiP2pIpServer,
- CONNECTIVITY_SCOPE_LOCAL, true /* useLastAddress */);
- assertEquals(mLegacyWifiP2pAddress, address);
- mPrivateAddressCoordinator.releaseDownstream(mWifiP2pIpServer);
+ releaseDownstream(mHotspotIpServer);
}
@Test
public void testEnableSapAndLohsConcurrently() throws Exception {
- // 0x2b05 -> 43.5, 0x8605 -> 134.5
- when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(0x2b05, 0x8605);
+ final LinkAddress hotspotAddress =
+ requestStickyDownstreamAddress(mHotspotIpServer, CONNECTIVITY_SCOPE_GLOBAL);
+ assertNotNull(hotspotAddress);
- final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
- assertEquals("Wrong hotspot prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
+ final LinkAddress localHotspotAddress =
+ requestStickyDownstreamAddress(mLocalHotspotIpServer, CONNECTIVITY_SCOPE_LOCAL);
+ assertNotNull(localHotspotAddress);
- final LinkAddress localHotspotAddress = requestDownstreamAddress(mLocalHotspotIpServer,
- CONNECTIVITY_SCOPE_LOCAL, true /* useLastAddress */);
- assertEquals("Wrong local hotspot prefix: ", new LinkAddress("192.168.134.5/24"),
- localHotspotAddress);
+ final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
+ final IpPrefix localHotspotPrefix = asIpPrefix(localHotspotAddress);
+ assertFalse(hotspotPrefix.containsPrefix(localHotspotPrefix));
+ assertFalse(localHotspotPrefix.containsPrefix(hotspotPrefix));
}
@Test
public void testStartedPrefixRange() throws Exception {
- when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(true);
+ when(mDeps.isFeatureEnabled(TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION)).thenReturn(true);
startedPrefixBaseTest("192.168.0.0/16", 0);
@@ -602,10 +349,10 @@
private void startedPrefixBaseTest(final String expected, final int randomIntForPrefixBase)
throws Exception {
- mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
+ mPrivateAddressCoordinator =
+ spy(new PrivateAddressCoordinator(mConnectivityMgr::getAllNetworks, mDeps));
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomIntForPrefixBase);
- final LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
- CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
+ final LinkAddress address = requestDownstreamAddress(mHotspotIpServer);
final IpPrefix prefixBase = new IpPrefix(expected);
assertTrue(address + " is not part of " + prefixBase,
prefixBase.containsPrefix(asIpPrefix(address)));
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index c0d7ad4..0dbf772 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -33,12 +33,16 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import android.app.AppOpsManager;
import android.app.UiAutomation;
import android.content.Intent;
import android.net.IIntResultListener;
@@ -79,7 +83,9 @@
public final class TetheringServiceTest {
private static final String TEST_IFACE_NAME = "test_wlan0";
private static final String TEST_CALLER_PKG = "com.android.shell";
+ private static final int TEST_CALLER_UID = 1234;
private static final String TEST_ATTRIBUTION_TAG = null;
+ private static final String TEST_WRONG_PACKAGE = "wrong.package";
@Mock private ITetheringEventCallback mITetheringEventCallback;
@Rule public ServiceTestRule mServiceTestRule;
private Tethering mTethering;
@@ -87,6 +93,7 @@
private MockTetheringConnector mMockConnector;
private ITetheringConnector mTetheringConnector;
private UiAutomation mUiAutomation;
+ @Mock private AppOpsManager mAppOps;
private class TestTetheringResult extends IIntResultListener.Stub {
private int mResult = -1; // Default value that does not match any result code.
@@ -128,6 +135,10 @@
mTetheringConnector = ITetheringConnector.Stub.asInterface(mMockConnector.getIBinder());
final MockTetheringService service = mMockConnector.getService();
mTethering = service.getTethering();
+ mMockConnector.setCallingUid(TEST_CALLER_UID);
+ mMockConnector.setPackageNameUid(TEST_CALLER_PKG, TEST_CALLER_UID);
+ doThrow(new SecurityException()).when(mAppOps).checkPackage(anyInt(),
+ eq(TEST_WRONG_PACKAGE));
}
@After
@@ -330,6 +341,15 @@
});
runAsTetherPrivileged((result) -> {
+ mTetheringConnector.startTethering(request, TEST_WRONG_PACKAGE,
+ TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering, never()).startTethering(
+ eq(new TetheringRequest(request)), eq(TEST_WRONG_PACKAGE), eq(result));
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsTetherPrivileged((result) -> {
runStartTethering(result, request);
verifyNoMoreInteractionsForTethering();
});
@@ -445,6 +465,13 @@
verifyNoMoreInteractionsForTethering();
});
+ runAsTetherPrivileged((none) -> {
+ mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
+ true /* showEntitlementUi */, TEST_WRONG_PACKAGE, TEST_ATTRIBUTION_TAG);
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractions(mTethering);
+ });
+
runAsWriteSettings((none) -> {
runRequestLatestTetheringEntitlementResult();
verify(mTethering).isTetherProvisioningRequired();
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 6ba5d48..17f5081 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -17,7 +17,10 @@
package com.android.networkstack.tethering;
import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_STACK;
import static android.content.pm.PackageManager.GET_ACTIVITIES;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.hardware.usb.UsbManager.USB_CONFIGURED;
import static android.hardware.usb.UsbManager.USB_CONNECTED;
import static android.hardware.usb.UsbManager.USB_FUNCTION_NCM;
@@ -33,6 +36,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
import static android.net.RouteInfo.RTN_UNICAST;
import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
@@ -96,6 +100,7 @@
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
@@ -124,6 +129,7 @@
import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.usb.UsbManager;
+import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.EthernetManager;
import android.net.EthernetManager.TetheredInterfaceCallback;
@@ -162,6 +168,7 @@
import android.net.wifi.WifiClient;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.SoftApCallback;
+import android.net.wifi.WifiSsid;
import android.net.wifi.p2p.WifiP2pGroup;
import android.net.wifi.p2p.WifiP2pInfo;
import android.net.wifi.p2p.WifiP2pManager;
@@ -188,9 +195,12 @@
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.PrivateAddressCoordinator;
import com.android.net.module.util.RoutingCoordinatorManager;
+import com.android.net.module.util.RoutingCoordinatorService;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.ip.IpNeighborMonitor;
import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -219,6 +229,7 @@
import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -253,6 +264,7 @@
private static final String TEST_WIFI_REGEX = "test_wlan\\d";
private static final String TEST_P2P_REGEX = "test_p2p-p2p\\d-.*";
private static final String TEST_BT_REGEX = "test_pan\\d";
+ private static final int TEST_CALLER_UID = 1000;
private static final String TEST_CALLER_PKG = "com.test.tethering";
private static final int CELLULAR_NETID = 100;
@@ -291,7 +303,7 @@
@Mock private BluetoothPanShim mBluetoothPanShim;
@Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
@Mock private TetheringMetrics mTetheringMetrics;
- @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
+ @Mock private PrivateAddressCoordinator.Dependencies mPrivateAddressCoordinatorDependencies;
private final MockIpServerDependencies mIpServerDependencies =
spy(new MockIpServerDependencies());
@@ -315,15 +327,16 @@
private TetheringConfiguration mConfig;
private EntitlementManager mEntitleMgr;
private OffloadController mOffloadCtrl;
- private PrivateAddressCoordinator mPrivateAddressCoordinator;
private SoftApCallback mSoftApCallback;
private SoftApCallback mLocalOnlyHotspotCallback;
private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
private UpstreamNetworkMonitor.EventListener mEventListener;
private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
+ private RoutingCoordinatorManager mRoutingCoordinatorManager;
private TestConnectivityManager mCm;
private boolean mForceEthernetServiceUnavailable = false;
+ private int mBinderCallingUid = TEST_CALLER_UID;
private class TestContext extends BroadcastInterceptingContext {
TestContext(Context base) {
@@ -374,6 +387,7 @@
@Override
public String getSystemServiceName(Class<?> serviceClass) {
if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE;
+ if (ConnectivityManager.class.equals(serviceClass)) return Context.CONNECTIVITY_SERVICE;
return super.getSystemServiceName(serviceClass);
}
}
@@ -485,8 +499,16 @@
}
@Override
- public RoutingCoordinatorManager getRoutingCoordinator(final Context context,
- SharedLog log) {
+ public RoutingCoordinatorManager getRoutingCoordinator(
+ final Context context, SharedLog log) {
+ ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
+ when(mPrivateAddressCoordinatorDependencies.isFeatureEnabled(anyString()))
+ .thenReturn(false);
+ RoutingCoordinatorService service = new RoutingCoordinatorService(
+ getINetd(context, log),
+ cm::getAllNetworks,
+ mPrivateAddressCoordinatorDependencies);
+ mRoutingCoordinatorManager = spy(new RoutingCoordinatorManager(context, service));
return mRoutingCoordinatorManager;
}
@@ -533,13 +555,6 @@
}
@Override
- public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx,
- TetheringConfiguration cfg) {
- mPrivateAddressCoordinator = super.makePrivateAddressCoordinator(ctx, cfg);
- return mPrivateAddressCoordinator;
- }
-
- @Override
public BluetoothPanShim makeBluetoothPanShim(BluetoothPan pan) {
try {
when(mBluetoothPanShim.requestTetheredInterface(
@@ -549,6 +564,11 @@
}
return mBluetoothPanShim;
}
+
+ @Override
+ public int getBinderCallingUid() {
+ return mBinderCallingUid;
+ }
}
private static LinkProperties buildUpstreamLinkProperties(String interfaceName,
@@ -679,6 +699,7 @@
new IntentFilter(ACTION_TETHER_STATE_CHANGED));
mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class)));
+ when(mCm.getAllNetworks()).thenReturn(new Network[] {});
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
@@ -862,6 +883,9 @@
assertTrue(TestConnectivityManager.looksLikeDefaultRequest(reqCaptor.getValue()));
}
+ // Ignore calls to {@link ConnectivityManager#getallNetworks}.
+ verify(mCm, atLeast(0)).getAllNetworks();
+
// The default network request is only ever filed once.
verifyNoMoreInteractions(mCm);
}
@@ -2325,7 +2349,7 @@
// 2. Enable wifi tethering.
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
initTetheringUpstream(upstreamState);
- when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
+ when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
mLooper.dispatchAll();
tetherState = callback.pollTetherStatesChanged();
@@ -2370,6 +2394,105 @@
}
@Test
+ public void testSoftApConfigInTetheringEventCallback() throws Exception {
+ assumeTrue(SdkLevel.isAtLeastV());
+ when(mContext.checkCallingOrSelfPermission(NETWORK_SETTINGS))
+ .thenReturn(PERMISSION_DENIED);
+ when(mContext.checkCallingOrSelfPermission(NETWORK_STACK))
+ .thenReturn(PERMISSION_DENIED);
+ when(mContext.checkCallingOrSelfPermission(PERMISSION_MAINLINE_NETWORK_STACK))
+ .thenReturn(PERMISSION_DENIED);
+ initTetheringOnTestThread();
+ TestTetheringEventCallback callback = new TestTetheringEventCallback();
+ TestTetheringEventCallback differentCallback = new TestTetheringEventCallback();
+ TestTetheringEventCallback settingsCallback = new TestTetheringEventCallback();
+ SoftApConfiguration softApConfig = new SoftApConfiguration.Builder().setWifiSsid(
+ WifiSsid.fromBytes("SoftApConfig".getBytes(StandardCharsets.UTF_8))).build();
+ final TetheringRequest tetheringRequest = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfig).build();
+ tetheringRequest.setUid(TEST_CALLER_UID);
+ final TetheringInterface wifiIfaceWithoutConfig = new TetheringInterface(
+ TETHERING_WIFI, TEST_WLAN_IFNAME, null);
+ final TetheringInterface wifiIfaceWithConfig = new TetheringInterface(
+ TETHERING_WIFI, TEST_WLAN_IFNAME, softApConfig);
+
+ // Register callback before running any tethering.
+ mTethering.registerTetheringEventCallback(callback);
+ mLooper.dispatchAll();
+ callback.expectTetheredClientChanged(Collections.emptyList());
+ callback.expectUpstreamChanged(NULL_NETWORK);
+ callback.expectConfigurationChanged(
+ mTethering.getTetheringConfiguration().toStableParcelable());
+ // Register callback with different UID
+ mBinderCallingUid = TEST_CALLER_UID + 1;
+ mTethering.registerTetheringEventCallback(differentCallback);
+ mLooper.dispatchAll();
+ differentCallback.expectTetheredClientChanged(Collections.emptyList());
+ differentCallback.expectUpstreamChanged(NULL_NETWORK);
+ differentCallback.expectConfigurationChanged(
+ mTethering.getTetheringConfiguration().toStableParcelable());
+ // Register Settings callback
+ when(mContext.checkCallingOrSelfPermission(NETWORK_SETTINGS))
+ .thenReturn(PERMISSION_GRANTED);
+ mTethering.registerTetheringEventCallback(settingsCallback);
+ mLooper.dispatchAll();
+ settingsCallback.expectTetheredClientChanged(Collections.emptyList());
+ settingsCallback.expectUpstreamChanged(NULL_NETWORK);
+ settingsCallback.expectConfigurationChanged(
+ mTethering.getTetheringConfiguration().toStableParcelable());
+
+ assertTetherStatesNotNullButEmpty(callback.pollTetherStatesChanged());
+ assertTetherStatesNotNullButEmpty(differentCallback.pollTetherStatesChanged());
+ assertTetherStatesNotNullButEmpty(settingsCallback.pollTetherStatesChanged());
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+ UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
+ initTetheringUpstream(upstreamState);
+ when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ mLooper.dispatchAll();
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+ callback.pollTetherStatesChanged().availableList);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+ differentCallback.pollTetherStatesChanged().availableList);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+ settingsCallback.pollTetherStatesChanged().availableList);
+
+ // Enable wifi tethering
+ mBinderCallingUid = TEST_CALLER_UID;
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
+ callback.pollTetherStatesChanged().tetheredList);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+ differentCallback.pollTetherStatesChanged().tetheredList);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
+ settingsCallback.pollTetherStatesChanged().tetheredList);
+ callback.expectUpstreamChanged(upstreamState.network);
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED);
+
+ // Disable wifi tethering
+ mLooper.dispatchAll();
+ mTethering.stopTethering(TETHERING_WIFI);
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED);
+ if (isAtLeastT()) {
+ // After T, tethering doesn't support WIFI_AP_STATE_DISABLED with null interface name.
+ callback.assertNoStateChangeCallback();
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME,
+ IFACE_IP_MODE_TETHERED);
+ }
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
+ callback.pollTetherStatesChanged().availableList);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+ differentCallback.pollTetherStatesChanged().availableList);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
+ settingsCallback.pollTetherStatesChanged().availableList);
+ mLooper.dispatchAll();
+ callback.expectUpstreamChanged(NULL_NETWORK);
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+ callback.assertNoCallback();
+ }
+
+ @Test
public void testReportFailCallbackIfOffloadNotSupported() throws Exception {
initTetheringOnTestThread();
final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index 7cef9cb..6b646ec 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -16,12 +16,23 @@
package com.android.networkstack.tethering.metrics;
+import static android.app.usage.NetworkStats.Bucket.STATE_ALL;
+import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
+import static android.net.NetworkTemplate.MATCH_ETHERNET;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_WIFI;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringManager.TETHERING_NCM;
@@ -45,46 +56,79 @@
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+import static android.stats.connectivity.UpstreamType.UT_BLUETOOTH;
+import static android.stats.connectivity.UpstreamType.UT_CELLULAR;
+import static android.stats.connectivity.UpstreamType.UT_ETHERNET;
+import static android.stats.connectivity.UpstreamType.UT_WIFI;
+
+import static com.android.networkstack.tethering.metrics.TetheringMetrics.EMPTY;
+import static com.android.testutils.NetworkStatsUtilsKt.makePublicStatsFromAndroidNetStats;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
+import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.net.NetworkCapabilities;
+import android.net.NetworkStats;
+import android.net.NetworkTemplate;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.stats.connectivity.DownstreamType;
import android.stats.connectivity.ErrorCode;
import android.stats.connectivity.UpstreamType;
import android.stats.connectivity.UserType;
+import android.util.ArrayMap;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.networkstack.tethering.UpstreamNetworkState;
+import com.android.networkstack.tethering.metrics.TetheringMetrics.DataUsage;
import com.android.networkstack.tethering.metrics.TetheringMetrics.Dependencies;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
+import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-@RunWith(AndroidJUnit4.class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
public final class TetheringMetricsTest {
+ @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
private static final String TEST_CALLER_PKG = "com.test.caller.pkg";
private static final String SETTINGS_PKG = "com.android.settings";
private static final String SYSTEMUI_PKG = "com.android.systemui";
private static final String GMS_PKG = "com.google.android.gms";
private static final long TEST_START_TIME = 1670395936033L;
private static final long SECOND_IN_MILLIS = 1_000L;
+ private static final long DEFAULT_TIMEOUT = 2000L;
+ private static final int MATCH_NONE = -1;
@Mock private Context mContext;
@Mock private Dependencies mDeps;
+ @Mock private NetworkStatsManager mNetworkStatsManager;
private TetheringMetrics mTetheringMetrics;
private final NetworkTetheringReported.Builder mStatsBuilder =
NetworkTetheringReported.newBuilder();
+ private final ArrayMap<UpstreamType, DataUsage> mMockUpstreamUsageBaseline = new ArrayMap<>();
+ private HandlerThread mThread;
+ private Handler mHandler;
private long mElapsedRealtime;
@@ -111,10 +155,35 @@
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
doReturn(TEST_START_TIME).when(mDeps).timeNow();
+ doReturn(mNetworkStatsManager).when(mContext).getSystemService(NetworkStatsManager.class);
+ mThread = new HandlerThread("TetheringMetricsTest");
+ mThread.start();
+ mHandler = new Handler(mThread.getLooper());
+ doReturn(mHandler).when(mDeps).createHandler();
+ // Set up the usage for upstream types.
+ mMockUpstreamUsageBaseline.put(UT_CELLULAR, new DataUsage(100L, 200L));
+ mMockUpstreamUsageBaseline.put(UT_WIFI, new DataUsage(400L, 800L));
+ mMockUpstreamUsageBaseline.put(UT_BLUETOOTH, new DataUsage(50L, 80L));
+ mMockUpstreamUsageBaseline.put(UT_ETHERNET, new DataUsage(0L, 0L));
+ doAnswer(inv -> {
+ final NetworkTemplate template = (NetworkTemplate) inv.getArguments()[0];
+ final DataUsage dataUsage = mMockUpstreamUsageBaseline.getOrDefault(
+ matchRuleToUpstreamType(template.getMatchRule()), new DataUsage(0L, 0L));
+ return makeNetworkStatsWithTxRxBytes(dataUsage);
+ }).when(mNetworkStatsManager).queryDetailsForUidTagState(any(), eq(Long.MIN_VALUE),
+ eq(Long.MAX_VALUE), eq(UID_TETHERING), eq(TAG_NONE), eq(STATE_ALL));
mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
mElapsedRealtime = 0L;
}
+ @After
+ public void tearDown() throws Exception {
+ if (mThread != null) {
+ mThread.quitSafely();
+ mThread.join();
+ }
+ }
+
private void verifyReport(final DownstreamType downstream, final ErrorCode error,
final UserType user, final UpstreamEvents.Builder upstreamEvents, final long duration)
throws Exception {
@@ -129,9 +198,15 @@
verify(mDeps).write(expectedReport);
}
+ private void runAndWaitForIdle(Runnable r) {
+ r.run();
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ }
+
private void updateErrorAndSendReport(final int downstream, final int error) {
mTetheringMetrics.updateErrorCode(downstream, error);
mTetheringMetrics.sendReport(downstream);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
}
private static NetworkCapabilities buildUpstreamCapabilities(final int[] transports) {
@@ -163,7 +238,7 @@
private void runDownstreamTypesTest(final int type, final DownstreamType expectedResult)
throws Exception {
mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
- mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG));
final long duration = 2 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
@@ -189,14 +264,15 @@
private void runErrorCodesTest(final int errorCode, final ErrorCode expectedResult)
throws Exception {
mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
- mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
final long duration = 2 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
updateErrorAndSendReport(TETHERING_WIFI, errorCode);
UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, duration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_WIFI, duration, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN,
upstreamEvents, getElapsedRealtime());
clearElapsedRealtime();
@@ -230,7 +306,7 @@
private void runUserTypesTest(final String callerPkg, final UserType expectedResult)
throws Exception {
mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
- mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg));
final long duration = 1 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
@@ -254,8 +330,8 @@
private void runUpstreamTypesTest(final UpstreamNetworkState ns,
final UpstreamType expectedResult) throws Exception {
mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
- mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
- mTetheringMetrics.maybeUpdateUpstreamType(ns);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG));
+ runAndWaitForIdle(() -> mTetheringMetrics.maybeUpdateUpstreamType(ns));
final long duration = 2 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
@@ -270,10 +346,10 @@
@Test
public void testUpstreamTypes() throws Exception {
runUpstreamTypesTest(null , UpstreamType.UT_NO_NETWORK);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR), UpstreamType.UT_CELLULAR);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI), UpstreamType.UT_WIFI);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_BLUETOOTH), UpstreamType.UT_BLUETOOTH);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_ETHERNET), UpstreamType.UT_ETHERNET);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR), UT_CELLULAR);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI), UT_WIFI);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_BLUETOOTH), UT_BLUETOOTH);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_ETHERNET), UT_ETHERNET);
runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI_AWARE), UpstreamType.UT_WIFI_AWARE);
runUpstreamTypesTest(buildUpstreamState(TRANSPORT_LOWPAN), UpstreamType.UT_LOWPAN);
runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR, TRANSPORT_WIFI,
@@ -282,13 +358,13 @@
@Test
public void testMultiBuildersCreatedBeforeSendReport() throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
final long wifiTetheringStartTime = currentTimeMillis();
incrementCurrentTime(1 * SECOND_IN_MILLIS);
- mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG));
final long usbTetheringStartTime = currentTimeMillis();
incrementCurrentTime(2 * SECOND_IN_MILLIS);
- mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG));
final long bluetoothTetheringStartTime = currentTimeMillis();
incrementCurrentTime(3 * SECOND_IN_MILLIS);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_DHCPSERVER_ERROR);
@@ -322,19 +398,20 @@
@Test
public void testUpstreamsWithMultipleDownstreams() throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
final long wifiTetheringStartTime = currentTimeMillis();
incrementCurrentTime(1 * SECOND_IN_MILLIS);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
final long wifiUpstreamStartTime = currentTimeMillis();
incrementCurrentTime(5 * SECOND_IN_MILLIS);
- mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG));
final long usbTetheringStartTime = currentTimeMillis();
incrementCurrentTime(5 * SECOND_IN_MILLIS);
updateErrorAndSendReport(TETHERING_USB, TETHER_ERROR_NO_ERROR);
UpstreamEvents.Builder usbTetheringUpstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(usbTetheringUpstreamEvents, UpstreamType.UT_WIFI,
+ addUpstreamEvent(usbTetheringUpstreamEvents, UT_WIFI,
currentTimeMillis() - usbTetheringStartTime, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_USB, ErrorCode.EC_NO_ERROR,
UserType.USER_SYSTEMUI, usbTetheringUpstreamEvents,
@@ -343,7 +420,7 @@
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
UpstreamEvents.Builder wifiTetheringUpstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(wifiTetheringUpstreamEvents, UpstreamType.UT_WIFI,
+ addUpstreamEvent(wifiTetheringUpstreamEvents, UT_WIFI,
currentTimeMillis() - wifiUpstreamStartTime, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
UserType.USER_SETTINGS, wifiTetheringUpstreamEvents,
@@ -352,24 +429,27 @@
@Test
public void testSwitchingMultiUpstreams() throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
final long wifiTetheringStartTime = currentTimeMillis();
incrementCurrentTime(1 * SECOND_IN_MILLIS);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
final long wifiDuration = 5 * SECOND_IN_MILLIS;
incrementCurrentTime(wifiDuration);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH)));
final long bluetoothDuration = 15 * SECOND_IN_MILLIS;
incrementCurrentTime(bluetoothDuration);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR)));
final long celltoothDuration = 20 * SECOND_IN_MILLIS;
incrementCurrentTime(celltoothDuration);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, wifiDuration, 0L, 0L);
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_BLUETOOTH, bluetoothDuration, 0L, 0L);
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_CELLULAR, celltoothDuration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_BLUETOOTH, bluetoothDuration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_CELLULAR, celltoothDuration, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
UserType.USER_SETTINGS, upstreamEvents,
@@ -384,12 +464,171 @@
@Test
public void testUsageSupportedForUpstreamTypeTest() {
- runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_CELLULAR, true /* isSupported */);
- runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI, true /* isSupported */);
- runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_BLUETOOTH, true /* isSupported */);
- runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_ETHERNET, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_CELLULAR, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_WIFI, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_BLUETOOTH, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_ETHERNET, true /* isSupported */);
runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI_AWARE, false /* isSupported */);
runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_LOWPAN, false /* isSupported */);
runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_UNKNOWN, false /* isSupported */);
}
+
+ private void runBuildNetworkTemplateForUpstreamType(final UpstreamType upstreamType,
+ final int matchRule) {
+ final NetworkTemplate template =
+ TetheringMetrics.buildNetworkTemplateForUpstreamType(upstreamType);
+ if (matchRule == MATCH_NONE) {
+ assertNull(template);
+ } else {
+ assertEquals(matchRule, template.getMatchRule());
+ }
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testBuildNetworkTemplateForUpstreamType() {
+ runBuildNetworkTemplateForUpstreamType(UT_CELLULAR, MATCH_MOBILE);
+ runBuildNetworkTemplateForUpstreamType(UT_WIFI, MATCH_WIFI);
+ runBuildNetworkTemplateForUpstreamType(UT_BLUETOOTH, MATCH_BLUETOOTH);
+ runBuildNetworkTemplateForUpstreamType(UT_ETHERNET, MATCH_ETHERNET);
+ runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI_AWARE, MATCH_NONE);
+ runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_LOWPAN, MATCH_NONE);
+ runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_UNKNOWN, MATCH_NONE);
+ }
+
+ private void verifyEmptyUsageForAllUpstreamTypes() {
+ mHandler.post(() -> {
+ for (UpstreamType type : UpstreamType.values()) {
+ assertEquals(EMPTY, mTetheringMetrics.getLastReportedUsageFromUpstreamType(type));
+ }
+ });
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ }
+
+ @Test
+ public void testInitializeUpstreamDataUsageBeforeT() {
+ // Verify the usage is empty for all upstream types before initialization.
+ verifyEmptyUsageForAllUpstreamTypes();
+
+ // Verify the usage is still empty after initialization if sdk is lower than T.
+ doReturn(false).when(mDeps).isUpstreamDataUsageMetricsEnabled(any());
+ runAndWaitForIdle(() -> mTetheringMetrics.initUpstreamUsageBaseline());
+ verifyEmptyUsageForAllUpstreamTypes();
+ }
+
+ private android.app.usage.NetworkStats makeNetworkStatsWithTxRxBytes(DataUsage dataUsage) {
+ final NetworkStats testAndroidNetStats =
+ new NetworkStats(0L /* elapsedRealtime */, 1 /* initialSize */).addEntry(
+ new NetworkStats.Entry("test", 10001, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, dataUsage.rxBytes,
+ 10, dataUsage.txBytes, 10, 10));
+ return makePublicStatsFromAndroidNetStats(testAndroidNetStats);
+ }
+
+ private static UpstreamType matchRuleToUpstreamType(int matchRule) {
+ switch (matchRule) {
+ case MATCH_MOBILE:
+ return UT_CELLULAR;
+ case MATCH_WIFI:
+ return UT_WIFI;
+ case MATCH_BLUETOOTH:
+ return UT_BLUETOOTH;
+ case MATCH_ETHERNET:
+ return UT_ETHERNET;
+ default:
+ return UpstreamType.UT_UNKNOWN;
+ }
+ }
+
+ private void initializeUpstreamUsageBaseline() {
+ doReturn(true).when(mDeps).isUpstreamDataUsageMetricsEnabled(any());
+ runAndWaitForIdle(() -> mTetheringMetrics.initUpstreamUsageBaseline());
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testInitUpstreamUsageBaselineAndCleanup() {
+ // Verify the usage is empty for all upstream types before initialization.
+ verifyEmptyUsageForAllUpstreamTypes();
+
+ // Verify the usage has been initialized
+ initializeUpstreamUsageBaseline();
+
+ mHandler.post(() -> {
+ for (UpstreamType type : UpstreamType.values()) {
+ final DataUsage dataUsage =
+ mTetheringMetrics.getLastReportedUsageFromUpstreamType(type);
+ if (TetheringMetrics.isUsageSupportedForUpstreamType(type)) {
+ assertEquals(mMockUpstreamUsageBaseline.get(type), dataUsage);
+ } else {
+ assertEquals(EMPTY, dataUsage);
+ }
+ }
+ });
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+
+ // Verify the usage is empty after clean up
+ runAndWaitForIdle(() -> mTetheringMetrics.cleanup());
+ verifyEmptyUsageForAllUpstreamTypes();
+ }
+
+ private void updateUpstreamDataUsage(UpstreamType type, long usageDiff) {
+ final DataUsage oldWifiUsage = mMockUpstreamUsageBaseline.get(type);
+ final DataUsage newWifiUsage = new DataUsage(
+ oldWifiUsage.txBytes + usageDiff,
+ oldWifiUsage.rxBytes + usageDiff);
+ mMockUpstreamUsageBaseline.put(type, newWifiUsage);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testDataUsageCalculation() throws Exception {
+ initializeUpstreamUsageBaseline();
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
+ final long wifiTetheringStartTime = currentTimeMillis();
+ incrementCurrentTime(1 * SECOND_IN_MILLIS);
+
+ // Change the upstream to Wi-Fi and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
+ final long wifiDuration = 5 * SECOND_IN_MILLIS;
+ final long wifiUsageDiff = 100L;
+ incrementCurrentTime(wifiDuration);
+ updateUpstreamDataUsage(UT_WIFI, wifiUsageDiff);
+
+ // Change the upstream to bluetooth and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH)));
+ final long bluetoothDuration = 15 * SECOND_IN_MILLIS;
+ final long btUsageDiff = 50L;
+ incrementCurrentTime(bluetoothDuration);
+ updateUpstreamDataUsage(UT_BLUETOOTH, btUsageDiff);
+
+ // Change the upstream to cellular and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR)));
+ final long cellDuration = 20 * SECOND_IN_MILLIS;
+ final long cellUsageDiff = 500L;
+ incrementCurrentTime(cellDuration);
+ updateUpstreamDataUsage(UT_CELLULAR, cellUsageDiff);
+
+ // Change the upstream back to Wi-FI and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
+ final long wifiDuration2 = 50 * SECOND_IN_MILLIS;
+ final long wifiUsageDiff2 = 1000L;
+ incrementCurrentTime(wifiDuration2);
+ updateUpstreamDataUsage(UT_WIFI, wifiUsageDiff2);
+
+ // Stop tethering and verify that the data usage is uploaded.
+ updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+ UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
+ addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration, wifiUsageDiff, wifiUsageDiff);
+ addUpstreamEvent(upstreamEvents, UT_BLUETOOTH, bluetoothDuration, btUsageDiff, btUsageDiff);
+ addUpstreamEvent(upstreamEvents, UT_CELLULAR, cellDuration, cellUsageDiff, cellUsageDiff);
+ addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration2, wifiUsageDiff2, wifiUsageDiff2);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
+ UserType.USER_SETTINGS, upstreamEvents,
+ currentTimeMillis() - wifiTetheringStartTime);
+ }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
index b7dc66e..ed4f3da 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
@@ -16,6 +16,8 @@
package com.android.networkstack.tethering.util;
+import static com.android.testutils.HandlerUtils.waitForIdle;
+
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.reset;
@@ -23,7 +25,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
-import android.os.Looper;
+import android.os.HandlerThread;
import android.os.UserHandle;
import androidx.test.filters.SmallTest;
@@ -33,7 +35,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -44,9 +45,11 @@
public class VersionedBroadcastListenerTest {
private static final String TAG = VersionedBroadcastListenerTest.class.getSimpleName();
private static final String ACTION_TEST = "action.test.happy.broadcasts";
+ private static final long TEST_TIMEOUT_MS = 10_000L;
@Mock private Context mContext;
private BroadcastInterceptingContext mServiceContext;
+ private HandlerThread mHandlerThread;
private Handler mHandler;
private VersionedBroadcastListener mListener;
private int mCallbackCount;
@@ -61,18 +64,13 @@
}
}
- @BeforeClass
- public static void setUpBeforeClass() throws Exception {
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
- }
-
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
reset(mContext);
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
mServiceContext = new MockContext(mContext);
- mHandler = new Handler(Looper.myLooper());
mCallbackCount = 0;
final IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_TEST);
@@ -85,11 +83,15 @@
mListener.stopListening();
mListener = null;
}
+ mHandlerThread.quitSafely();
+ mHandlerThread.join(TEST_TIMEOUT_MS);
}
private void sendBroadcast() {
final Intent intent = new Intent(ACTION_TEST);
mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ // Sending the broadcast is synchronous, but the receiver just posts on the handler
+ waitForIdle(mHandler, TEST_TIMEOUT_MS);
}
@Test
diff --git a/DnsResolver/Android.bp b/bpf/dns_helper/Android.bp
similarity index 100%
rename from DnsResolver/Android.bp
rename to bpf/dns_helper/Android.bp
diff --git a/DnsResolver/DnsBpfHelper.cpp b/bpf/dns_helper/DnsBpfHelper.cpp
similarity index 100%
rename from DnsResolver/DnsBpfHelper.cpp
rename to bpf/dns_helper/DnsBpfHelper.cpp
diff --git a/DnsResolver/DnsBpfHelper.h b/bpf/dns_helper/DnsBpfHelper.h
similarity index 100%
rename from DnsResolver/DnsBpfHelper.h
rename to bpf/dns_helper/DnsBpfHelper.h
diff --git a/DnsResolver/DnsBpfHelperTest.cpp b/bpf/dns_helper/DnsBpfHelperTest.cpp
similarity index 100%
rename from DnsResolver/DnsBpfHelperTest.cpp
rename to bpf/dns_helper/DnsBpfHelperTest.cpp
diff --git a/DnsResolver/DnsHelper.cpp b/bpf/dns_helper/DnsHelper.cpp
similarity index 100%
rename from DnsResolver/DnsHelper.cpp
rename to bpf/dns_helper/DnsHelper.cpp
diff --git a/DnsResolver/include/DnsHelperPublic.h b/bpf/dns_helper/include/DnsHelperPublic.h
similarity index 100%
rename from DnsResolver/include/DnsHelperPublic.h
rename to bpf/dns_helper/include/DnsHelperPublic.h
diff --git a/DnsResolver/libcom.android.tethering.dns_helper.map.txt b/bpf/dns_helper/libcom.android.tethering.dns_helper.map.txt
similarity index 100%
rename from DnsResolver/libcom.android.tethering.dns_helper.map.txt
rename to bpf/dns_helper/libcom.android.tethering.dns_helper.map.txt
diff --git a/staticlibs/native/bpf_headers/Android.bp b/bpf/headers/Android.bp
similarity index 93%
rename from staticlibs/native/bpf_headers/Android.bp
rename to bpf/headers/Android.bp
index d55584a..aaf8d8d 100644
--- a/staticlibs/native/bpf_headers/Android.bp
+++ b/bpf/headers/Android.bp
@@ -48,11 +48,10 @@
"BpfMapTest.cpp",
"BpfRingbufTest.cpp",
],
- defaults: ["bpf_defaults"],
+ defaults: ["bpf_cc_defaults"],
cflags: [
- "-Wall",
- "-Werror",
- "-Wno-error=unused-variable",
+ "-Wno-unused-variable",
+ "-Wno-sign-compare",
],
header_libs: ["bpf_headers"],
static_libs: ["libgmock"],
diff --git a/staticlibs/native/bpf_headers/BpfMapTest.cpp b/bpf/headers/BpfMapTest.cpp
similarity index 100%
rename from staticlibs/native/bpf_headers/BpfMapTest.cpp
rename to bpf/headers/BpfMapTest.cpp
diff --git a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp b/bpf/headers/BpfRingbufTest.cpp
similarity index 100%
rename from staticlibs/native/bpf_headers/BpfRingbufTest.cpp
rename to bpf/headers/BpfRingbufTest.cpp
diff --git a/staticlibs/native/bpf_headers/TEST_MAPPING b/bpf/headers/TEST_MAPPING
similarity index 100%
rename from staticlibs/native/bpf_headers/TEST_MAPPING
rename to bpf/headers/TEST_MAPPING
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h b/bpf/headers/include/bpf/BpfClassic.h
similarity index 95%
rename from staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
rename to bpf/headers/include/bpf/BpfClassic.h
index 81be37d..e6cef89 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
+++ b/bpf/headers/include/bpf/BpfClassic.h
@@ -63,6 +63,10 @@
#define BPF_LOAD_SKB_PROTOCOL \
BPF_STMT(BPF_LD | BPF_H | BPF_ABS, (__u32)SKF_AD_OFF + SKF_AD_PROTOCOL)
+// loads skb->pkt_type (0..7: see uapi/linux/if_packet.h PACKET_* constants)
+#define BPF_LOAD_SKB_PKTTYPE \
+ BPF_STMT(BPF_LD | BPF_B | BPF_ABS, (__u32)SKF_AD_OFF + SKF_AD_PKTTYPE)
+
// 8-bit load relative to start of link layer (mac/ethernet) header.
#define BPF_LOAD_MAC_RELATIVE_U8(ofs) \
BPF_STMT(BPF_LD | BPF_B | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
@@ -160,6 +164,9 @@
#define BPF_LOAD_NETX_RELATIVE_ICMP_TYPE BPF_LOAD_NETX_RELATIVE_L4_U8(0)
#define BPF_LOAD_NETX_RELATIVE_ICMP_CODE BPF_LOAD_NETX_RELATIVE_L4_U8(1)
+// IGMP start with u8 type
+#define BPF_LOAD_NETX_RELATIVE_IGMP_TYPE BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+
// IPv6 extension headers (HOPOPTS, DSTOPS, FRAG) begin with a u8 nexthdr
#define BPF_LOAD_NETX_RELATIVE_V6EXTHDR_NEXTHDR BPF_LOAD_NETX_RELATIVE_L4_U8(0)
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/bpf/headers/include/bpf/BpfMap.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/BpfMap.h
rename to bpf/headers/include/bpf/BpfMap.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/bpf/headers/include/bpf/BpfRingbuf.h
similarity index 94%
rename from staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
rename to bpf/headers/include/bpf/BpfRingbuf.h
index cd51004..5fe4ef7 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
+++ b/bpf/headers/include/bpf/BpfRingbuf.h
@@ -20,6 +20,7 @@
#include <android-base/unique_fd.h>
#include <linux/bpf.h>
#include <poll.h>
+#include <sys/epoll.h>
#include <sys/mman.h>
#include <utils/Log.h>
@@ -33,7 +34,7 @@
// BpfRingbufBase contains the non-templated functionality of BPF ring buffers.
class BpfRingbufBase {
public:
- ~BpfRingbufBase() {
+ virtual ~BpfRingbufBase() {
if (mConsumerPos) munmap(mConsumerPos, mConsumerSize);
if (mProducerPos) munmap(mProducerPos, mProducerSize);
mConsumerPos = nullptr;
@@ -98,6 +99,7 @@
// 32-bit kernel will just ignore the high-order bits.
std::atomic_uint64_t* mConsumerPos = nullptr;
std::atomic_uint32_t* mProducerPos = nullptr;
+ std::atomic_uint32_t* mLength = nullptr;
// In order to guarantee atomic access in a 32 bit userspace environment, atomic_uint64_t is used
// in addition to std::atomic<T>::is_always_lock_free that guarantees that read / write operations
@@ -139,12 +141,24 @@
static base::Result<std::unique_ptr<BpfRingbuf<Value>>> Create(
const char* path);
+ int epoll_ctl_add(int epfd, struct epoll_event *event) {
+ return epoll_ctl(epfd, EPOLL_CTL_ADD, mRingFd.get(), event);
+ }
+
+ int epoll_ctl_mod(int epfd, struct epoll_event *event) {
+ return epoll_ctl(epfd, EPOLL_CTL_MOD, mRingFd.get(), event);
+ }
+
+ int epoll_ctl_del(int epfd) {
+ return epoll_ctl(epfd, EPOLL_CTL_DEL, mRingFd.get(), NULL);
+ }
+
// Consumes all messages from the ring buffer, passing them to the callback.
// Returns the number of messages consumed or a non-ok result on error. If the
// ring buffer has no pending messages an OK result with count 0 is returned.
base::Result<int> ConsumeAll(const MessageCallback& callback);
- private:
+ protected:
// Empty ctor for use by Create.
BpfRingbuf() : BpfRingbufBase(sizeof(Value)) {}
};
@@ -234,7 +248,8 @@
// u32 len;
// u32 pg_off;
// };
- uint32_t length = *reinterpret_cast<volatile uint32_t*>(start_ptr);
+ mLength = reinterpret_cast<decltype(mLength)>(start_ptr);
+ uint32_t length = mLength->load(std::memory_order_acquire);
// If the sample isn't committed, we're caught up with the producer.
if (length & BPF_RINGBUF_BUSY_BIT) return count;
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h b/bpf/headers/include/bpf/BpfUtils.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
rename to bpf/headers/include/bpf/BpfUtils.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h b/bpf/headers/include/bpf/KernelUtils.h
similarity index 97%
rename from staticlibs/native/bpf_headers/include/bpf/KernelUtils.h
rename to bpf/headers/include/bpf/KernelUtils.h
index 417a5c4..68bc607 100644
--- a/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h
+++ b/bpf/headers/include/bpf/KernelUtils.h
@@ -55,11 +55,12 @@
isKernelVersion(4, 9) || // minimum for Android S & T
isKernelVersion(4, 14) || // minimum for Android U
isKernelVersion(4, 19) || // minimum for Android V
- isKernelVersion(5, 4) || // first supported in Android R
+ isKernelVersion(5, 4) || // first supported in Android R, min for W
isKernelVersion(5, 10) || // first supported in Android S
isKernelVersion(5, 15) || // first supported in Android T
isKernelVersion(6, 1) || // first supported in Android U
- isKernelVersion(6, 6); // first supported in Android V
+ isKernelVersion(6, 6) || // first supported in Android V
+ isKernelVersion(6, 12); // first supported in Android W
}
// Figure out the bitness of userspace.
diff --git a/staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h b/bpf/headers/include/bpf/WaitForProgsLoaded.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
rename to bpf/headers/include/bpf/WaitForProgsLoaded.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
similarity index 90%
rename from staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
rename to bpf/headers/include/bpf_helpers.h
index 4ddec8b..b994a9f 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -7,7 +7,7 @@
#include "bpf_map_def.h"
/******************************************************************************
- * WARNING: CHANGES TO THIS FILE OUTSIDE OF AOSP/MASTER ARE LIKELY TO BREAK *
+ * WARNING: CHANGES TO THIS FILE OUTSIDE OF AOSP/MAIN ARE LIKELY TO BREAK *
* DEVICE COMPATIBILITY WITH MAINLINE MODULES SHIPPING EBPF CODE. *
* *
* THIS WILL LIKELY RESULT IN BRICKED DEVICES AT SOME ARBITRARY FUTURE TIME *
@@ -50,18 +50,21 @@
// Note: this value (and the following +1u's) are hardcoded in NetBpfLoad.cpp
#define BPFLOADER_MAINLINE_VERSION 42u
-// Android Mainline BpfLoader when running on Android T
+// Android Mainline BpfLoader when running on Android T (sdk=33)
#define BPFLOADER_MAINLINE_T_VERSION (BPFLOADER_MAINLINE_VERSION + 1u)
-// Android Mainline BpfLoader when running on Android U
+// Android Mainline BpfLoader when running on Android U (sdk=34)
#define BPFLOADER_MAINLINE_U_VERSION (BPFLOADER_MAINLINE_T_VERSION + 1u)
// Android Mainline BpfLoader when running on Android U QPR3
#define BPFLOADER_MAINLINE_U_QPR3_VERSION (BPFLOADER_MAINLINE_U_VERSION + 1u)
-// Android Mainline BpfLoader when running on Android V
+// Android Mainline BpfLoader when running on Android V (sdk=35)
#define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_QPR3_VERSION + 1u)
+// Android Mainline BpfLoader when running on Android W (sdk=36)
+#define BPFLOADER_MAINLINE_W_VERSION (BPFLOADER_MAINLINE_V_VERSION + 1u)
+
/* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
* before #include "bpf_helpers.h" to change which bpfloaders will
* process the resulting .o file.
@@ -71,11 +74,11 @@
* In which case it's just best to use the default.
*/
#ifndef BPFLOADER_MIN_VER
-#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION // inclusive, ie. >=
#endif
#ifndef BPFLOADER_MAX_VER
-#define BPFLOADER_MAX_VER DEFAULT_BPFLOADER_MAX_VER
+#define BPFLOADER_MAX_VER 0x10000u // exclusive, ie. < v1.0
#endif
/* place things in different elf sections */
@@ -99,23 +102,19 @@
*
* If missing, bpfloader_{min/max}_ver default to 0/0x10000 ie. [v0.0, v1.0),
* while size_of_bpf_{map/prog}_def default to 32/20 which are the v0.0 sizes.
- */
-#define LICENSE(NAME) \
- unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER; \
- unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER; \
- size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def); \
- size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
- char _license[] SECTION("license") = (NAME)
-
-/* This macro disables loading BTF map debug information on Android <=U *and* all user builds.
*
- * Note: Bpfloader v0.39+ honours 'btf_user_min_bpfloader_ver' on user builds,
- * and 'btf_min_bpfloader_ver' on non-user builds.
- * Older BTF capable versions unconditionally honour 'btf_min_bpfloader_ver'
+ * This macro also disables loading BTF map debug information, as versions
+ * of the platform bpfloader that support BTF require fork-exec of btfloader
+ * which causes a regression in boot time.
*/
-#define DISABLE_BTF_ON_USER_BUILDS() \
- unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = 39u; \
- unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu
+#define LICENSE(NAME) \
+ unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER; \
+ unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER; \
+ size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def); \
+ size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
+ unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = BPFLOADER_MAINLINE_VERSION; \
+ unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu; \
+ char _license[] SECTION("license") = (NAME)
/* flag the resulting bpf .o file as critical to system functionality,
* loading all kernel version appropriate programs in it must succeed
@@ -292,6 +291,12 @@
bpf_ringbuf_submit_unsafe(v, 0); \
}
+#define DEFINE_BPF_RINGBUF(the_map, ValueType, size_bytes, usr, grp, md) \
+ DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md, \
+ DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, \
+ PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
/* There exist buggy kernels with pre-T OS, that due to
* kernel patch "[ALPS05162612] bpf: fix ubsan error"
* do not support userspace writes into non-zero index of bpf map arrays.
@@ -350,11 +355,17 @@
#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
#endif
-#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
- DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, PRIVATE, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG, \
- LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// for maps not meant to be accessed from userspace
+#define DEFINE_BPF_MAP_KERNEL_INTERNAL(the_map, TYPE, KeyType, ValueType, num_entries) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, AID_ROOT, AID_ROOT, \
+ 0000, "fs_bpf_loader", "", PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
+ DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, \
+ PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -387,12 +398,19 @@
static int (*bpf_probe_read_user_str)(void* dst, int size, const void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_user_str;
static unsigned long long (*bpf_ktime_get_ns)(void) = (void*) BPF_FUNC_ktime_get_ns;
static unsigned long long (*bpf_ktime_get_boot_ns)(void) = (void*)BPF_FUNC_ktime_get_boot_ns;
-static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
static unsigned long long (*bpf_get_current_pid_tgid)(void) = (void*) BPF_FUNC_get_current_pid_tgid;
static unsigned long long (*bpf_get_current_uid_gid)(void) = (void*) BPF_FUNC_get_current_uid_gid;
static unsigned long long (*bpf_get_smp_processor_id)(void) = (void*) BPF_FUNC_get_smp_processor_id;
static long (*bpf_get_stackid)(void* ctx, void* map, uint64_t flags) = (void*) BPF_FUNC_get_stackid;
static long (*bpf_get_current_comm)(void* buf, uint32_t buf_size) = (void*) BPF_FUNC_get_current_comm;
+// bpf_sk_fullsock requires 5.1+ kernel
+static struct bpf_sock* (*bpf_sk_fullsock)(struct bpf_sock* sk) = (void*) BPF_FUNC_sk_fullsock;
+
+// GPL only:
+static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
+#define bpf_printf(s, n...) bpf_trace_printk(s, sizeof(s), ## n)
+// Note: bpf only supports up to 3 arguments, log via: bpf_printf("msg %d %d %d", 1, 2, 3);
+// and read via the blocking: sudo cat /sys/kernel/debug/tracing/trace_pipe
#define DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
min_loader, max_loader, opt, selinux, pindir, ignore_eng, \
@@ -414,19 +432,10 @@
SECTION(SECTION_NAME) \
int the_prog
-#ifndef DEFAULT_BPF_PROG_SELINUX_CONTEXT
-#define DEFAULT_BPF_PROG_SELINUX_CONTEXT ""
-#endif
-
-#ifndef DEFAULT_BPF_PROG_PIN_SUBDIR
-#define DEFAULT_BPF_PROG_PIN_SUBDIR ""
-#endif
-
#define DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
opt) \
DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt, \
- DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt, "", "", \
LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
// Programs (here used in the sense of functions/sections) marked optional are allowed to fail
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/bpf/headers/include/bpf_map_def.h
similarity index 95%
rename from staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
rename to bpf/headers/include/bpf_map_def.h
index 00ef91a..2d6736c 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/bpf/headers/include/bpf_map_def.h
@@ -29,7 +29,7 @@
* *
* ! ! ! W A R N I N G ! ! ! *
* *
- * CHANGES TO THESE STRUCTURE DEFINITIONS OUTSIDE OF AOSP/MASTER *WILL* BREAK *
+ * CHANGES TO THESE STRUCTURE DEFINITIONS OUTSIDE OF AOSP/MAIN *WILL* BREAK *
* MAINLINE MODULE COMPATIBILITY *
* *
* AND THUS MAY RESULT IN YOUR DEVICE BRICKING AT SOME ARBITRARY POINT IN *
@@ -42,12 +42,6 @@
* *
******************************************************************************/
-// These are the values used if these fields are missing
-#define DEFAULT_BPFLOADER_MIN_VER 0u // v0.0 (this is inclusive ie. >= v0.0)
-#define DEFAULT_BPFLOADER_MAX_VER 0x10000u // v1.0 (this is exclusive ie. < v1.0)
-#define DEFAULT_SIZEOF_BPF_MAP_DEF 32 // v0.0 struct: enum (uint sized) + 7 uint
-#define DEFAULT_SIZEOF_BPF_PROG_DEF 20 // v0.0 struct: 4 uint + bool + 3 byte alignment pad
-
/*
* The bpf_{map,prog}_def structures are compiled for different architectures.
* Once by the BPF compiler for the BPF architecture, and once by a C++
diff --git a/netbpfload/Android.bp b/bpf/loader/Android.bp
similarity index 92%
rename from netbpfload/Android.bp
rename to bpf/loader/Android.bp
index 908bb13..b08913a 100644
--- a/netbpfload/Android.bp
+++ b/bpf/loader/Android.bp
@@ -33,12 +33,7 @@
cc_binary {
name: "netbpfload",
- defaults: ["bpf_defaults"],
- cflags: [
- "-Wall",
- "-Werror",
- "-Wthread-safety",
- ],
+ defaults: ["bpf_cc_defaults"],
sanitize: {
integer_overflow: true,
},
@@ -48,10 +43,7 @@
"libbase",
"liblog",
],
- srcs: [
- "loader.cpp",
- "NetBpfLoad.cpp",
- ],
+ srcs: ["NetBpfLoad.cpp"],
apex_available: [
"com.android.tethering",
"//apex_available:platform",
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
new file mode 100644
index 0000000..c2a1d6e
--- /dev/null
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -0,0 +1,1697 @@
+/*
+ * Copyright (C) 2018-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.
+ */
+
+#define LOG_TAG "NetBpfLoad"
+
+#include <arpa/inet.h>
+#include <dirent.h>
+#include <elf.h>
+#include <errno.h>
+#include <error.h>
+#include <fcntl.h>
+#include <fstream>
+#include <inttypes.h>
+#include <iostream>
+#include <linux/unistd.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <optional>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <string>
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <sysexits.h>
+#include <unistd.h>
+#include <unordered_map>
+#include <vector>
+
+#include <android-base/cmsg.h>
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/macros.h>
+#include <android-base/properties.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <android/api-level.h>
+
+#include "BpfSyscallWrappers.h"
+#include "bpf/BpfUtils.h"
+#include "bpf_map_def.h"
+
+// The following matches bpf_helpers.h, which is only for inclusion in bpf code
+#define BPFLOADER_MAINLINE_VERSION 42u
+
+using android::base::EndsWith;
+using android::base::GetIntProperty;
+using android::base::GetProperty;
+using android::base::InitLogging;
+using android::base::KernelLogger;
+using android::base::SetProperty;
+using android::base::Split;
+using android::base::StartsWith;
+using android::base::Tokenize;
+using android::base::unique_fd;
+using std::ifstream;
+using std::ios;
+using std::optional;
+using std::string;
+using std::vector;
+
+namespace android {
+namespace bpf {
+
+// Bpf programs may specify per-program & per-map selinux_context and pin_subdir.
+//
+// The BpfLoader needs to convert these bpf.o specified strings into an enum
+// for internal use (to check that valid values were specified for the specific
+// location of the bpf.o file).
+//
+// It also needs to map selinux_context's into pin_subdir's.
+// This is because of how selinux_context is actually implemented via pin+rename.
+//
+// Thus 'domain' enumerates all selinux_context's/pin_subdir's that the BpfLoader
+// is aware of. Thus there currently needs to be a 1:1 mapping between the two.
+//
+enum class domain : int {
+ unspecified = 0, // means just use the default for that specific pin location
+ tethering, // (S+) fs_bpf_tethering /sys/fs/bpf/tethering
+ net_private, // (T+) fs_bpf_net_private /sys/fs/bpf/net_private
+ net_shared, // (T+) fs_bpf_net_shared /sys/fs/bpf/net_shared
+ netd_readonly, // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
+ netd_shared, // (T+) fs_bpf_netd_shared /sys/fs/bpf/netd_shared
+ loader, // (U+) fs_bpf_loader /sys/fs/bpf/loader
+ // on T due to lack of sepolicy/genfscon rules it behaves simply as 'fs_bpf'
+};
+
+static constexpr domain AllDomains[] = {
+ domain::unspecified,
+ domain::tethering,
+ domain::net_private,
+ domain::net_shared,
+ domain::netd_readonly,
+ domain::netd_shared,
+ domain::loader,
+};
+
+static constexpr bool specified(domain d) {
+ return d != domain::unspecified;
+}
+
+struct Location {
+ const char* const dir = "";
+ const char* const prefix = "";
+};
+
+// Returns the build type string (from ro.build.type).
+const std::string& getBuildType() {
+ static std::string t = GetProperty("ro.build.type", "unknown");
+ return t;
+}
+
+// The following functions classify the 3 Android build types.
+inline bool isEng() {
+ return getBuildType() == "eng";
+}
+
+inline bool isUser() {
+ return getBuildType() == "user";
+}
+
+inline bool isUserdebug() {
+ return getBuildType() == "userdebug";
+}
+
+#define BPF_FS_PATH "/sys/fs/bpf/"
+
+static unsigned int page_size = static_cast<unsigned int>(getpagesize());
+
+constexpr const char* lookupSelinuxContext(const domain d) {
+ switch (d) {
+ case domain::unspecified: return "";
+ case domain::tethering: return "fs_bpf_tethering";
+ case domain::net_private: return "fs_bpf_net_private";
+ case domain::net_shared: return "fs_bpf_net_shared";
+ case domain::netd_readonly: return "fs_bpf_netd_readonly";
+ case domain::netd_shared: return "fs_bpf_netd_shared";
+ case domain::loader: return "fs_bpf_loader";
+ }
+}
+
+domain getDomainFromSelinuxContext(const char s[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE]) {
+ for (domain d : AllDomains) {
+ // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+ if (strlen(lookupSelinuxContext(d)) >= BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE) abort();
+ if (!strncmp(s, lookupSelinuxContext(d), BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE)) return d;
+ }
+ ALOGE("unrecognized selinux_context '%-32s'", s);
+ // Note: we *can* just abort() here as we only load bpf .o files shipped
+ // in the same mainline module / apex as NetBpfLoad itself.
+ abort();
+}
+
+constexpr const char* lookupPinSubdir(const domain d, const char* const unspecified = "") {
+ switch (d) {
+ case domain::unspecified: return unspecified;
+ case domain::tethering: return "tethering/";
+ case domain::net_private: return "net_private/";
+ case domain::net_shared: return "net_shared/";
+ case domain::netd_readonly: return "netd_readonly/";
+ case domain::netd_shared: return "netd_shared/";
+ case domain::loader: return "loader/";
+ }
+};
+
+domain getDomainFromPinSubdir(const char s[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE]) {
+ for (domain d : AllDomains) {
+ // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+ if (strlen(lookupPinSubdir(d)) >= BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE) abort();
+ if (!strncmp(s, lookupPinSubdir(d), BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE)) return d;
+ }
+ ALOGE("unrecognized pin_subdir '%-32s'", s);
+ // Note: we *can* just abort() here as we only load bpf .o files shipped
+ // in the same mainline module / apex as NetBpfLoad itself.
+ abort();
+}
+
+static string pathToObjName(const string& path) {
+ // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
+ string filename = Split(path, "/").back();
+ // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
+ string name = filename.substr(0, filename.find_last_of('.'));
+ // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
+ // this can be used to provide duplicate programs (mux based on the bpfloader version)
+ return name.substr(0, name.find_last_of('@'));
+}
+
+typedef struct {
+ const char* name;
+ enum bpf_prog_type type;
+ enum bpf_attach_type attach_type;
+} sectionType;
+
+/*
+ * Map section name prefixes to program types, the section name will be:
+ * SECTION(<prefix>/<name-of-program>)
+ * For example:
+ * SECTION("tracepoint/sched_switch_func") where sched_switch_funcs
+ * is the name of the program, and tracepoint is the type.
+ *
+ * However, be aware that you should not be directly using the SECTION() macro.
+ * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE macros.
+ *
+ * Programs shipped inside the tethering apex should be limited to networking stuff,
+ * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
+ * since they are less stable abi/api and may conflict with platform uses of bpf.
+ */
+sectionType sectionNameTypes[] = {
+ {"bind4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
+ {"bind6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
+ {"cgroupskb/", BPF_PROG_TYPE_CGROUP_SKB},
+ {"cgroupsock/", BPF_PROG_TYPE_CGROUP_SOCK},
+ {"cgroupsockcreate/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET_SOCK_CREATE},
+ {"cgroupsockrelease/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET_SOCK_RELEASE},
+ {"connect4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
+ {"connect6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
+ {"egress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_EGRESS},
+ {"getsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_GETSOCKOPT},
+ {"ingress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_INGRESS},
+ {"postbind4/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET4_POST_BIND},
+ {"postbind6/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET6_POST_BIND},
+ {"recvmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
+ {"recvmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
+ {"schedact/", BPF_PROG_TYPE_SCHED_ACT},
+ {"schedcls/", BPF_PROG_TYPE_SCHED_CLS},
+ {"sendmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
+ {"sendmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
+ {"setsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_SETSOCKOPT},
+ {"skfilter/", BPF_PROG_TYPE_SOCKET_FILTER},
+ {"sockops/", BPF_PROG_TYPE_SOCK_OPS, BPF_CGROUP_SOCK_OPS},
+ {"sysctl", BPF_PROG_TYPE_CGROUP_SYSCTL, BPF_CGROUP_SYSCTL},
+ {"xdp/", BPF_PROG_TYPE_XDP},
+};
+
+typedef struct {
+ enum bpf_prog_type type;
+ enum bpf_attach_type attach_type;
+ string name;
+ vector<char> data;
+ vector<char> rel_data;
+ optional<struct bpf_prog_def> prog_def;
+
+ unique_fd prog_fd; // fd after loading
+} codeSection;
+
+static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
+ elfFile.seekg(0);
+ if (elfFile.fail()) return -1;
+
+ if (!elfFile.read((char*)eh, sizeof(*eh))) return -1;
+
+ return 0;
+}
+
+// Reads all section header tables into an Shdr array
+static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
+ Elf64_Ehdr eh;
+ int ret = 0;
+
+ ret = readElfHeader(elfFile, &eh);
+ if (ret) return ret;
+
+ elfFile.seekg(eh.e_shoff);
+ if (elfFile.fail()) return -1;
+
+ // Read shdr table entries
+ shTable.resize(eh.e_shnum);
+
+ if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
+
+ return 0;
+}
+
+// Read a section by its index - for ex to get sec hdr strtab blob
+static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
+ vector<Elf64_Shdr> shTable;
+ int ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ elfFile.seekg(shTable[id].sh_offset);
+ if (elfFile.fail()) return -1;
+
+ sec.resize(shTable[id].sh_size);
+ if (!elfFile.read(sec.data(), shTable[id].sh_size)) return -1;
+
+ return 0;
+}
+
+// Read whole section header string table
+static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
+ Elf64_Ehdr eh;
+ int ret = readElfHeader(elfFile, &eh);
+ if (ret) return ret;
+
+ ret = readSectionByIdx(elfFile, eh.e_shstrndx, strtab);
+ if (ret) return ret;
+
+ return 0;
+}
+
+// Get name from offset in strtab
+static int getSymName(ifstream& elfFile, int nameOff, string& name) {
+ int ret;
+ vector<char> secStrTab;
+
+ ret = readSectionHeaderStrtab(elfFile, secStrTab);
+ if (ret) return ret;
+
+ if (nameOff >= (int)secStrTab.size()) return -1;
+
+ name = string((char*)secStrTab.data() + nameOff);
+ return 0;
+}
+
+// Reads a full section by name - example to get the GPL license
+static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
+ vector<char> secStrTab;
+ vector<Elf64_Shdr> shTable;
+ int ret;
+
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ ret = readSectionHeaderStrtab(elfFile, secStrTab);
+ if (ret) return ret;
+
+ for (int i = 0; i < (int)shTable.size(); i++) {
+ char* secname = secStrTab.data() + shTable[i].sh_name;
+ if (!secname) continue;
+
+ if (!strcmp(secname, name)) {
+ vector<char> dataTmp;
+ dataTmp.resize(shTable[i].sh_size);
+
+ elfFile.seekg(shTable[i].sh_offset);
+ if (elfFile.fail()) return -1;
+
+ if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+ data = dataTmp;
+ return 0;
+ }
+ }
+ return -2;
+}
+
+unsigned int readSectionUint(const char* name, ifstream& elfFile) {
+ vector<char> theBytes;
+ int ret = readSectionByName(name, elfFile, theBytes);
+ if (ret) {
+ ALOGE("Couldn't find section %s.", name);
+ abort();
+ } else if (theBytes.size() < sizeof(unsigned int)) {
+ ALOGE("Section %s is too short.", name);
+ abort();
+ } else {
+ // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
+ unsigned int value = static_cast<unsigned char>(theBytes[3]);
+ value <<= 8;
+ value += static_cast<unsigned char>(theBytes[2]);
+ value <<= 8;
+ value += static_cast<unsigned char>(theBytes[1]);
+ value <<= 8;
+ value += static_cast<unsigned char>(theBytes[0]);
+ ALOGD("Section %s value is %u [0x%x]", name, value, value);
+ return value;
+ }
+}
+
+static int readSectionByType(ifstream& elfFile, int type, vector<char>& data) {
+ int ret;
+ vector<Elf64_Shdr> shTable;
+
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ for (int i = 0; i < (int)shTable.size(); i++) {
+ if ((int)shTable[i].sh_type != type) continue;
+
+ vector<char> dataTmp;
+ dataTmp.resize(shTable[i].sh_size);
+
+ elfFile.seekg(shTable[i].sh_offset);
+ if (elfFile.fail()) return -1;
+
+ if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+ data = dataTmp;
+ return 0;
+ }
+ return -2;
+}
+
+static bool symCompare(Elf64_Sym a, Elf64_Sym b) {
+ return (a.st_value < b.st_value);
+}
+
+static int readSymTab(ifstream& elfFile, int sort, vector<Elf64_Sym>& data) {
+ int ret, numElems;
+ Elf64_Sym* buf;
+ vector<char> secData;
+
+ ret = readSectionByType(elfFile, SHT_SYMTAB, secData);
+ if (ret) return ret;
+
+ buf = (Elf64_Sym*)secData.data();
+ numElems = (secData.size() / sizeof(Elf64_Sym));
+ data.assign(buf, buf + numElems);
+
+ if (sort) std::sort(data.begin(), data.end(), symCompare);
+ return 0;
+}
+
+static enum bpf_prog_type getSectionType(string& name) {
+ for (auto& snt : sectionNameTypes)
+ if (StartsWith(name, snt.name)) return snt.type;
+
+ return BPF_PROG_TYPE_UNSPEC;
+}
+
+static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd) {
+ vector<char> pdData;
+ int ret = readSectionByName("progs", elfFile, pdData);
+ if (ret) return ret;
+
+ if (pdData.size() % sizeof(struct bpf_prog_def)) {
+ ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
+ pdData.size(), sizeof(struct bpf_prog_def));
+ return -1;
+ };
+
+ pd.resize(pdData.size() / sizeof(struct bpf_prog_def));
+
+ const char* dataPtr = pdData.data();
+ for (auto& p : pd) {
+ // Copy the structure from the ELF file and move to the next one.
+ memcpy(&p, dataPtr, sizeof(struct bpf_prog_def));
+ dataPtr += sizeof(struct bpf_prog_def);
+ }
+ return 0;
+}
+
+static int getSectionSymNames(ifstream& elfFile, const string& sectionName, vector<string>& names,
+ optional<unsigned> symbolType = std::nullopt) {
+ int ret;
+ string name;
+ vector<Elf64_Sym> symtab;
+ vector<Elf64_Shdr> shTable;
+
+ ret = readSymTab(elfFile, 1 /* sort */, symtab);
+ if (ret) return ret;
+
+ // Get index of section
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ int sec_idx = -1;
+ for (int i = 0; i < (int)shTable.size(); i++) {
+ ret = getSymName(elfFile, shTable[i].sh_name, name);
+ if (ret) return ret;
+
+ if (!name.compare(sectionName)) {
+ sec_idx = i;
+ break;
+ }
+ }
+
+ // No section found with matching name
+ if (sec_idx == -1) {
+ ALOGW("No %s section could be found in elf object", sectionName.c_str());
+ return -1;
+ }
+
+ for (int i = 0; i < (int)symtab.size(); i++) {
+ if (symbolType.has_value() && ELF_ST_TYPE(symtab[i].st_info) != symbolType) continue;
+
+ if (symtab[i].st_shndx == sec_idx) {
+ string s;
+ ret = getSymName(elfFile, symtab[i].st_name, s);
+ if (ret) return ret;
+ names.push_back(s);
+ }
+ }
+
+ return 0;
+}
+
+// Read a section by its index - for ex to get sec hdr strtab blob
+static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs) {
+ vector<Elf64_Shdr> shTable;
+ int entries, ret = 0;
+
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+ entries = shTable.size();
+
+ vector<struct bpf_prog_def> pd;
+ ret = readProgDefs(elfFile, pd);
+ if (ret) return ret;
+ vector<string> progDefNames;
+ ret = getSectionSymNames(elfFile, "progs", progDefNames);
+ if (!pd.empty() && ret) return ret;
+
+ for (int i = 0; i < entries; i++) {
+ string name;
+ codeSection cs_temp;
+ cs_temp.type = BPF_PROG_TYPE_UNSPEC;
+
+ ret = getSymName(elfFile, shTable[i].sh_name, name);
+ if (ret) return ret;
+
+ enum bpf_prog_type ptype = getSectionType(name);
+
+ if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
+
+ // This must be done before '/' is replaced with '_'.
+ for (auto& snt : sectionNameTypes)
+ if (StartsWith(name, snt.name)) cs_temp.attach_type = snt.attach_type;
+
+ string oldName = name;
+
+ // convert all slashes to underscores
+ std::replace(name.begin(), name.end(), '/', '_');
+
+ cs_temp.type = ptype;
+ cs_temp.name = name;
+
+ ret = readSectionByIdx(elfFile, i, cs_temp.data);
+ if (ret) return ret;
+ ALOGV("Loaded code section %d (%s)", i, name.c_str());
+
+ vector<string> csSymNames;
+ ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
+ if (ret || !csSymNames.size()) return ret;
+ for (size_t i = 0; i < progDefNames.size(); ++i) {
+ if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
+ cs_temp.prog_def = pd[i];
+ break;
+ }
+ }
+
+ // Check for rel section
+ if (cs_temp.data.size() > 0 && i < entries) {
+ ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
+ if (ret) return ret;
+
+ if (name == (".rel" + oldName)) {
+ ret = readSectionByIdx(elfFile, i + 1, cs_temp.rel_data);
+ if (ret) return ret;
+ ALOGV("Loaded relo section %d (%s)", i, name.c_str());
+ }
+ }
+
+ if (cs_temp.data.size() > 0) {
+ cs.push_back(std::move(cs_temp));
+ ALOGV("Adding section %d to cs list", i);
+ }
+ }
+ return 0;
+}
+
+static int getSymNameByIdx(ifstream& elfFile, int index, string& name) {
+ vector<Elf64_Sym> symtab;
+ int ret = 0;
+
+ ret = readSymTab(elfFile, 0 /* !sort */, symtab);
+ if (ret) return ret;
+
+ if (index >= (int)symtab.size()) return -1;
+
+ return getSymName(elfFile, symtab[index].st_name, name);
+}
+
+static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
+ const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
+ // bpfGetFd... family of functions require at minimum a 4.14 kernel,
+ // so on 4.9-T kernels just pretend the map matches our expectations.
+ // Additionally we'll get almost equivalent test coverage on newer devices/kernels.
+ // This is because the primary failure mode we're trying to detect here
+ // is either a source code misconfiguration (which is likely kernel independent)
+ // or a newly introduced kernel feature/bug (which is unlikely to get backported to 4.9).
+ if (!isAtLeastKernelVersion(4, 14, 0)) return true;
+
+ // Assuming fd is a valid Bpf Map file descriptor then
+ // all the following should always succeed on a 4.14+ kernel.
+ // If they somehow do fail, they'll return -1 (and set errno),
+ // which should then cause (among others) a key_size mismatch.
+ int fd_type = bpfGetFdMapType(fd);
+ int fd_key_size = bpfGetFdKeySize(fd);
+ int fd_value_size = bpfGetFdValueSize(fd);
+ int fd_max_entries = bpfGetFdMaxEntries(fd);
+ int fd_map_flags = bpfGetFdMapFlags(fd);
+
+ // DEVMAPs are readonly from the bpf program side's point of view, as such
+ // the kernel in kernel/bpf/devmap.c dev_map_init_map() will set the flag
+ int desired_map_flags = (int)mapDef.map_flags;
+ if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
+ desired_map_flags |= BPF_F_RDONLY_PROG;
+
+ if (type == BPF_MAP_TYPE_LPM_TRIE)
+ desired_map_flags |= BPF_F_NO_PREALLOC;
+
+ // The .h file enforces that this is a power of two, and page size will
+ // also always be a power of two, so this logic is actually enough to
+ // force it to be a multiple of the page size, as required by the kernel.
+ unsigned int desired_max_entries = mapDef.max_entries;
+ if (type == BPF_MAP_TYPE_RINGBUF) {
+ if (desired_max_entries < page_size) desired_max_entries = page_size;
+ }
+
+ // The following checks should *never* trigger, if one of them somehow does,
+ // it probably means a bpf .o file has been changed/replaced at runtime
+ // and bpfloader was manually rerun (normally it should only run *once*
+ // early during the boot process).
+ // Another possibility is that something is misconfigured in the code:
+ // most likely a shared map is declared twice differently.
+ // But such a change should never be checked into the source tree...
+ if ((fd_type == type) &&
+ (fd_key_size == (int)mapDef.key_size) &&
+ (fd_value_size == (int)mapDef.value_size) &&
+ (fd_max_entries == (int)desired_max_entries) &&
+ (fd_map_flags == desired_map_flags)) {
+ return true;
+ }
+
+ ALOGE("bpf map name %s mismatch: desired/found: "
+ "type:%d/%d key:%u/%d value:%u/%d entries:%u/%d flags:%u/%d",
+ mapName.c_str(), type, fd_type, mapDef.key_size, fd_key_size, mapDef.value_size,
+ fd_value_size, mapDef.max_entries, fd_max_entries, desired_map_flags, fd_map_flags);
+ return false;
+}
+
+static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
+ const char* prefix, const unsigned int bpfloader_ver) {
+ int ret;
+ vector<char> mdData;
+ vector<struct bpf_map_def> md;
+ vector<string> mapNames;
+ string objName = pathToObjName(string(elfPath));
+
+ ret = readSectionByName("maps", elfFile, mdData);
+ if (ret == -2) return 0; // no maps to read
+ if (ret) return ret;
+
+ if (mdData.size() % sizeof(struct bpf_map_def)) {
+ ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
+ mdData.size(), sizeof(struct bpf_map_def));
+ return -1;
+ };
+
+ md.resize(mdData.size() / sizeof(struct bpf_map_def));
+
+ const char* dataPtr = mdData.data();
+ for (auto& m : md) {
+ // Copy the structure from the ELF file and move to the next one.
+ memcpy(&m, dataPtr, sizeof(struct bpf_map_def));
+ dataPtr += sizeof(struct bpf_map_def);
+ }
+
+ ret = getSectionSymNames(elfFile, "maps", mapNames);
+ if (ret) return ret;
+
+ unsigned kvers = kernelVersion();
+
+ for (int i = 0; i < (int)mapNames.size(); i++) {
+ if (md[i].zero != 0) abort();
+
+ if (bpfloader_ver < md[i].bpfloader_min_ver) {
+ ALOGD("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
+ md[i].bpfloader_min_ver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if (bpfloader_ver >= md[i].bpfloader_max_ver) {
+ ALOGD("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
+ md[i].bpfloader_max_ver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if (kvers < md[i].min_kver) {
+ ALOGD("skipping map %s which requires kernel version 0x%x >= 0x%x",
+ mapNames[i].c_str(), kvers, md[i].min_kver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if (kvers >= md[i].max_kver) {
+ ALOGD("skipping map %s which requires kernel version 0x%x < 0x%x",
+ mapNames[i].c_str(), kvers, md[i].max_kver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
+ (md[i].ignore_on_userdebug && isUserdebug())) {
+ ALOGD("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
+ getBuildType().c_str());
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if ((isArm() && isKernel32Bit() && md[i].ignore_on_arm32) ||
+ (isArm() && isKernel64Bit() && md[i].ignore_on_aarch64) ||
+ (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
+ (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
+ (isRiscV() && md[i].ignore_on_riscv64)) {
+ ALOGD("skipping map %s which is ignored on %s", mapNames[i].c_str(),
+ describeArch());
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ enum bpf_map_type type = md[i].type;
+ if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
+ // On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
+ // of be approximated: ARRAY has the same userspace api, though it is not usable
+ // by the same ebpf programs. However, that's okay because the bpf_redirect_map()
+ // helper doesn't exist on 4.9-T anyway (so the bpf program would fail to load,
+ // and thus needs to be tagged as 4.14+ either way), so there's nothing useful you
+ // could do with a DEVMAP anyway (that isn't already provided by an ARRAY)...
+ // Hence using an ARRAY instead of a DEVMAP simply makes life easier for userspace.
+ type = BPF_MAP_TYPE_ARRAY;
+ }
+ if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
+ // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
+ // of be approximated: HASH has the same userspace visible api.
+ // However it cannot be used by ebpf programs in the same way.
+ // Since bpf_redirect_map() only requires 4.14, a program using a DEVMAP_HASH map
+ // would fail to load (due to trying to redirect to a HASH instead of DEVMAP_HASH).
+ // One must thus tag any BPF_MAP_TYPE_DEVMAP_HASH + bpf_redirect_map() using
+ // programs as being 5.4+...
+ type = BPF_MAP_TYPE_HASH;
+ }
+
+ // The .h file enforces that this is a power of two, and page size will
+ // also always be a power of two, so this logic is actually enough to
+ // force it to be a multiple of the page size, as required by the kernel.
+ unsigned int max_entries = md[i].max_entries;
+ if (type == BPF_MAP_TYPE_RINGBUF) {
+ if (max_entries < page_size) max_entries = page_size;
+ }
+
+ domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
+ if (specified(selinux_context)) {
+ ALOGV("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
+ md[i].selinux_context, static_cast<int>(selinux_context),
+ lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
+ }
+
+ domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
+ if (specified(pin_subdir)) {
+ ALOGV("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
+ static_cast<int>(pin_subdir), lookupPinSubdir(pin_subdir));
+ }
+
+ // Format of pin location is /sys/fs/bpf/<pin_subdir|prefix>map_<objName>_<mapName>
+ // except that maps shared across .o's have empty <objName>
+ // Note: <objName> refers to the extension-less basename of the .o file (without @ suffix).
+ string mapPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "map_" +
+ (md[i].shared ? "" : objName) + "_" + mapNames[i];
+ bool reuse = false;
+ unique_fd fd;
+ int saved_errno;
+
+ if (access(mapPinLoc.c_str(), F_OK) == 0) {
+ fd.reset(mapRetrieveRO(mapPinLoc.c_str()));
+ saved_errno = errno;
+ ALOGD("bpf_create_map reusing map %s, ret: %d", mapNames[i].c_str(), fd.get());
+ reuse = true;
+ } else {
+ union bpf_attr req = {
+ .map_type = type,
+ .key_size = md[i].key_size,
+ .value_size = md[i].value_size,
+ .max_entries = max_entries,
+ .map_flags = md[i].map_flags | (type == BPF_MAP_TYPE_LPM_TRIE ? BPF_F_NO_PREALLOC : 0),
+ };
+ if (isAtLeastKernelVersion(4, 15, 0))
+ strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
+ fd.reset(bpf(BPF_MAP_CREATE, req));
+ saved_errno = errno;
+ if (fd.ok()) {
+ ALOGD("bpf_create_map[%s] -> %d", mapNames[i].c_str(), fd.get());
+ } else {
+ ALOGE("bpf_create_map[%s] -> %d errno:%d", mapNames[i].c_str(), fd.get(), saved_errno);
+ }
+ }
+
+ if (!fd.ok()) return -saved_errno;
+
+ // When reusing a pinned map, we need to check the map type/sizes/etc match, but for
+ // safety (since reuse code path is rare) run these checks even if we just created it.
+ // We assume failure is due to pinned map mismatch, hence the 'NOT UNIQUE' return code.
+ if (!mapMatchesExpectations(fd, mapNames[i], md[i], type)) return -ENOTUNIQ;
+
+ if (!reuse) {
+ if (specified(selinux_context)) {
+ string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+ "tmp_map_" + objName + "_" + mapNames[i];
+ ret = bpfFdPin(fd, createLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ ret = renameat2(AT_FDCWD, createLoc.c_str(),
+ AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
+ if (ret) {
+ int err = errno;
+ ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
+ err, strerror(err));
+ return -err;
+ }
+ } else {
+ ret = bpfFdPin(fd, mapPinLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ }
+ ret = chmod(mapPinLoc.c_str(), md[i].mode);
+ if (ret) {
+ int err = errno;
+ ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
+ strerror(err));
+ return -err;
+ }
+ ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
+ if (ret) {
+ int err = errno;
+ ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
+ ret, err, strerror(err));
+ return -err;
+ }
+ }
+
+ int mapId = bpfGetFdMapId(fd);
+ if (mapId == -1) {
+ if (isAtLeastKernelVersion(4, 14, 0))
+ ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
+ } else {
+ ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
+ }
+
+ mapFds.push_back(std::move(fd));
+ }
+
+ return ret;
+}
+
+static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
+ int insnIndex;
+ struct bpf_insn *insn, *insns;
+
+ insns = (struct bpf_insn*)(insnsPtr);
+
+ insnIndex = offset / sizeof(struct bpf_insn);
+ insn = &insns[insnIndex];
+
+ // Occasionally might be useful for relocation debugging, but pretty spammy
+ if (0) {
+ ALOGV("applying relo to instruction at byte offset: %llu, "
+ "insn offset %d, insn %llx",
+ (unsigned long long)offset, insnIndex, *(unsigned long long*)insn);
+ }
+
+ if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
+ ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
+ return;
+ }
+
+ insn->imm = fd;
+ insn->src_reg = BPF_PSEUDO_MAP_FD;
+}
+
+static void applyMapRelo(ifstream& elfFile, vector<unique_fd> &mapFds, vector<codeSection>& cs) {
+ vector<string> mapNames;
+
+ int ret = getSectionSymNames(elfFile, "maps", mapNames);
+ if (ret) return;
+
+ for (int k = 0; k != (int)cs.size(); k++) {
+ Elf64_Rel* rel = (Elf64_Rel*)(cs[k].rel_data.data());
+ int n_rel = cs[k].rel_data.size() / sizeof(*rel);
+
+ for (int i = 0; i < n_rel; i++) {
+ int symIndex = ELF64_R_SYM(rel[i].r_info);
+ string symName;
+
+ ret = getSymNameByIdx(elfFile, symIndex, symName);
+ if (ret) return;
+
+ // Find the map fd and apply relo
+ for (int j = 0; j < (int)mapNames.size(); j++) {
+ if (!mapNames[j].compare(symName)) {
+ applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
+ break;
+ }
+ }
+ }
+ }
+}
+
+static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
+ const char* prefix, const unsigned int bpfloader_ver) {
+ unsigned kvers = kernelVersion();
+
+ if (!kvers) {
+ ALOGE("unable to get kernel version");
+ return -EINVAL;
+ }
+
+ string objName = pathToObjName(string(elfPath));
+
+ for (int i = 0; i < (int)cs.size(); i++) {
+ unique_fd& fd = cs[i].prog_fd;
+ int ret;
+ string name = cs[i].name;
+
+ if (!cs[i].prog_def.has_value()) {
+ ALOGE("[%d] '%s' missing program definition! bad bpf.o build?", i, name.c_str());
+ return -EINVAL;
+ }
+
+ unsigned min_kver = cs[i].prog_def->min_kver;
+ unsigned max_kver = cs[i].prog_def->max_kver;
+ ALOGD("cs[%d].name:%s min_kver:%x .max_kver:%x (kvers:%x)", i, name.c_str(), min_kver,
+ max_kver, kvers);
+ if (kvers < min_kver) continue;
+ if (kvers >= max_kver) continue;
+
+ unsigned bpfMinVer = cs[i].prog_def->bpfloader_min_ver;
+ unsigned bpfMaxVer = cs[i].prog_def->bpfloader_max_ver;
+ domain selinux_context = getDomainFromSelinuxContext(cs[i].prog_def->selinux_context);
+ domain pin_subdir = getDomainFromPinSubdir(cs[i].prog_def->pin_subdir);
+
+ ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
+ bpfMinVer, bpfMaxVer);
+ if (bpfloader_ver < bpfMinVer) continue;
+ if (bpfloader_ver >= bpfMaxVer) continue;
+
+ if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
+ (cs[i].prog_def->ignore_on_user && isUser()) ||
+ (cs[i].prog_def->ignore_on_userdebug && isUserdebug())) {
+ ALOGD("cs[%d].name:%s is ignored on %s builds", i, name.c_str(),
+ getBuildType().c_str());
+ continue;
+ }
+
+ if ((isArm() && isKernel32Bit() && cs[i].prog_def->ignore_on_arm32) ||
+ (isArm() && isKernel64Bit() && cs[i].prog_def->ignore_on_aarch64) ||
+ (isX86() && isKernel32Bit() && cs[i].prog_def->ignore_on_x86_32) ||
+ (isX86() && isKernel64Bit() && cs[i].prog_def->ignore_on_x86_64) ||
+ (isRiscV() && cs[i].prog_def->ignore_on_riscv64)) {
+ ALOGD("cs[%d].name:%s is ignored on %s", i, name.c_str(), describeArch());
+ continue;
+ }
+
+ if (specified(selinux_context)) {
+ ALOGV("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
+ cs[i].prog_def->selinux_context, static_cast<int>(selinux_context),
+ lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
+ }
+
+ if (specified(pin_subdir)) {
+ ALOGV("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
+ cs[i].prog_def->pin_subdir, static_cast<int>(pin_subdir),
+ lookupPinSubdir(pin_subdir));
+ }
+
+ // strip any potential $foo suffix
+ // this can be used to provide duplicate programs
+ // conditionally loaded based on running kernel version
+ name = name.substr(0, name.find_last_of('$'));
+
+ bool reuse = false;
+ // Format of pin location is
+ // /sys/fs/bpf/<prefix>prog_<objName>_<progName>
+ string progPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "prog_" +
+ objName + '_' + string(name);
+ if (access(progPinLoc.c_str(), F_OK) == 0) {
+ fd.reset(retrieveProgram(progPinLoc.c_str()));
+ ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
+ (!fd.ok() ? std::strerror(errno) : "no error"));
+ reuse = true;
+ } else {
+ static char log_buf[1 << 20]; // 1 MiB logging buffer
+
+ union bpf_attr req = {
+ .prog_type = cs[i].type,
+ .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
+ .insns = ptr_to_u64(cs[i].data.data()),
+ .license = ptr_to_u64(license.c_str()),
+ .log_level = 1,
+ .log_size = sizeof(log_buf),
+ .log_buf = ptr_to_u64(log_buf),
+ .kern_version = kvers,
+ .expected_attach_type = cs[i].attach_type,
+ };
+ if (isAtLeastKernelVersion(4, 15, 0))
+ strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
+ fd.reset(bpf(BPF_PROG_LOAD, req));
+
+ // Kernel should have NULL terminated the log buffer, but force it anyway for safety
+ log_buf[sizeof(log_buf) - 1] = 0;
+
+ // Strip out final newline if present
+ int log_chars = strlen(log_buf);
+ if (log_chars && log_buf[log_chars - 1] == '\n') log_buf[--log_chars] = 0;
+
+ bool log_oneline = !strchr(log_buf, '\n');
+
+ ALOGD("BPF_PROG_LOAD call for %s (%s) returned '%s' fd: %d (%s)", elfPath,
+ cs[i].name.c_str(), log_oneline ? log_buf : "{multiline}",
+ fd.get(), (!fd.ok() ? std::strerror(errno) : "ok"));
+
+ if (!fd.ok()) {
+ // kernel NULL terminates log_buf, so this checks for non-empty string
+ if (log_buf[0]) {
+ vector<string> lines = Split(log_buf, "\n");
+
+ ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
+ for (const auto& line : lines) ALOGW("%s", line.c_str());
+ ALOGW("BPF_PROG_LOAD - END log_buf contents.");
+ }
+
+ if (cs[i].prog_def->optional) {
+ ALOGW("failed program %s is marked optional - continuing...",
+ cs[i].name.c_str());
+ continue;
+ }
+ ALOGE("non-optional program %s failed to load.", cs[i].name.c_str());
+ }
+ }
+
+ if (!fd.ok()) return fd.get();
+
+ if (!reuse) {
+ if (specified(selinux_context)) {
+ string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+ "tmp_prog_" + objName + '_' + string(name);
+ ret = bpfFdPin(fd, createLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ ret = renameat2(AT_FDCWD, createLoc.c_str(),
+ AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
+ if (ret) {
+ int err = errno;
+ ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
+ err, strerror(err));
+ return -err;
+ }
+ } else {
+ ret = bpfFdPin(fd, progPinLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ }
+ if (chmod(progPinLoc.c_str(), 0440)) {
+ int err = errno;
+ ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
+ return -err;
+ }
+ if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
+ (gid_t)cs[i].prog_def->gid)) {
+ int err = errno;
+ ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
+ cs[i].prog_def->gid, err, strerror(err));
+ return -err;
+ }
+ }
+
+ int progId = bpfGetFdProgId(fd);
+ if (progId == -1) {
+ ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
+ } else {
+ ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
+ }
+ }
+
+ return 0;
+}
+
+int loadProg(const char* const elfPath, const unsigned int bpfloader_ver,
+ const char* const prefix) {
+ vector<char> license;
+ vector<codeSection> cs;
+ vector<unique_fd> mapFds;
+ int ret;
+
+ ifstream elfFile(elfPath, ios::in | ios::binary);
+ if (!elfFile.is_open()) return -1;
+
+ ret = readSectionByName("license", elfFile, license);
+ if (ret) {
+ ALOGE("Couldn't find license in %s", elfPath);
+ return ret;
+ } else {
+ ALOGD("Loading ELF object %s with license %s",
+ elfPath, (char*)license.data());
+ }
+
+ unsigned int bpfLoaderMinVer = readSectionUint("bpfloader_min_ver", elfFile);
+ unsigned int bpfLoaderMaxVer = readSectionUint("bpfloader_max_ver", elfFile);
+
+ // inclusive lower bound check
+ if (bpfloader_ver < bpfLoaderMinVer) {
+ ALOGD("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
+ bpfloader_ver, elfPath, bpfLoaderMinVer);
+ return 0;
+ }
+
+ // exclusive upper bound check
+ if (bpfloader_ver >= bpfLoaderMaxVer) {
+ ALOGD("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
+ bpfloader_ver, elfPath, bpfLoaderMaxVer);
+ return 0;
+ }
+
+ ALOGD("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
+ bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
+
+ ret = createMaps(elfPath, elfFile, mapFds, prefix, bpfloader_ver);
+ if (ret) {
+ ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
+ return ret;
+ }
+
+ for (int i = 0; i < (int)mapFds.size(); i++)
+ ALOGV("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
+
+ ret = readCodeSections(elfFile, cs);
+ // BPF .o's with no programs are only supported by mainline netbpfload,
+ // make sure .o's targeting non-mainline (ie. S) bpfloader don't show up.
+ if (ret == -ENOENT && bpfLoaderMinVer >= BPFLOADER_MAINLINE_VERSION)
+ return 0;
+ if (ret) {
+ ALOGE("Couldn't read all code sections in %s", elfPath);
+ return ret;
+ }
+
+ applyMapRelo(elfFile, mapFds, cs);
+
+ ret = loadCodeSections(elfPath, cs, string(license.data()), prefix, bpfloader_ver);
+ if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
+
+ return ret;
+}
+
+static bool exists(const char* const path) {
+ int v = access(path, F_OK);
+ if (!v) return true;
+ if (errno == ENOENT) return false;
+ ALOGE("FATAL: access(%s, F_OK) -> %d [%d:%s]", path, v, errno, strerror(errno));
+ abort(); // can only hit this if permissions (likely selinux) are screwed up
+}
+
+#define APEXROOT "/apex/com.android.tethering"
+#define BPFROOT APEXROOT "/etc/bpf"
+
+const Location locations[] = {
+ // S+ Tethering mainline module (network_stack): tether offload
+ {
+ .dir = BPFROOT "/",
+ .prefix = "tethering/",
+ },
+ // T+ Tethering mainline module (shared with netd & system server)
+ // netutils_wrapper (for iptables xt_bpf) has access to programs
+ {
+ .dir = BPFROOT "/netd_shared/",
+ .prefix = "netd_shared/",
+ },
+ // T+ Tethering mainline module (shared with netd & system server)
+ // netutils_wrapper has no access, netd has read only access
+ {
+ .dir = BPFROOT "/netd_readonly/",
+ .prefix = "netd_readonly/",
+ },
+ // T+ Tethering mainline module (shared with system server)
+ {
+ .dir = BPFROOT "/net_shared/",
+ .prefix = "net_shared/",
+ },
+ // T+ Tethering mainline module (not shared, just network_stack)
+ {
+ .dir = BPFROOT "/net_private/",
+ .prefix = "net_private/",
+ },
+};
+
+static int loadAllElfObjects(const unsigned int bpfloader_ver, const Location& location) {
+ int retVal = 0;
+ DIR* dir;
+ struct dirent* ent;
+
+ if ((dir = opendir(location.dir)) != NULL) {
+ while ((ent = readdir(dir)) != NULL) {
+ string s = ent->d_name;
+ if (!EndsWith(s, ".o")) continue;
+
+ string progPath(location.dir);
+ progPath += s;
+
+ int ret = loadProg(progPath.c_str(), bpfloader_ver, location.prefix);
+ if (ret) {
+ retVal = ret;
+ ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
+ } else {
+ ALOGD("Loaded object: %s", progPath.c_str());
+ }
+ }
+ closedir(dir);
+ }
+ return retVal;
+}
+
+static int createSysFsBpfSubDir(const char* const prefix) {
+ if (*prefix) {
+ mode_t prevUmask = umask(0);
+
+ string s = "/sys/fs/bpf/";
+ s += prefix;
+
+ errno = 0;
+ int ret = mkdir(s.c_str(), S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO);
+ if (ret && errno != EEXIST) {
+ const int err = errno;
+ ALOGE("Failed to create directory: %s, ret: %s", s.c_str(), std::strerror(err));
+ return -err;
+ }
+
+ umask(prevUmask);
+ }
+ return 0;
+}
+
+// Technically 'value' doesn't need to be newline terminated, but it's best
+// to include a newline to match 'echo "value" > /proc/sys/...foo' behaviour,
+// which is usually how kernel devs test the actual sysctl interfaces.
+static int writeProcSysFile(const char *filename, const char *value) {
+ unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
+ if (fd < 0) {
+ const int err = errno;
+ ALOGE("open('%s', O_WRONLY | O_CLOEXEC) -> %s", filename, strerror(err));
+ return -err;
+ }
+ int len = strlen(value);
+ int v = write(fd, value, len);
+ if (v < 0) {
+ const int err = errno;
+ ALOGE("write('%s', '%s', %d) -> %s", filename, value, len, strerror(err));
+ return -err;
+ }
+ if (v != len) {
+ // In practice, due to us only using this for /proc/sys/... files, this can't happen.
+ ALOGE("write('%s', '%s', %d) -> short write [%d]", filename, value, len, v);
+ return -EINVAL;
+ }
+ return 0;
+}
+
+#define APEX_MOUNT_POINT "/apex/com.android.tethering"
+const char * const platformBpfLoader = "/system/bin/bpfloader";
+const char *const uprobestatsBpfLoader =
+ "/apex/com.android.uprobestats/bin/uprobestatsbpfload";
+
+static int logTetheringApexVersion(void) {
+ char * found_blockdev = NULL;
+ FILE * f = NULL;
+ char buf[4096];
+
+ f = fopen("/proc/mounts", "re");
+ if (!f) return 1;
+
+ // /proc/mounts format: block_device [space] mount_point [space] other stuff... newline
+ while (fgets(buf, sizeof(buf), f)) {
+ char * blockdev = buf;
+ char * space = strchr(blockdev, ' ');
+ if (!space) continue;
+ *space = '\0';
+ char * mntpath = space + 1;
+ space = strchr(mntpath, ' ');
+ if (!space) continue;
+ *space = '\0';
+ if (strcmp(mntpath, APEX_MOUNT_POINT)) continue;
+ found_blockdev = strdup(blockdev);
+ break;
+ }
+ fclose(f);
+ f = NULL;
+
+ if (!found_blockdev) return 2;
+ ALOGV("Found Tethering Apex mounted from blockdev %s", found_blockdev);
+
+ f = fopen("/proc/mounts", "re");
+ if (!f) { free(found_blockdev); return 3; }
+
+ while (fgets(buf, sizeof(buf), f)) {
+ char * blockdev = buf;
+ char * space = strchr(blockdev, ' ');
+ if (!space) continue;
+ *space = '\0';
+ char * mntpath = space + 1;
+ space = strchr(mntpath, ' ');
+ if (!space) continue;
+ *space = '\0';
+ if (strcmp(blockdev, found_blockdev)) continue;
+ if (strncmp(mntpath, APEX_MOUNT_POINT "@", strlen(APEX_MOUNT_POINT "@"))) continue;
+ char * at = strchr(mntpath, '@');
+ if (!at) continue;
+ char * ver = at + 1;
+ ALOGI("Tethering APEX version %s", ver);
+ }
+ fclose(f);
+ free(found_blockdev);
+ return 0;
+}
+
+static bool hasGSM() {
+ static string ph = GetProperty("gsm.current.phone-type", "");
+ static bool gsm = (ph != "");
+ static bool logged = false;
+ if (!logged) {
+ logged = true;
+ ALOGI("hasGSM(gsm.current.phone-type='%s'): %s", ph.c_str(), gsm ? "true" : "false");
+ }
+ return gsm;
+}
+
+static bool isTV() {
+ if (hasGSM()) return false; // TVs don't do GSM
+
+ static string key = GetProperty("ro.oem.key1", "");
+ static bool tv = StartsWith(key, "ATV00");
+ static bool logged = false;
+ if (!logged) {
+ logged = true;
+ ALOGI("isTV(ro.oem.key1='%s'): %s.", key.c_str(), tv ? "true" : "false");
+ }
+ return tv;
+}
+
+static bool isWear() {
+ static string wearSdkStr = GetProperty("ro.cw_build.wear_sdk.version", "");
+ static int wearSdkInt = GetIntProperty("ro.cw_build.wear_sdk.version", 0);
+ static string buildChars = GetProperty("ro.build.characteristics", "");
+ static vector<string> v = Tokenize(buildChars, ",");
+ static bool watch = (std::find(v.begin(), v.end(), "watch") != v.end());
+ static bool wear = (wearSdkInt > 0) || watch;
+ static bool logged = false;
+ if (!logged) {
+ logged = true;
+ ALOGI("isWear(ro.cw_build.wear_sdk.version=%d[%s] ro.build.characteristics='%s'): %s",
+ wearSdkInt, wearSdkStr.c_str(), buildChars.c_str(), wear ? "true" : "false");
+ }
+ return wear;
+}
+
+static int doLoad(char** argv, char * const envp[]) {
+ const bool runningAsRoot = !getuid(); // true iff U QPR3 or V+
+
+ // Any released device will have codename REL instead of a 'real' codename.
+ // For safety: default to 'REL' so we default to unreleased=false on failure.
+ const bool unreleased = (GetProperty("ro.build.version.codename", "REL") != "REL");
+
+ // goog/main device_api_level is bumped *way* before aosp/main api level
+ // (the latter only gets bumped during the push of goog/main to aosp/main)
+ //
+ // Since we develop in AOSP, we want it to behave as if it was bumped too.
+ //
+ // Note that AOSP doesn't really have a good api level (for example during
+ // early V dev cycle, it would have *all* of T, some but not all of U, and some V).
+ // One could argue that for our purposes AOSP api level should be infinite or 10000.
+ //
+ // This could also cause api to be increased in goog/main or other branches,
+ // but I can't imagine a case where this would be a problem: the problem
+ // is rather a too low api level, rather than some ill defined high value.
+ // For example as I write this aosp is 34/U, and goog is 35/V,
+ // we want to treat both goog & aosp as 35/V, but it's harmless if we
+ // treat goog as 36 because that value isn't yet defined to mean anything,
+ // and we thus never compare against it.
+ //
+ // Also note that 'android_get_device_api_level()' is what the
+ // //system/core/init/apex_init_util.cpp
+ // apex init .XXrc parsing code uses for XX filtering.
+ //
+ // That code has a hack to bump <35 to 35 (to force aosp/main to parse .35rc),
+ // but could (should?) perhaps be adjusted to match this.
+ const int effective_api_level = android_get_device_api_level() + (int)unreleased;
+ const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
+ const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
+ const bool isAtLeastV = (effective_api_level >= __ANDROID_API_V__);
+ const bool isAtLeastW = (effective_api_level > __ANDROID_API_V__); // TODO: switch to W
+
+ const int first_api_level = GetIntProperty("ro.board.first_api_level", effective_api_level);
+
+ // last in U QPR2 beta1
+ const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
+ // first in U QPR2 beta~2
+ const bool has_platform_netbpfload_rc = exists("/system/etc/init/netbpfload.rc");
+
+ // Version of Network BpfLoader depends on the Android OS version
+ unsigned int bpfloader_ver = BPFLOADER_MAINLINE_VERSION; // [42u]
+ if (isAtLeastT) ++bpfloader_ver; // [43] BPFLOADER_MAINLINE_T_VERSION
+ if (isAtLeastU) ++bpfloader_ver; // [44] BPFLOADER_MAINLINE_U_VERSION
+ if (runningAsRoot) ++bpfloader_ver; // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
+ if (isAtLeastV) ++bpfloader_ver; // [46] BPFLOADER_MAINLINE_V_VERSION
+ if (isAtLeastW) ++bpfloader_ver; // [47] BPFLOADER_MAINLINE_W_VERSION
+
+ ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
+ bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
+ kernelVersion(), describeArch(), getuid(),
+ has_platform_bpfloader_rc, has_platform_netbpfload_rc);
+
+ if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
+ ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
+ return 1;
+ }
+
+ if (has_platform_bpfloader_rc && has_platform_netbpfload_rc) {
+ ALOGE("Platform has *both* bpfloader & netbpfload init scripts.");
+ return 1;
+ }
+
+ logTetheringApexVersion();
+
+ if (!isAtLeastT) {
+ ALOGE("Impossible - not reachable on Android <T.");
+ return 1;
+ }
+
+ // both S and T require kernel 4.9 (and eBpf support)
+ if (isAtLeastT && !isAtLeastKernelVersion(4, 9, 0)) {
+ ALOGE("Android T requires kernel 4.9.");
+ return 1;
+ }
+
+ // U bumps the kernel requirement up to 4.14
+ if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
+ ALOGE("Android U requires kernel 4.14.");
+ return 1;
+ }
+
+ // V bumps the kernel requirement up to 4.19
+ // see also: //system/netd/tests/kernel_test.cpp TestKernel419
+ if (isAtLeastV && !isAtLeastKernelVersion(4, 19, 0)) {
+ ALOGE("Android V requires kernel 4.19.");
+ return 1;
+ }
+
+ // Technically already required by U, but only enforce on V+
+ // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
+ if (isAtLeastV && isKernel32Bit() && isAtLeastKernelVersion(5, 16, 0)) {
+ ALOGE("Android V+ platform with 32 bit kernel version >= 5.16.0 is unsupported");
+ if (!isTV()) return 1;
+ }
+
+ // 6.6 is highest version supported by Android V, so this is effectively W+ (sdk=36+)
+ if (isKernel32Bit() && isAtLeastKernelVersion(6, 7, 0)) {
+ ALOGE("Android platform with 32 bit kernel version >= 6.7.0 is unsupported");
+ return 1;
+ }
+
+ // Various known ABI layout issues, particularly wrt. bpf and ipsec/xfrm.
+ if (isAtLeastV && isKernel32Bit() && isX86()) {
+ ALOGE("Android V requires X86 kernel to be 64-bit.");
+ if (!isTV()) return 1;
+ }
+
+ if (isAtLeastV) {
+ bool bad = false;
+
+ if (!isLtsKernel()) {
+ ALOGW("Android V only supports LTS kernels.");
+ bad = true;
+ }
+
+#define REQUIRE(maj, min, sub) \
+ if (isKernelVersion(maj, min) && !isAtLeastKernelVersion(maj, min, sub)) { \
+ ALOGW("Android V requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
+ bad = true; \
+ }
+
+ REQUIRE(4, 19, 236)
+ REQUIRE(5, 4, 186)
+ REQUIRE(5, 10, 199)
+ REQUIRE(5, 15, 136)
+ REQUIRE(6, 1, 57)
+ REQUIRE(6, 6, 0)
+
+#undef REQUIRE
+
+ if (bad) {
+ ALOGE("Unsupported kernel version (%07x).", kernelVersion());
+ }
+ }
+
+ /* Android 14/U should only launch on 64-bit kernels
+ * T launches on 5.10/5.15
+ * U launches on 5.15/6.1
+ * So >=5.16 implies isKernel64Bit()
+ *
+ * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
+ *
+ * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
+ * we also require 64-bit userspace.
+ *
+ * There are various known issues with 32-bit userspace talking to various
+ * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
+ * Some of these have userspace or kernel workarounds/hacks.
+ * Some of them don't...
+ * We're going to be removing the hacks.
+ * (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
+ * Note: this check/enforcement only applies to *system* userspace code,
+ * it does not affect unprivileged apps, the 32-on-64 compatibility
+ * problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
+ *
+ * Additionally the 32-bit kernel jit support is poor,
+ * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
+ */
+ if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
+ // Stuff won't work reliably, but...
+ if (isTV()) {
+ // exempt TVs... they don't really need functional advanced networking
+ ALOGW("[TV] 32-bit userspace unsupported on 6.2+ kernels.");
+ } else if (isWear() && isArm()) {
+ // exempt Arm Wear devices (arm32 ABI is far less problematic than x86-32)
+ ALOGW("[Arm Wear] 32-bit userspace unsupported on 6.2+ kernels.");
+ } else if (first_api_level <= __ANDROID_API_T__ && isArm()) {
+ // also exempt Arm devices upgrading with major kernel rev from T-
+ // might possibly be better for them to run with a newer kernel...
+ ALOGW("[Arm KernelUpRev] 32-bit userspace unsupported on 6.2+ kernels.");
+ } else if (isArm()) {
+ ALOGE("[Arm] 64-bit userspace required on 6.2+ kernels (%d).", first_api_level);
+ return 1;
+ } else { // x86 since RiscV cannot be 32-bit
+ ALOGE("[x86] 64-bit userspace required on 6.2+ kernels.");
+ return 1;
+ }
+ }
+
+ // Note: 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
+ if (isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
+ ALOGE("64-bit userspace required on 6.7+ kernels.");
+ return 1;
+ }
+
+ // Ensure we can determine the Android build type.
+ if (!isEng() && !isUser() && !isUserdebug()) {
+ ALOGE("Failed to determine the build type: got %s, want 'eng', 'user', or 'userdebug'",
+ getBuildType().c_str());
+ return 1;
+ }
+
+ if (runningAsRoot) {
+ // Note: writing this proc file requires being root (always the case on V+)
+
+ // Linux 5.16-rc1 changed the default to 2 (disabled but changeable),
+ // but we need 0 (enabled)
+ // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on
+ // pre-5.13, on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
+ if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
+ isAtLeastKernelVersion(5, 13, 0)) return 1;
+ }
+
+ if (isAtLeastU) {
+ // Note: writing these proc files requires CAP_NET_ADMIN
+ // and sepolicy which is only present on U+,
+ // on Android T and earlier versions they're written from the 'load_bpf_programs'
+ // trigger (ie. by init itself) instead.
+
+ // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
+ // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
+ // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+ // kernel does not have CONFIG_BPF_JIT=y)
+ // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
+ // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
+ if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
+
+ // Enable JIT kallsyms export for privileged users only
+ // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+ // kernel does not have CONFIG_HAVE_EBPF_JIT=y)
+ if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
+ }
+
+ // Create all the pin subdirectories
+ // (this must be done first to allow selinux_context and pin_subdir functionality,
+ // which could otherwise fail with ENOENT during object pinning or renaming,
+ // due to ordering issues)
+ for (const auto& location : locations) {
+ if (createSysFsBpfSubDir(location.prefix)) return 1;
+ }
+
+ // Note: there's no actual src dir for fs_bpf_loader .o's,
+ // so it is not listed in 'locations[].prefix'.
+ // This is because this is primarily meant for triggering genfscon rules,
+ // and as such this will likely always be the case.
+ // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
+ if (createSysFsBpfSubDir("loader")) return 1;
+
+ // Load all ELF objects, create programs and maps, and pin them
+ for (const auto& location : locations) {
+ if (loadAllElfObjects(bpfloader_ver, location) != 0) {
+ ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
+ ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
+ ALOGE("If this triggers randomly, you might be hitting some memory allocation "
+ "problems or startup script race.");
+ ALOGE("--- DO NOT EXPECT SYSTEM TO BOOT SUCCESSFULLY ---");
+ sleep(20);
+ return 2;
+ }
+ }
+
+ int key = 1;
+ int value = 123;
+ unique_fd map(
+ createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
+ if (writeToMapEntry(map, &key, &value, BPF_ANY)) {
+ ALOGE("Critical kernel bug - failure to write into index 1 of 2 element bpf map array.");
+ return 1;
+ }
+
+ // leave a flag that we're done
+ if (createSysFsBpfSubDir("netd_shared/mainline_done")) return 1;
+
+ // platform bpfloader will only succeed when run as root
+ if (!runningAsRoot) {
+ // unreachable on U QPR3+ which always runs netbpfload as root
+
+ ALOGI("mainline done, no need to transfer control to platform bpf loader.");
+ return 0;
+ }
+
+ // unreachable before U QPR3
+ {
+ ALOGI("done, transferring control to uprobestatsbpfload.");
+ const char *args[] = {
+ uprobestatsBpfLoader,
+ NULL,
+ };
+ execve(args[0], (char **)args, envp);
+ }
+
+ ALOGI("unable to execute uprobestatsbpfload, transferring control to "
+ "platform bpfloader.");
+ // platform BpfLoader *needs* to run as root
+ const char * args[] = { platformBpfLoader, NULL, };
+ execve(args[0], (char**)args, envp);
+ ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
+ return 1;
+}
+
+} // namespace bpf
+} // namespace android
+
+int main(int argc, char** argv, char * const envp[]) {
+ InitLogging(argv, &KernelLogger);
+
+ if (argc == 2 && !strcmp(argv[1], "done")) {
+ // we're being re-exec'ed from platform bpfloader to 'finalize' things
+ if (!SetProperty("bpf.progs_loaded", "1")) {
+ ALOGE("Failed to set bpf.progs_loaded property to 1.");
+ return 125;
+ }
+ ALOGI("success.");
+ return 0;
+ }
+
+ return android::bpf::doLoad(argv, envp);
+}
diff --git a/bpf/loader/initrc-doc/README.txt b/bpf/loader/initrc-doc/README.txt
new file mode 100644
index 0000000..2b22326
--- /dev/null
+++ b/bpf/loader/initrc-doc/README.txt
@@ -0,0 +1,116 @@
+This directory contains comment stripped versions of
+ //system/bpf/bpfloader/bpfloader.rc
+or
+ //packages/modules/Connectivity/bpf/loader/netbpfload.rc
+(as appropriate) from previous versions of Android.
+
+Generated via:
+ (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+ git cat-file -p remotes/aosp/android14-qpr2-release:netbpfload/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2-24Q1.rc
+ git cat-file -p remotes/aosp/android14-qpr3-release:netbpfload/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR3-24Q2.rc
+ git cat-file -p remotes/aosp/android15-release:netbpfload/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk35-15-V-24Q3.rc
+ git cat-file -p remotes/aosp/main:bpf/loader/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk35-15-V-QPR1-24Q4.rc
+
+see also:
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android11-release/bpfloader/bpfloader.rc
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android12-release/bpfloader/bpfloader.rc
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android13-release/bpfloader/bpfloader.rc
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-release/bpfloader/bpfloader.rc
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-qpr1-release/bpfloader/bpfloader.rc
+ https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-qpr2-release/bpfloader/ (rc file is gone in QPR2)
+ https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android14-qpr2-release/netbpfload/netbpfload.rc
+ https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android14-qpr3-release/netbpfload/netbpfload.rc
+ https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android15-release/netbpfload/netbpfload.rc
+ https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android15-qpr1-release/netbpfload/netbpfload.rc
+ https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/main/netbpfload/netbpfload.rc
+or:
+ https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q1-release/netbpfload/netbpfload.rc
+ https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q2-release/netbpfload/netbpfload.rc
+ https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q3-release/netbpfload/netbpfload.rc
+ https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q4-release/bpf/loader/netbpfload.rc
+
+this is entirely equivalent to:
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+
+it is also equivalent to:
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-v2-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+
+ie. there were no changes between R/S/T and R/S/T QPR3, and no change between U and U QPR1.
+
+Note: Sv2 sdk/api level is actually 32, it just didn't change anything wrt. bpf, so doesn't matter.
+
+
+Key takeaways:
+
+= R bpfloader (platform)
+ - CHOWN + SYS_ADMIN
+ - asynchronous startup
+ - platform only
+ - proc file setup handled by initrc
+
+= S bpfloader (platform)
+ - adds NET_ADMIN
+ - synchronous startup
+ - platform + mainline tethering offload
+
+= T bpfloader (platform)
+ - platform + mainline networking (including tethering offload)
+ - supported btf for maps via exec of btfloader
+
+= U bpfloader (platform)
+ - proc file setup moved into bpfloader binary
+ - explicitly specified user and groups:
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+
+= U QPR2 [24Q1] bpfloader (platform netbpfload -> platform bpfloader)
+ - drops support of btf for maps
+ - invocation of /system/bin/netbpfload binary, which after handling *all*
+ networking bpf related things executes the platform /system/bin/bpfloader
+ which handles non-networking bpf.
+ - Note: this does not (by itself) call into apex NetBpfLoad
+
+= U QPR3 [24Q2] bpfloader (platform netbpfload -> apex netbpfload -> platform bpfloader)
+ - platform NetBpfload *always* execs into apex NetBpfLoad,
+ - shipped with mainline tethering apex that includes NetBpfLoad binary.
+
+= V [24Q3] bpfloader (apex netbpfload -> platform bpfloader)
+ - no significant changes, though it does hard require the apex NetBpfLoad
+ by virtue of the platform NetBpfLoad no longer being present.
+ ie. the apex must override the platform 'bpfloader' service for 35+:
+ the V FRC M-2024-08+ tethering apex does this.
+
+= V QPR1 [24Q4] bpfloader (apex netbpfload -> platform bpfloader)
+ - made netd start earlier (previously happened in parallel to zygote)
+ - renamed and moved the trigger out of netbpload.rc into
+ //system/core/rootdir/init.rc
+ - the new sequence is:
+ trigger post-fs-data (logd available, starts apexd)
+ trigger load-bpf-programs (does: exec_start bpfloader)
+ trigger bpf-progs-loaded (does: start netd)
+ trigger zygote-start
+ - this is more or less irrelevant from the point of view of the bpfloader,
+ but it does mean netd init could fail and abort the boot earlier,
+ before 'A/B update_verifier marks a successful boot'.
+ Though note that due to netd being started asynchronously, it is racy.
+
+Note that there is now a copy of 'netbpfload' provided by the tethering apex
+mainline module at /apex/com.android.tethering/bin/netbpfload, which due
+to the use of execve("/system/bin/bpfloader") relies on T+ selinux which was
+added for btf map support (specifically the ability to exec the "btfloader").
+
+= mainline tethering apex M-2024-08+ overrides the platform service for V+
+ thus loading mainline (ie. networking) bpf programs from mainline 'NetBpfLoad'
+ and platform ones from platform 'bpfloader'.
+
+= mainline tethering apex M-2024-09+ changes T+ behaviour (U QPR3+ unaffected)
+ netd -> netd_updatable.so -> ctl.start=mdnsd_netbpfload -> load net bpf programs
diff --git a/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc b/bpf/loader/initrc-doc/bpfloader-sdk30-11-R.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk30-11-R.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc b/bpf/loader/initrc-doc/bpfloader-sdk31-12-S.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk31-12-S.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc b/bpf/loader/initrc-doc/bpfloader-sdk33-13-T.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk33-13-T.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2-24Q1.rc
similarity index 100%
copy from netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
copy to bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2-24Q1.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3-24Q2.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3-24Q2.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk34-14-U.rc
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-24Q3.rc b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-24Q3.rc
new file mode 100644
index 0000000..066cfc8
--- /dev/null
+++ b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-24Q3.rc
@@ -0,0 +1,8 @@
+on load_bpf_programs
+ exec_start bpfloader
+
+service bpfloader /system/bin/false
+ user root
+ oneshot
+ reboot_on_failure reboot,netbpfload-missing
+ updatable
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc
new file mode 100644
index 0000000..e2639ac
--- /dev/null
+++ b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc
@@ -0,0 +1,5 @@
+service bpfloader /system/bin/false
+ user root
+ oneshot
+ reboot_on_failure reboot,netbpfload-missing
+ updatable
diff --git a/netbpfload/netbpfload.33rc b/bpf/loader/netbpfload.33rc
similarity index 98%
rename from netbpfload/netbpfload.33rc
rename to bpf/loader/netbpfload.33rc
index 493731f..eb937dd 100644
--- a/netbpfload/netbpfload.33rc
+++ b/bpf/loader/netbpfload.33rc
@@ -18,4 +18,3 @@
rlimit memlock 1073741824 1073741824
oneshot
reboot_on_failure reboot,netbpfload-failed
- override
diff --git a/netbpfload/netbpfload.35rc b/bpf/loader/netbpfload.35rc
similarity index 100%
rename from netbpfload/netbpfload.35rc
rename to bpf/loader/netbpfload.35rc
diff --git a/netbpfload/netbpfload.rc b/bpf/loader/netbpfload.rc
similarity index 80%
rename from netbpfload/netbpfload.rc
rename to bpf/loader/netbpfload.rc
index e1af47f..10bfbb2 100644
--- a/netbpfload/netbpfload.rc
+++ b/bpf/loader/netbpfload.rc
@@ -1,22 +1,3 @@
-# zygote-start is what officially starts netd (see //system/core/rootdir/init.rc)
-# However, on some hardware it's started from post-fs-data as well, which is just
-# a tad earlier. There's no benefit to that though, since on 4.9+ P+ devices netd
-# will just block until bpfloader finishes and sets the bpf.progs_loaded property.
-#
-# It is important that we start bpfloader after:
-# - /sys/fs/bpf is already mounted,
-# - apex (incl. rollback) is initialized (so that in the future we can load bpf
-# programs shipped as part of apex mainline modules)
-# - logd is ready for us to log stuff
-#
-# At the same time we want to be as early as possible to reduce races and thus
-# failures (before memory is fragmented, and cpu is busy running tons of other
-# stuff) and we absolutely want to be before netd and the system boot slot is
-# considered to have booted successfully.
-#
-on load_bpf_programs
- exec_start bpfloader
-
# Note: This will actually execute /apex/com.android.tethering/bin/netbpfload
# by virtue of 'service bpfloader' being overridden by the apex shipped .rc
# Warning: most of the below settings are irrelevant unless the apex is missing.
diff --git a/netd/Android.bp b/bpf/netd/Android.bp
similarity index 98%
rename from netd/Android.bp
rename to bpf/netd/Android.bp
index fe4d999..473c8c9 100644
--- a/netd/Android.bp
+++ b/bpf/netd/Android.bp
@@ -82,7 +82,6 @@
"libcutils",
"liblog",
"libnetdutils",
- "libprocessgroup",
],
compile_multilib: "both",
multilib: {
diff --git a/netd/BpfBaseTest.cpp b/bpf/netd/BpfBaseTest.cpp
similarity index 90%
rename from netd/BpfBaseTest.cpp
rename to bpf/netd/BpfBaseTest.cpp
index c979a7b..4b8a04e 100644
--- a/netd/BpfBaseTest.cpp
+++ b/bpf/netd/BpfBaseTest.cpp
@@ -29,7 +29,6 @@
#include <gtest/gtest.h>
#include <cutils/qtaguid.h>
-#include <processgroup/processgroup.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
@@ -54,13 +53,6 @@
BpfBasicTest() {}
};
-TEST_F(BpfBasicTest, TestCgroupMounted) {
- std::string cg2_path;
- ASSERT_EQ(true, CgroupGetControllerPath(CGROUPV2_CONTROLLER_NAME, &cg2_path));
- ASSERT_EQ(0, access(cg2_path.c_str(), R_OK));
- ASSERT_EQ(0, access((cg2_path + "/cgroup.controllers").c_str(), R_OK));
-}
-
TEST_F(BpfBasicTest, TestTagSocket) {
BpfMap<uint64_t, UidTagValue> cookieTagMap(COOKIE_TAG_MAP_PATH);
ASSERT_TRUE(cookieTagMap.isValid());
diff --git a/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
similarity index 85%
rename from netd/BpfHandler.cpp
rename to bpf/netd/BpfHandler.cpp
index 9682545..340acda 100644
--- a/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -97,6 +97,7 @@
ALOGE("Failed to open the cgroup directory: %s", strerror(err));
return statusFromErrno(err, "Open the cgroup directory failed");
}
+
RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_ALLOWLIST_PROG_PATH));
RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_DENYLIST_PROG_PATH));
RETURN_IF_NOT_OK(checkProgramAccessible(XT_BPF_EGRESS_PROG_PATH));
@@ -114,19 +115,28 @@
cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
}
+ if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_RELEASE_PROG_PATH,
+ cg_fd, BPF_CGROUP_INET_SOCK_RELEASE));
+ }
+
if (modules::sdklevel::IsAtLeastV()) {
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT4_PROG_PATH,
- cg_fd, BPF_CGROUP_INET4_CONNECT));
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT6_PROG_PATH,
- cg_fd, BPF_CGROUP_INET6_CONNECT));
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_RECVMSG_PROG_PATH,
- cg_fd, BPF_CGROUP_UDP4_RECVMSG));
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_RECVMSG_PROG_PATH,
- cg_fd, BPF_CGROUP_UDP6_RECVMSG));
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_SENDMSG_PROG_PATH,
- cg_fd, BPF_CGROUP_UDP4_SENDMSG));
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_SENDMSG_PROG_PATH,
- cg_fd, BPF_CGROUP_UDP6_SENDMSG));
+ // V requires 4.19+, so technically this 2nd 'if' is not required, but it
+ // doesn't hurt us to try to support AOSP forks that try to support older kernels.
+ if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT4_PROG_PATH,
+ cg_fd, BPF_CGROUP_INET4_CONNECT));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT6_PROG_PATH,
+ cg_fd, BPF_CGROUP_INET6_CONNECT));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_RECVMSG_PROG_PATH,
+ cg_fd, BPF_CGROUP_UDP4_RECVMSG));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_RECVMSG_PROG_PATH,
+ cg_fd, BPF_CGROUP_UDP6_RECVMSG));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_SENDMSG_PROG_PATH,
+ cg_fd, BPF_CGROUP_UDP4_SENDMSG));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_SENDMSG_PROG_PATH,
+ cg_fd, BPF_CGROUP_UDP6_SENDMSG));
+ }
if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_GETSOCKOPT_PROG_PATH,
@@ -134,19 +144,12 @@
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SETSOCKOPT_PROG_PATH,
cg_fd, BPF_CGROUP_SETSOCKOPT));
}
-
- if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_RELEASE_PROG_PATH,
- cg_fd, BPF_CGROUP_INET_SOCK_RELEASE));
- }
}
if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
- RETURN_IF_NOT_OK(attachProgramToCgroup(
- "/sys/fs/bpf/netd_readonly/prog_block_bind4_block_port",
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND4_PROG_PATH,
cg_fd, BPF_CGROUP_INET4_BIND));
- RETURN_IF_NOT_OK(attachProgramToCgroup(
- "/sys/fs/bpf/netd_readonly/prog_block_bind6_block_port",
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND6_PROG_PATH,
cg_fd, BPF_CGROUP_INET6_BIND));
// This should trivially pass, since we just attached up above,
@@ -158,22 +161,26 @@
if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
}
+ if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
+ }
+
if (modules::sdklevel::IsAtLeastV()) {
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
+ // V requires 4.19+, so technically this 2nd 'if' is not required, but it
+ // doesn't hurt us to try to support AOSP forks that try to support older kernels.
+ if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
+ }
if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
if (bpf::queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
if (bpf::queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
}
-
- if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
- }
}
return netdutils::status::ok;
@@ -203,7 +210,7 @@
}
}
-Status BpfHandler::init(const char* cg2_path) {
+static inline void waitForBpf() {
// Note: netd *can* be restarted, so this might get called a second time after boot is complete
// at which point we don't need to (and shouldn't) wait for (more importantly start) loading bpf
@@ -231,6 +238,21 @@
}
ALOGI("BPF programs are loaded");
+}
+
+Status BpfHandler::init(const char* cg2_path) {
+ // This wait is effectively a no-op on U QPR3+ devices (as netd starts
+ // *after* the synchronous 'exec_start bpfloader' which calls NetBpfLoad)
+ // but checking for U QPR3 is hard.
+ //
+ // Waiting should not be required on U QPR3+ devices,
+ // ...
+ //
+ // ...unless someone changed 'exec_start bpfloader' to 'start bpfloader'
+ // in the rc file.
+ //
+ // TODO: should be: if (!modules::sdklevel::IsAtLeastW())
+ if (android_get_device_api_level() <= __ANDROID_API_V__) waitForBpf();
RETURN_IF_NOT_OK(initPrograms(cg2_path));
RETURN_IF_NOT_OK(initMaps());
diff --git a/netd/BpfHandler.h b/bpf/netd/BpfHandler.h
similarity index 100%
rename from netd/BpfHandler.h
rename to bpf/netd/BpfHandler.h
diff --git a/netd/BpfHandlerTest.cpp b/bpf/netd/BpfHandlerTest.cpp
similarity index 100%
rename from netd/BpfHandlerTest.cpp
rename to bpf/netd/BpfHandlerTest.cpp
diff --git a/netd/NetdUpdatable.cpp b/bpf/netd/NetdUpdatable.cpp
similarity index 100%
rename from netd/NetdUpdatable.cpp
rename to bpf/netd/NetdUpdatable.cpp
diff --git a/netd/include/NetdUpdatablePublic.h b/bpf/netd/include/NetdUpdatablePublic.h
similarity index 100%
rename from netd/include/NetdUpdatablePublic.h
rename to bpf/netd/include/NetdUpdatablePublic.h
diff --git a/netd/libnetd_updatable.map.txt b/bpf/netd/libnetd_updatable.map.txt
similarity index 100%
rename from netd/libnetd_updatable.map.txt
rename to bpf/netd/libnetd_updatable.map.txt
diff --git a/bpf_progs/Android.bp b/bpf/progs/Android.bp
similarity index 76%
rename from bpf_progs/Android.bp
rename to bpf/progs/Android.bp
index 1958aa8..20d194c 100644
--- a/bpf_progs/Android.bp
+++ b/bpf/progs/Android.bp
@@ -38,6 +38,7 @@
cflags: [
"-Wall",
"-Werror",
+ "-Wextra",
],
sdk_version: "30",
min_sdk_version: "30",
@@ -46,8 +47,8 @@
"com.android.tethering",
],
visibility: [
- "//packages/modules/Connectivity/DnsResolver",
- "//packages/modules/Connectivity/netd",
+ "//packages/modules/Connectivity/bpf/dns_helper",
+ "//packages/modules/Connectivity/bpf/netd",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service/native/libs/libclat",
"//packages/modules/Connectivity/Tethering",
@@ -63,75 +64,42 @@
// bpf kernel programs
//
bpf {
- name: "block.o",
- srcs: ["block.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- ],
- sub_dir: "net_shared",
-}
-
-bpf {
name: "dscpPolicy.o",
srcs: ["dscpPolicy.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- ],
sub_dir: "net_shared",
}
+// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
bpf {
name: "offload.o",
srcs: ["offload.c"],
- cflags: [
- "-Wall",
- "-Werror",
- ],
+ btf: false,
}
+// This version ships to Android T+ which uses mainline netbpfload.
bpf {
name: "offload@mainline.o",
srcs: ["offload@mainline.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- "-DMAINLINE",
- ],
+ cflags: ["-DMAINLINE"],
}
+// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
bpf {
name: "test.o",
srcs: ["test.c"],
- cflags: [
- "-Wall",
- "-Werror",
- ],
+ btf: false,
}
+// This version ships to Android T+ which uses mainline netbpfload.
bpf {
name: "test@mainline.o",
srcs: ["test@mainline.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- "-DMAINLINE",
- ],
+ cflags: ["-DMAINLINE"],
}
bpf {
name: "clatd.o",
srcs: ["clatd.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- ],
sub_dir: "net_shared",
}
@@ -139,11 +107,6 @@
// WARNING: Android T's non-updatable netd depends on 'netd' string for xt_bpf programs it loads
name: "netd.o",
srcs: ["netd.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- ],
// WARNING: Android T's non-updatable netd depends on 'netd_shared' string for xt_bpf programs
sub_dir: "netd_shared",
}
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf/progs/bpf_net_helpers.h
similarity index 60%
rename from bpf_progs/bpf_net_helpers.h
rename to bpf/progs/bpf_net_helpers.h
index 1511ee5..a5664ba 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf/progs/bpf_net_helpers.h
@@ -17,21 +17,72 @@
#pragma once
#include <linux/bpf.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
#include <linux/if_packet.h>
-#include <stdbool.h>
-#include <stdint.h>
-
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/pkt_cls.h>
+#include <linux/tcp.h>
// bionic kernel uapi linux/udp.h header is munged...
#define __kernel_udphdr udphdr
#include <linux/udp.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "bpf_helpers.h"
+
+// IP flags. (from kernel's include/net/ip.h)
+#define IP_CE 0x8000 // Flag: "Congestion" (really reserved 'evil bit')
+#define IP_DF 0x4000 // Flag: "Don't Fragment"
+#define IP_MF 0x2000 // Flag: "More Fragments"
+#define IP_OFFSET 0x1FFF // "Fragment Offset" part
+
+// IPv6 fragmentation header. (from kernel's include/net/ipv6.h)
+struct frag_hdr {
+ __u8 nexthdr;
+ __u8 reserved; // always zero
+ __be16 frag_off; // 13 bit offset, 2 bits zero, 1 bit "More Fragments"
+ __be32 identification;
+};
+
+// ----- Helper functions for offsets to fields -----
+
+// They all assume simple IP packets:
+// - no VLAN ethernet tags
+// - no IPv4 options (see IPV4_HLEN/TCP4_OFFSET/UDP4_OFFSET)
+// - no IPv6 extension headers
+// - no TCP options (see TCP_HLEN)
+
+//#define ETH_HLEN sizeof(struct ethhdr)
+#define IP4_HLEN sizeof(struct iphdr)
+#define IP6_HLEN sizeof(struct ipv6hdr)
+#define TCP_HLEN sizeof(struct tcphdr)
+#define UDP_HLEN sizeof(struct udphdr)
// Offsets from beginning of L4 (TCP/UDP) header
#define TCP_OFFSET(field) offsetof(struct tcphdr, field)
#define UDP_OFFSET(field) offsetof(struct udphdr, field)
-// Offsets from beginning of L3 (IPv4/IPv6) header
+// Offsets from beginning of L3 (IPv4) header
#define IP4_OFFSET(field) offsetof(struct iphdr, field)
+#define IP4_TCP_OFFSET(field) (IP4_HLEN + TCP_OFFSET(field))
+#define IP4_UDP_OFFSET(field) (IP4_HLEN + UDP_OFFSET(field))
+
+// Offsets from beginning of L3 (IPv6) header
#define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
+#define IP6_TCP_OFFSET(field) (IP6_HLEN + TCP_OFFSET(field))
+#define IP6_UDP_OFFSET(field) (IP6_HLEN + UDP_OFFSET(field))
+
+// Offsets from beginning of L2 (ie. Ethernet) header (which must be present)
+#define ETH_IP4_OFFSET(field) (ETH_HLEN + IP4_OFFSET(field))
+#define ETH_IP4_TCP_OFFSET(field) (ETH_HLEN + IP4_TCP_OFFSET(field))
+#define ETH_IP4_UDP_OFFSET(field) (ETH_HLEN + IP4_UDP_OFFSET(field))
+#define ETH_IP6_OFFSET(field) (ETH_HLEN + IP6_OFFSET(field))
+#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
+#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
// this returns 0 iff skb->sk is NULL
static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
@@ -83,11 +134,29 @@
// try to make the first 'len' header bytes readable/writable via direct packet access
// (note: AFAIK there is no way to ask for only direct packet read without also getting write)
-static inline __always_inline void try_make_writable(struct __sk_buff* skb, int len) {
+static inline __always_inline void try_make_writable(struct __sk_buff* skb, unsigned len) {
if (len > skb->len) len = skb->len;
if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
}
+// anti-compiler-optimizer no-op: explicitly force full calculation of 'v'
+//
+// The use for this is to force full calculation of a complex arithmetic (likely binary
+// bitops) value, and then check the result only once (thus likely reducing the number
+// of required conditional jump instructions that badly affect bpf verifier runtime)
+//
+// The compiler cannot look into the assembly statement, so it doesn't know it does nothing.
+// Since the statement takes 'v' as both input and output in a register (+r),
+// the compiler must fully calculate the precise value of 'v' before this,
+// and must use the (possibly modified) value of 'v' afterwards (thus cannot
+// do funky optimizations to use partial results from before the asm).
+//
+// As this is not flagged 'volatile' this may still be moved out of a loop,
+// or even entirely optimized out if 'v' is never used afterwards.
+//
+// See: https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
+#define COMPILER_FORCE_CALCULATION(v) asm ("" : "+r" (v))
+
struct egress_bool { bool egress; };
#define INGRESS ((struct egress_bool){ .egress = false })
#define EGRESS ((struct egress_bool){ .egress = true })
@@ -103,3 +172,10 @@
struct updatetime_bool { bool updatetime; };
#define NO_UPDATETIME ((struct updatetime_bool){ .updatetime = false })
#define UPDATETIME ((struct updatetime_bool){ .updatetime = true })
+
+// Return value for xt_bpf (netfilter match extension) programs
+static const int XTBPF_NOMATCH = 0;
+static const int XTBPF_MATCH = 1;
+
+static const int BPF_DISALLOW = 0;
+static const int BPF_ALLOW = 1;
diff --git a/bpf_progs/clat_mark.h b/bpf/progs/clat_mark.h
similarity index 100%
rename from bpf_progs/clat_mark.h
rename to bpf/progs/clat_mark.h
diff --git a/bpf_progs/clatd.c b/bpf/progs/clatd.c
similarity index 93%
rename from bpf_progs/clatd.c
rename to bpf/progs/clatd.c
index f83e5ae..2d4551e 100644
--- a/bpf_progs/clatd.c
+++ b/bpf/progs/clatd.c
@@ -14,44 +14,13 @@
* limitations under the License.
*/
-#include <linux/bpf.h>
-#include <linux/if.h>
-#include <linux/if_ether.h>
-#include <linux/in.h>
-#include <linux/in6.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/swab.h>
-#include <stdbool.h>
-#include <stdint.h>
-
-// bionic kernel uapi linux/udp.h header is munged...
-#define __kernel_udphdr udphdr
-#include <linux/udp.h>
-
// The resulting .o needs to load on Android T+
#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
#include "clatd.h"
#include "clat_mark.h"
-// IP flags. (from kernel's include/net/ip.h)
-#define IP_CE 0x8000 // Flag: "Congestion" (really reserved 'evil bit')
-#define IP_DF 0x4000 // Flag: "Don't Fragment"
-#define IP_MF 0x2000 // Flag: "More Fragments"
-#define IP_OFFSET 0x1FFF // "Fragment Offset" part
-
-// from kernel's include/net/ipv6.h
-struct frag_hdr {
- __u8 nexthdr;
- __u8 reserved; // always zero
- __be16 frag_off; // 13 bit offset, 2 bits zero, 1 bit "More Fragments"
- __be32 identification;
-};
-
DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
static inline __always_inline int nat64(struct __sk_buff* skb,
@@ -177,7 +146,7 @@
// Calculate the IPv4 one's complement checksum of the IPv4 header.
__wsum sum4 = 0;
- for (int i = 0; i < sizeof(ip) / sizeof(__u16); ++i) {
+ for (unsigned i = 0; i < sizeof(ip) / sizeof(__u16); ++i) {
sum4 += ((__u16*)&ip)[i];
}
// Note that sum4 is guaranteed to be non-zero by virtue of ip.version == 4
@@ -188,7 +157,7 @@
// Calculate the *negative* IPv6 16-bit one's complement checksum of the IPv6 header.
__wsum sum6 = 0;
// We'll end up with a non-zero sum due to ip6->version == 6 (which has '0' bits)
- for (int i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) {
+ for (unsigned i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) {
sum6 += ~((__u16*)ip6)[i]; // note the bitwise negation
}
@@ -321,7 +290,7 @@
// Calculate the IPv4 one's complement checksum of the IPv4 header.
__wsum sum4 = 0;
- for (int i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
+ for (unsigned i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
sum4 += ((__u16*)ip4)[i];
}
// Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
@@ -387,7 +356,7 @@
// Calculate the IPv6 16-bit one's complement checksum of the IPv6 header.
__wsum sum6 = 0;
// We'll end up with a non-zero sum due to ip6.version == 6
- for (int i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) {
+ for (unsigned i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) {
sum6 += ((__u16*)&ip6)[i];
}
@@ -430,4 +399,3 @@
LICENSE("Apache 2.0");
CRITICAL("Connectivity");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/clatd.h b/bpf/progs/clatd.h
similarity index 100%
rename from bpf_progs/clatd.h
rename to bpf/progs/clatd.h
diff --git a/bpf/progs/dscpPolicy.c b/bpf/progs/dscpPolicy.c
new file mode 100644
index 0000000..94d717b
--- /dev/null
+++ b/bpf/progs/dscpPolicy.c
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+
+#include "bpf_net_helpers.h"
+#include "dscpPolicy.h"
+
+#define ECN_MASK 3
+#define UPDATE_TOS(dscp, tos) ((dscp) << 2) | ((tos) & ECN_MASK)
+
+// The cache is never read nor written by userspace and is indexed by socket cookie % CACHE_MAP_SIZE
+#define CACHE_MAP_SIZE 32 // should be a power of two so we can % cheaply
+DEFINE_BPF_MAP_KERNEL_INTERNAL(socket_policy_cache_map, PERCPU_ARRAY, uint32_t, RuleEntry,
+ CACHE_MAP_SIZE)
+
+DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+
+static inline __always_inline uint64_t calculate_u64(uint64_t v) {
+ COMPILER_FORCE_CALCULATION(v);
+ return v;
+}
+
+static inline __always_inline void match_policy(struct __sk_buff* skb, const bool ipv4) {
+ void* data = (void*)(long)skb->data;
+ const void* data_end = (void*)(long)skb->data_end;
+
+ const int l2_header_size = sizeof(struct ethhdr);
+ struct ethhdr* eth = data;
+
+ if (data + l2_header_size > data_end) return;
+
+ int hdr_size = 0;
+
+ // used for map lookup
+ uint64_t cookie = bpf_get_socket_cookie(skb);
+ if (!cookie) return;
+
+ uint32_t cacheid = cookie % CACHE_MAP_SIZE;
+
+ __be16 sport = 0;
+ uint16_t dport = 0;
+ uint8_t protocol = 0; // TODO: Use are reserved value? Or int (-1) and cast to uint below?
+ struct in6_addr src_ip = {};
+ struct in6_addr dst_ip = {};
+ uint8_t tos = 0; // Only used for IPv4
+ __be32 old_first_be32 = 0; // Only used for IPv6
+ if (ipv4) {
+ const struct iphdr* const iph = (void*)(eth + 1);
+ hdr_size = l2_header_size + sizeof(struct iphdr);
+ // Must have ipv4 header
+ if (data + hdr_size > data_end) return;
+
+ // IP version must be 4
+ if (iph->version != 4) return;
+
+ // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+ if (iph->ihl != 5) return;
+
+ // V4 mapped address in in6_addr sets 10/11 position to 0xff.
+ src_ip.s6_addr32[2] = htonl(0x0000ffff);
+ dst_ip.s6_addr32[2] = htonl(0x0000ffff);
+
+ // Copy IPv4 address into in6_addr for easy comparison below.
+ src_ip.s6_addr32[3] = iph->saddr;
+ dst_ip.s6_addr32[3] = iph->daddr;
+ protocol = iph->protocol;
+ tos = iph->tos;
+ } else {
+ struct ipv6hdr* ip6h = (void*)(eth + 1);
+ hdr_size = l2_header_size + sizeof(struct ipv6hdr);
+ // Must have ipv6 header
+ if (data + hdr_size > data_end) return;
+
+ if (ip6h->version != 6) return;
+
+ src_ip = ip6h->saddr;
+ dst_ip = ip6h->daddr;
+ protocol = ip6h->nexthdr;
+ old_first_be32 = *(__be32*)ip6h;
+ }
+
+ switch (protocol) {
+ case IPPROTO_UDP:
+ case IPPROTO_UDPLITE: {
+ struct udphdr* udp;
+ udp = data + hdr_size;
+ if ((void*)(udp + 1) > data_end) return;
+ sport = udp->source;
+ dport = ntohs(udp->dest);
+ } break;
+ case IPPROTO_TCP: {
+ struct tcphdr* tcp;
+ tcp = data + hdr_size;
+ if ((void*)(tcp + 1) > data_end) return;
+ sport = tcp->source;
+ dport = ntohs(tcp->dest);
+ } break;
+ default:
+ return;
+ }
+
+ // this array lookup cannot actually fail
+ RuleEntry* existing_rule = bpf_socket_policy_cache_map_lookup_elem(&cacheid);
+
+ if (!existing_rule) return; // impossible
+
+ uint64_t nomatch = 0;
+ nomatch |= v6_not_equal(src_ip, existing_rule->src_ip);
+ nomatch |= v6_not_equal(dst_ip, existing_rule->dst_ip);
+ nomatch |= (skb->ifindex ^ existing_rule->ifindex);
+ nomatch |= (sport ^ existing_rule->src_port);
+ nomatch |= (dport ^ existing_rule->dst_port);
+ nomatch |= (protocol ^ existing_rule->proto);
+ COMPILER_FORCE_CALCULATION(nomatch);
+
+ /*
+ * After the above funky bitwise arithmetic we have 'nomatch == 0' iff
+ * src_ip == existing_rule->src_ip &&
+ * dst_ip == existing_rule->dst_ip &&
+ * skb->ifindex == existing_rule->ifindex &&
+ * sport == existing_rule->src_port &&
+ * dport == existing_rule->dst_port &&
+ * protocol == existing_rule->proto
+ */
+
+ if (!nomatch) {
+ if (existing_rule->dscp_val < 0) return; // cached no-op
+
+ if (ipv4) {
+ uint8_t newTos = UPDATE_TOS(existing_rule->dscp_val, tos);
+ bpf_l3_csum_replace(skb, l2_header_size + IP4_OFFSET(check), htons(tos), htons(newTos),
+ sizeof(uint16_t));
+ bpf_skb_store_bytes(skb, l2_header_size + IP4_OFFSET(tos), &newTos, sizeof(newTos), 0);
+ } else {
+ __be32 new_first_be32 =
+ htonl(ntohl(old_first_be32) & 0xF03FFFFF | (existing_rule->dscp_val << 22));
+ bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
+ BPF_F_RECOMPUTE_CSUM);
+ }
+ return; // cached DSCP mutation
+ }
+
+ // Linear scan ipv?_dscp_policies_map since stored params didn't match skb.
+ uint64_t best_score = 0;
+ int8_t new_dscp = -1; // meaning no mutation
+
+ for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
+ // Using a uint64 in for loop prevents infinite loop during BPF load,
+ // but the key is uint32, so convert back.
+ uint32_t key = i;
+
+ DscpPolicy* policy;
+ if (ipv4) {
+ policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+ } else {
+ policy = bpf_ipv6_dscp_policies_map_lookup_elem(&key);
+ }
+
+ // Lookup failure cannot happen on an array with MAX_POLICIES entries.
+ // While 'continue' would make logical sense here, 'return' should be
+ // easier for the verifier to analyze.
+ if (!policy) return;
+
+ // Think of 'nomatch' as a 64-bit boolean: false iff zero, true iff non-zero.
+ // Start off with nomatch being false, ie. we assume things *are* matching.
+ uint64_t nomatch = 0;
+
+ // Due to 'a ^ b' being 0 iff a == b:
+ // nomatch |= a ^ b
+ // should/can be read as:
+ // nomatch ||= (a != b)
+ // which you can also think of as:
+ // match &&= (a == b)
+
+ // If policy iface index does not match skb, then skip to next policy.
+ nomatch |= (policy->ifindex ^ skb->ifindex);
+
+ // policy->match_* are normal booleans, and should thus always be 0 or 1,
+ // thus you can think of these as:
+ // if (policy->match_foo) match &&= (foo == policy->foo);
+ nomatch |= policy->match_proto * (protocol ^ policy->proto);
+ nomatch |= policy->match_src_ip * v6_not_equal(src_ip, policy->src_ip);
+ nomatch |= policy->match_dst_ip * v6_not_equal(dst_ip, policy->dst_ip);
+ nomatch |= policy->match_src_port * (sport ^ policy->src_port);
+
+ // Since these values are u16s (<=63 bits), we can rely on u64 subtraction
+ // underflow setting the topmost bit. Basically, you can think of:
+ // nomatch |= (a - b) >> 63
+ // as:
+ // match &&= (a >= b)
+ uint64_t dport64 = dport; // Note: dst_port_{start_end} range is inclusive of both ends.
+ nomatch |= calculate_u64(dport64 - policy->dst_port_start) >> 63;
+ nomatch |= calculate_u64(policy->dst_port_end - dport64) >> 63;
+
+ // score is 0x10000 for each matched field (proto, src_ip, dst_ip, src_port)
+ // plus 1..0x10000 for the dst_port range match (smaller for bigger ranges)
+ uint64_t score = 0;
+ score += policy->match_proto; // reminder: match_* are boolean, thus 0 or 1
+ score += policy->match_src_ip;
+ score += policy->match_dst_ip;
+ score += policy->match_src_port;
+ score += 1; // for a 1 element dst_port_{start,end} range
+ score <<= 16; // scale up: ie. *= 0x10000
+ // now reduce score if the dst_port range is more than a single element
+ // we want to prioritize (ie. better score) matches of smaller ranges
+ score -= (policy->dst_port_end - policy->dst_port_start); // -= 0..0xFFFF
+
+ // Here we need:
+ // match &&= (score > best_score)
+ // which is the same as
+ // match &&= (score >= best_score + 1)
+ // > not >= because we want equal score matches to prefer choosing earlier policies
+ nomatch |= calculate_u64(score - best_score - 1) >> 63;
+
+ COMPILER_FORCE_CALCULATION(nomatch);
+ if (nomatch) continue;
+
+ // only reachable if we matched the policy and (score > best_score)
+ best_score = score;
+ new_dscp = policy->dscp_val;
+ }
+
+ // Update cache with found policy.
+ *existing_rule = (RuleEntry){
+ .src_ip = src_ip,
+ .dst_ip = dst_ip,
+ .ifindex = skb->ifindex,
+ .src_port = sport,
+ .dst_port = dport,
+ .proto = protocol,
+ .dscp_val = new_dscp,
+ };
+
+ if (new_dscp < 0) return;
+
+ // Need to store bytes after updating map or program will not load.
+ if (ipv4) {
+ uint8_t new_tos = UPDATE_TOS(new_dscp, tos);
+ bpf_l3_csum_replace(skb, l2_header_size + IP4_OFFSET(check), htons(tos), htons(new_tos), 2);
+ bpf_skb_store_bytes(skb, l2_header_size + IP4_OFFSET(tos), &new_tos, sizeof(new_tos), 0);
+ } else {
+ __be32 new_first_be32 = htonl(ntohl(old_first_be32) & 0xF03FFFFF | (new_dscp << 22));
+ bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
+ BPF_F_RECOMPUTE_CSUM);
+ }
+ return;
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM, schedcls_set_dscp_ether,
+ KVER_5_15)
+(struct __sk_buff* skb) {
+ if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
+
+ if (skb->protocol == htons(ETH_P_IP)) {
+ match_policy(skb, true);
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ match_policy(skb, false);
+ }
+
+ // Always return TC_ACT_PIPE
+ return TC_ACT_PIPE;
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("Connectivity");
diff --git a/bpf_progs/dscpPolicy.h b/bpf/progs/dscpPolicy.h
similarity index 60%
rename from bpf_progs/dscpPolicy.h
rename to bpf/progs/dscpPolicy.h
index e565966..413fb0f 100644
--- a/bpf_progs/dscpPolicy.h
+++ b/bpf/progs/dscpPolicy.h
@@ -14,14 +14,8 @@
* limitations under the License.
*/
-#define CACHE_MAP_SIZE 1024
#define MAX_POLICIES 16
-#define SRC_IP_MASK_FLAG 1
-#define DST_IP_MASK_FLAG 2
-#define SRC_PORT_MASK_FLAG 4
-#define PROTO_MASK_FLAG 8
-
#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
// Retrieve the first (ie. high) 64 bits of an IPv6 address (in network order)
@@ -34,20 +28,6 @@
#define v6_not_equal(a, b) ((v6_hi_be64(a) ^ v6_hi_be64(b)) \
| (v6_lo_be64(a) ^ v6_lo_be64(b)))
-// Returns 'a == b' as boolean
-#define v6_equal(a, b) (!v6_not_equal((a), (b)))
-
-// TODO: these are already defined in packages/modules/Connectivity/bpf_progs/bpf_net_helpers.h.
-// smove to common location in future.
-static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
- (void*)BPF_FUNC_get_socket_cookie;
-static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
- __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
-static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
- __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
-static long (*bpf_skb_ecn_set_ce)(struct __sk_buff* skb) =
- (void*)BPF_FUNC_skb_ecn_set_ce;
-
typedef struct {
struct in6_addr src_ip;
struct in6_addr dst_ip;
@@ -57,10 +37,12 @@
uint16_t dst_port_end;
uint8_t proto;
int8_t dscp_val; // -1 none, or 0..63 DSCP value
- uint8_t present_fields;
- uint8_t pad[3];
+ bool match_src_ip;
+ bool match_dst_ip;
+ bool match_src_port;
+ bool match_proto;
} DscpPolicy;
-STRUCT_SIZE(DscpPolicy, 2 * 16 + 4 + 3 * 2 + 3 * 1 + 3); // 48
+STRUCT_SIZE(DscpPolicy, 2 * 16 + 4 + 3 * 2 + 6 * 1); // 48
typedef struct {
struct in6_addr src_ip;
@@ -72,4 +54,4 @@
int8_t dscp_val; // -1 none, or 0..63 DSCP value
uint8_t pad[2];
} RuleEntry;
-STRUCT_SIZE(RuleEntry, 2 * 16 + 1 * 4 + 2 * 2 + 2 * 1 + 2); // 44
+STRUCT_SIZE(RuleEntry, 2 * 16 + 4 + 2 * 2 + 4 * 1); // 44
diff --git a/bpf_progs/netd.c b/bpf/progs/netd.c
similarity index 92%
rename from bpf_progs/netd.c
rename to bpf/progs/netd.c
index b3cde45..ed0eed5 100644
--- a/bpf_progs/netd.c
+++ b/bpf/progs/netd.c
@@ -17,19 +17,6 @@
// The resulting .o needs to load on Android T+
#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-#include <bpf_helpers.h>
-#include <linux/bpf.h>
-#include <linux/if.h>
-#include <linux/if_ether.h>
-#include <linux/if_packet.h>
-#include <linux/in.h>
-#include <linux/in6.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-#include <stdbool.h>
-#include <stdint.h>
#include "bpf_net_helpers.h"
#include "netd.h"
@@ -38,10 +25,6 @@
static const int PASS = 1;
static const int DROP_UNLESS_DNS = 2; // internal to our program
-// This is used for xt_bpf program only.
-static const int BPF_NOMATCH = 0;
-static const int BPF_MATCH = 1;
-
// Used for 'bool enable_tracing'
static const bool TRACE_ON = true;
static const bool TRACE_OFF = false;
@@ -86,6 +69,8 @@
// TODO: consider whether we can merge some of these maps
// for example it might be possible to merge 2 or 3 of:
// uid_counterset_map + uid_owner_map + uid_permission_map
+DEFINE_BPF_MAP_NO_NETD(blocked_ports_map, ARRAY, int, uint64_t,
+ 1024 /* 64K ports -> 1024 u64s */)
DEFINE_BPF_MAP_RW_NETD(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE)
DEFINE_BPF_MAP_NO_NETD(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE)
DEFINE_BPF_MAP_NO_NETD(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE)
@@ -184,7 +169,7 @@
static __always_inline inline void update_##the_stats_map(const struct __sk_buff* const skb, \
const TypeOfKey* const key, \
const struct egress_bool egress, \
- const struct kver_uint kver) { \
+ __unused const struct kver_uint kver) { \
StatsValue* value = bpf_##the_stats_map##_lookup_elem(key); \
if (!value) { \
StatsValue newValue = {}; \
@@ -524,22 +509,12 @@
return match;
}
-// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
-DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace_user", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_trace_user, KVER_5_8, KVER_INF,
- BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
- "fs_bpf_netd_readonly", "",
- IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
-(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
-}
-
-// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
+// Tracing on Android U+ 5.8+
DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
"fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
}
@@ -556,22 +531,12 @@
return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
}
-// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
-DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace_user", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
- BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
- "fs_bpf_netd_readonly", "",
- IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
-(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
-}
-
-// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
+// Tracing on Android U+ 5.8+
DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
"fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
@@ -599,12 +564,12 @@
if (sock_uid == AID_SYSTEM) {
uint64_t cookie = bpf_get_socket_cookie(skb);
UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
- if (utag && utag->uid == AID_CLAT) return BPF_NOMATCH;
+ if (utag && utag->uid == AID_CLAT) return XTBPF_NOMATCH;
}
uint32_t key = skb->ifindex;
update_iface_stats_map(skb, &key, EGRESS, KVER_NONE);
- return BPF_MATCH;
+ return XTBPF_MATCH;
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
@@ -617,7 +582,7 @@
uint32_t key = skb->ifindex;
update_iface_stats_map(skb, &key, INGRESS, KVER_NONE);
- return BPF_MATCH;
+ return XTBPF_MATCH;
}
DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
@@ -635,7 +600,7 @@
DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
- if (is_system_uid(sock_uid)) return BPF_MATCH;
+ if (is_system_uid(sock_uid)) return XTBPF_MATCH;
// kernel's DEFAULT_OVERFLOWUID is 65534, this is the overflow 'nobody' uid,
// usually this being returned means that skb->sk is NULL during RX
@@ -643,11 +608,11 @@
// packets to an unconnected udp socket.
// But it can also happen for egress from a timewait socket.
// Let's treat such cases as 'root' which is_system_uid()
- if (sock_uid == 65534) return BPF_MATCH;
+ if (sock_uid == 65534) return XTBPF_MATCH;
UidOwnerValue* allowlistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
- if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
- return BPF_NOMATCH;
+ if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? XTBPF_MATCH : XTBPF_NOMATCH;
+ return XTBPF_NOMATCH;
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
@@ -656,8 +621,8 @@
uint32_t sock_uid = bpf_get_socket_uid(skb);
UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
uint32_t penalty_box = PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH;
- if (denylistMatch) return denylistMatch->rule & penalty_box ? BPF_MATCH : BPF_NOMATCH;
- return BPF_NOMATCH;
+ if (denylistMatch) return denylistMatch->rule & penalty_box ? XTBPF_MATCH : XTBPF_NOMATCH;
+ return XTBPF_NOMATCH;
}
static __always_inline inline uint8_t get_app_permissions() {
@@ -676,13 +641,12 @@
DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", AID_ROOT, AID_ROOT, inet_socket_create,
KVER_4_14)
-(struct bpf_sock* sk) {
- // A return value of 1 means allow, everything else means deny.
- return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? 1 : 0;
+(__unused struct bpf_sock* sk) {
+ return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? BPF_ALLOW : BPF_DISALLOW;
}
-DEFINE_NETD_V_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
- inet_socket_release, KVER_5_10)
+DEFINE_NETD_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
+ inet_socket_release, KVER_5_10)
(struct bpf_sock* sk) {
uint64_t cookie = bpf_get_sk_cookie(sk);
if (cookie) bpf_cookie_tag_map_delete_elem(&cookie);
@@ -690,7 +654,7 @@
return 1;
}
-static __always_inline inline int check_localhost(struct bpf_sock_addr *ctx) {
+static __always_inline inline int check_localhost(__unused struct bpf_sock_addr *ctx) {
// See include/uapi/linux/bpf.h:
//
// struct bpf_sock_addr {
@@ -705,35 +669,72 @@
// __u32 msg_src_ip6[4]; // BE, R: 1,2,4,8-byte, W: 4,8-byte
// __bpf_md_ptr(struct bpf_sock *, sk);
// };
- return 1;
+ return BPF_ALLOW;
}
-DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_14)
+static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
+ if (!ctx->user_port) return BPF_ALLOW;
+
+ switch (ctx->protocol) {
+ case IPPROTO_TCP:
+ case IPPROTO_MPTCP:
+ case IPPROTO_UDP:
+ case IPPROTO_UDPLITE:
+ case IPPROTO_DCCP:
+ case IPPROTO_SCTP:
+ break;
+ default:
+ return BPF_ALLOW; // unknown protocols are allowed
+ }
+
+ int key = ctx->user_port >> 6;
+ int shift = ctx->user_port & 63;
+
+ uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
+ // Lookup should never fail in reality, but if it does return here to keep the
+ // BPF verifier happy.
+ if (!val) return BPF_ALLOW;
+
+ if ((*val >> shift) & 1) return BPF_DISALLOW;
+ return BPF_ALLOW;
+}
+
+DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", AID_ROOT, AID_ROOT, inet4_bind, KVER_4_19)
+(struct bpf_sock_addr *ctx) {
+ return block_port(ctx);
+}
+
+DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", AID_ROOT, AID_ROOT, inet6_bind, KVER_4_19)
+(struct bpf_sock_addr *ctx) {
+ return block_port(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", AID_ROOT, AID_ROOT, inet6_connect, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", AID_ROOT, AID_ROOT, inet6_connect, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", AID_ROOT, AID_ROOT, udp4_recvmsg, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", AID_ROOT, AID_ROOT, udp4_recvmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", AID_ROOT, AID_ROOT, udp6_recvmsg, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", AID_ROOT, AID_ROOT, udp6_recvmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", AID_ROOT, AID_ROOT, udp4_sendmsg, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", AID_ROOT, AID_ROOT, udp4_sendmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", AID_ROOT, AID_ROOT, udp6_sendmsg, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", AID_ROOT, AID_ROOT, udp6_sendmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
@@ -743,7 +744,7 @@
// Tell kernel to return 'original' kernel reply (instead of the bpf modified buffer)
// This is important if the answer is larger than PAGE_SIZE (max size this bpf hook can provide)
ctx->optlen = 0;
- return 1; // ALLOW
+ return BPF_ALLOW;
}
DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", AID_ROOT, AID_ROOT, setsockopt_prog, KVER_5_4)
@@ -751,9 +752,8 @@
// Tell kernel to use/process original buffer provided by userspace.
// This is important if it is larger than PAGE_SIZE (max size this bpf hook can handle).
ctx->optlen = 0;
- return 1; // ALLOW
+ return BPF_ALLOW;
}
LICENSE("Apache 2.0");
CRITICAL("Connectivity and netd");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/netd.h b/bpf/progs/netd.h
similarity index 98%
rename from bpf_progs/netd.h
rename to bpf/progs/netd.h
index 4877a4b..be7c311 100644
--- a/bpf_progs/netd.h
+++ b/bpf/progs/netd.h
@@ -157,6 +157,8 @@
#define CGROUP_INET_CREATE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
#define CGROUP_INET_RELEASE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsockrelease_inet_release"
+#define CGROUP_BIND4_PROG_PATH BPF_NETD_PATH "prog_netd_bind4_inet4_bind"
+#define CGROUP_BIND6_PROG_PATH BPF_NETD_PATH "prog_netd_bind6_inet6_bind"
#define CGROUP_CONNECT4_PROG_PATH BPF_NETD_PATH "prog_netd_connect4_inet4_connect"
#define CGROUP_CONNECT6_PROG_PATH BPF_NETD_PATH "prog_netd_connect6_inet6_connect"
#define CGROUP_UDP4_RECVMSG_PROG_PATH BPF_NETD_PATH "prog_netd_recvmsg4_udp4_recvmsg"
diff --git a/bpf_progs/offload.c b/bpf/progs/offload.c
similarity index 88%
rename from bpf_progs/offload.c
rename to bpf/progs/offload.c
index 4f152bf..631908a 100644
--- a/bpf_progs/offload.c
+++ b/bpf/progs/offload.c
@@ -14,16 +14,6 @@
* limitations under the License.
*/
-#include <linux/if.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-
-// bionic kernel uapi linux/udp.h header is munged...
-#define __kernel_udphdr udphdr
-#include <linux/udp.h>
-
#ifdef MAINLINE
// BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
// ship a different file than for later versions, but we need bpfloader v0.25+
@@ -35,61 +25,16 @@
#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
#endif /* MAINLINE */
-// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
-#define TETHERING_UID AID_ROOT
-
-#define TETHERING_GID AID_NETWORK_STACK
-
-#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
#include "offload.h"
-// From kernel:include/net/ip.h
-#define IP_DF 0x4000 // Flag: "Don't Fragment"
-
-// ----- Helper functions for offsets to fields -----
-
-// They all assume simple IP packets:
-// - no VLAN ethernet tags
-// - no IPv4 options (see IPV4_HLEN/TCP4_OFFSET/UDP4_OFFSET)
-// - no IPv6 extension headers
-// - no TCP options (see TCP_HLEN)
-
-//#define ETH_HLEN sizeof(struct ethhdr)
-#define IP4_HLEN sizeof(struct iphdr)
-#define IP6_HLEN sizeof(struct ipv6hdr)
-#define TCP_HLEN sizeof(struct tcphdr)
-#define UDP_HLEN sizeof(struct udphdr)
-
-// Offsets from beginning of L4 (TCP/UDP) header
-#define TCP_OFFSET(field) offsetof(struct tcphdr, field)
-#define UDP_OFFSET(field) offsetof(struct udphdr, field)
-
-// Offsets from beginning of L3 (IPv4) header
-#define IP4_OFFSET(field) offsetof(struct iphdr, field)
-#define IP4_TCP_OFFSET(field) (IP4_HLEN + TCP_OFFSET(field))
-#define IP4_UDP_OFFSET(field) (IP4_HLEN + UDP_OFFSET(field))
-
-// Offsets from beginning of L3 (IPv6) header
-#define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
-#define IP6_TCP_OFFSET(field) (IP6_HLEN + TCP_OFFSET(field))
-#define IP6_UDP_OFFSET(field) (IP6_HLEN + UDP_OFFSET(field))
-
-// Offsets from beginning of L2 (ie. Ethernet) header (which must be present)
-#define ETH_IP4_OFFSET(field) (ETH_HLEN + IP4_OFFSET(field))
-#define ETH_IP4_TCP_OFFSET(field) (ETH_HLEN + IP4_TCP_OFFSET(field))
-#define ETH_IP4_UDP_OFFSET(field) (ETH_HLEN + IP4_UDP_OFFSET(field))
-#define ETH_IP6_OFFSET(field) (ETH_HLEN + IP6_OFFSET(field))
-#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
-#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
-
// ----- Tethering Error Counters -----
// Note that pre-T devices with Mediatek chipsets may have a kernel bug (bad patch
// "[ALPS05162612] bpf: fix ubsan error") making it impossible to write to non-zero
// offset of bpf map ARRAYs. This file (offload.o) loads on S+, but luckily this
// array is only written by bpf code, and only read by userspace.
-DEFINE_BPF_MAP_RO(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX, TETHERING_GID)
+DEFINE_BPF_MAP_RO(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX, AID_NETWORK_STACK)
#define COUNT_AND_RETURN(counter, ret) do { \
uint32_t code = BPF_TETHER_ERR_ ## counter; \
@@ -107,27 +52,27 @@
// ----- Tethering Data Stats and Limits -----
// Tethering stats, indexed by upstream interface.
-DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, AID_NETWORK_STACK)
// Tethering data limit, indexed by upstream interface.
// (tethering allowed when stats[iif].rxBytes + stats[iif].txBytes < limit[iif])
-DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, AID_NETWORK_STACK)
// ----- IPv6 Support -----
DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 64,
- TETHERING_GID)
+ AID_NETWORK_STACK)
DEFINE_BPF_MAP_GRW(tether_downstream64_map, HASH, TetherDownstream64Key, TetherDownstream64Value,
- 1024, TETHERING_GID)
+ 1024, AID_NETWORK_STACK)
DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
- TETHERING_GID)
+ AID_NETWORK_STACK)
static inline __always_inline int do_forward6(struct __sk_buff* skb,
const struct rawip_bool rawip,
const struct stream_bool stream,
- const struct kver_uint kver) {
+ __unused const struct kver_uint kver) {
const bool is_ethernet = !rawip.rawip;
// Must be meta-ethernet IPv6 frame
@@ -140,9 +85,8 @@
// Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
// not trigger and thus we need to manually make sure we can read packet headers via DPA.
- // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
// It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
- try_make_writable(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+ if (bpf_skb_pull_data(skb, l2_header_size + IP6_HLEN)) return TC_ACT_PIPE;
void* data = (void*)(long)skb->data;
const void* data_end = (void*)(long)skb->data_end;
@@ -165,6 +109,14 @@
// If hardware offload is running and programming flows based on conntrack entries,
// try not to interfere with it.
if (ip6->nexthdr == IPPROTO_TCP) {
+ // don't need to check return code, as it's effectively checked in the next 'if' below
+ bpf_skb_pull_data(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+
+ data = (void*)(long)skb->data;
+ data_end = (void*)(long)skb->data_end;
+ eth = is_ethernet ? data : NULL; // used iff is_ethernet
+ ip6 = is_ethernet ? (void*)(eth + 1) : data;
+
struct tcphdr* tcph = (void*)(ip6 + 1);
// Make sure we can get at the tcp header
@@ -302,13 +254,13 @@
return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
}
-DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream6_ether)
(struct __sk_buff* skb) {
return do_forward6(skb, ETHER, DOWNSTREAM, KVER_NONE);
}
-DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream6_ether)
(struct __sk_buff* skb) {
return do_forward6(skb, ETHER, UPSTREAM, KVER_NONE);
@@ -328,42 +280,42 @@
// and in system/netd/tests/binder_test.cpp NetdBinderTest TetherOffloadForwarding.
//
// Hence, these mandatory (must load successfully) implementations for 4.14+ kernels:
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream6_rawip_4_14, KVER_4_14)
(struct __sk_buff* skb) {
return do_forward6(skb, RAWIP, DOWNSTREAM, KVER_4_14);
}
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream6_rawip_4_14, KVER_4_14)
(struct __sk_buff* skb) {
return do_forward6(skb, RAWIP, UPSTREAM, KVER_4_14);
}
// and define no-op stubs for pre-4.14 kernels.
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
// ----- IPv4 Support -----
-DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
-DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
const int l2_header_size, void* data, const void* data_end,
struct ethhdr* eth, struct iphdr* ip, const struct rawip_bool rawip,
const struct stream_bool stream, const struct updatetime_bool updatetime,
- const bool is_tcp, const struct kver_uint kver) {
+ const bool is_tcp, __unused const struct kver_uint kver) {
const bool is_ethernet = !rawip.rawip;
struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
@@ -593,7 +545,7 @@
// Calculate the IPv4 one's complement checksum of the IPv4 header.
__wsum sum4 = 0;
- for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
+ for (unsigned i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
sum4 += ((__u16*)ip)[i];
}
// Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
@@ -656,25 +608,25 @@
// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
}
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
}
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
}
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
@@ -684,7 +636,7 @@
// (optional, because we need to be able to fallback for 4.14/4.19/5.4 pre-S kernels)
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_opt,
KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
@@ -692,7 +644,7 @@
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_opt,
KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
@@ -700,7 +652,7 @@
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_opt,
KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
@@ -708,7 +660,7 @@
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_opt,
KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
@@ -729,13 +681,13 @@
// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
}
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
@@ -745,7 +697,7 @@
// [Note: fallback for 4.14/4.19 (P/Q) kernels is below in stub section]
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_4_14,
KVER_4_14, KVER_5_4)
(struct __sk_buff* skb) {
@@ -753,7 +705,7 @@
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
- TETHERING_UID, TETHERING_GID,
+ AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_4_14,
KVER_4_14, KVER_5_4)
(struct __sk_buff* skb) {
@@ -762,13 +714,13 @@
// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
}
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
@@ -778,43 +730,43 @@
// RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER_5_4)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER_5_4)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
// ETHER: 4.9-P/Q kernel
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
// ----- XDP Support -----
-DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, AID_NETWORK_STACK)
-static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const struct rawip_bool rawip,
- const struct stream_bool stream) {
+static inline __always_inline int do_xdp_forward6(__unused struct xdp_md *ctx,
+ __unused const struct rawip_bool rawip, __unused const struct stream_bool stream) {
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const struct rawip_bool rawip,
- const struct stream_bool stream) {
+static inline __always_inline int do_xdp_forward4(__unused struct xdp_md *ctx,
+ __unused const struct rawip_bool rawip, __unused const struct stream_bool stream) {
return XDP_PASS;
}
@@ -853,7 +805,7 @@
}
#define DEFINE_XDP_PROG(str, func) \
- DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER_5_9)(struct xdp_md *ctx)
+ DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_9)(struct xdp_md *ctx)
DEFINE_XDP_PROG("xdp/tether_downstream_ether",
xdp_tether_downstream_ether) {
@@ -877,4 +829,3 @@
LICENSE("Apache 2.0");
CRITICAL("Connectivity (Tethering)");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/offload.h b/bpf/progs/offload.h
similarity index 100%
rename from bpf_progs/offload.h
rename to bpf/progs/offload.h
diff --git a/bpf_progs/offload@mainline.c b/bpf/progs/offload@mainline.c
similarity index 100%
rename from bpf_progs/offload@mainline.c
rename to bpf/progs/offload@mainline.c
diff --git a/bpf_progs/test.c b/bpf/progs/test.c
similarity index 63%
rename from bpf_progs/test.c
rename to bpf/progs/test.c
index 6a4471c..8585118 100644
--- a/bpf_progs/test.c
+++ b/bpf/progs/test.c
@@ -14,10 +14,6 @@
* limitations under the License.
*/
-#include <linux/if_ether.h>
-#include <linux/in.h>
-#include <linux/ip.h>
-
#ifdef MAINLINE
// BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
// ship a different file than for later versions, but we need bpfloader v0.25+
@@ -29,46 +25,30 @@
#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
#endif /* MAINLINE */
-// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
-#define TETHERING_UID AID_ROOT
-
-#define TETHERING_GID AID_NETWORK_STACK
-
// This is non production code, only used for testing
// Needed because the bitmap array definition is non-kosher for pre-T OS devices.
#define THIS_BPF_PROGRAM_IS_FOR_TEST_PURPOSES_ONLY
-#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
#include "offload.h"
// Used only by TetheringPrivilegedTests, not by production code.
DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
- TETHERING_GID)
+ AID_NETWORK_STACK)
DEFINE_BPF_MAP_GRW(tether2_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
- TETHERING_GID)
+ AID_NETWORK_STACK)
DEFINE_BPF_MAP_GRW(tether3_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
- TETHERING_GID)
+ AID_NETWORK_STACK)
// Used only by BpfBitmapTest, not by production code.
-DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, AID_NETWORK_STACK)
-DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", TETHERING_UID, TETHERING_GID,
- xdp_test, KVER_5_9)
-(struct xdp_md *ctx) {
- void *data = (void *)(long)ctx->data;
- void *data_end = (void *)(long)ctx->data_end;
-
- struct ethhdr *eth = data;
- int hsize = sizeof(*eth);
-
- struct iphdr *ip = data + hsize;
- hsize += sizeof(struct iphdr);
-
- if (data + hsize > data_end) return XDP_PASS;
- if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS;
- if (ip->protocol == IPPROTO_UDP) return XDP_DROP;
- return XDP_PASS;
+// we need at least 1 bpf program in the final .o for Android S bpfloader compatibility
+// this program is trivial, and has a 'infinite' minimum kernel version number,
+// so will always be skipped
+DEFINE_BPF_PROG_KVER("skfilter/match", AID_ROOT, AID_ROOT, match, KVER_INF)
+(__unused struct __sk_buff* skb) {
+ return XTBPF_MATCH;
}
LICENSE("Apache 2.0");
-DISABLE_BTF_ON_USER_BUILDS();
+CRITICAL("Networking xTS tests");
diff --git a/bpf_progs/test@mainline.c b/bpf/progs/test@mainline.c
similarity index 100%
rename from bpf_progs/test@mainline.c
rename to bpf/progs/test@mainline.c
diff --git a/staticlibs/native/bpf_syscall_wrappers/Android.bp b/bpf/syscall_wrappers/Android.bp
similarity index 100%
rename from staticlibs/native/bpf_syscall_wrappers/Android.bp
rename to bpf/syscall_wrappers/Android.bp
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
similarity index 75%
rename from staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
rename to bpf/syscall_wrappers/include/BpfSyscallWrappers.h
index 73cef89..a31445a 100644
--- a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
@@ -16,24 +16,20 @@
#pragma once
+#include <android-base/unique_fd.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/bpf.h>
#include <linux/unistd.h>
#include <sys/file.h>
-#ifdef BPF_FD_JUST_USE_INT
- #define BPF_FD_TYPE int
- #define BPF_FD_TO_U32(x) static_cast<__u32>(x)
-#else
- #include <android-base/unique_fd.h>
- #define BPF_FD_TYPE base::unique_fd&
- #define BPF_FD_TO_U32(x) static_cast<__u32>((x).get())
-#endif
namespace android {
namespace bpf {
+using ::android::base::borrowed_fd;
+using ::android::base::unique_fd;
+
inline uint64_t ptr_to_u64(const void * const x) {
return (uint64_t)(uintptr_t)x;
}
@@ -69,58 +65,59 @@
// 'inner_map_fd' is basically a template specifying {map_type, key_size, value_size, max_entries, map_flags}
// of the inner map type (and possibly only key_size/value_size actually matter?).
inline int createOuterMap(bpf_map_type map_type, uint32_t key_size, uint32_t value_size,
- uint32_t max_entries, uint32_t map_flags, const BPF_FD_TYPE inner_map_fd) {
+ uint32_t max_entries, uint32_t map_flags,
+ const borrowed_fd& inner_map_fd) {
return bpf(BPF_MAP_CREATE, {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries,
.map_flags = map_flags,
- .inner_map_fd = BPF_FD_TO_U32(inner_map_fd),
+ .inner_map_fd = static_cast<__u32>(inner_map_fd.get()),
});
}
-inline int writeToMapEntry(const BPF_FD_TYPE map_fd, const void* key, const void* value,
+inline int writeToMapEntry(const borrowed_fd& map_fd, const void* key, const void* value,
uint64_t flags) {
return bpf(BPF_MAP_UPDATE_ELEM, {
- .map_fd = BPF_FD_TO_U32(map_fd),
+ .map_fd = static_cast<__u32>(map_fd.get()),
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
.flags = flags,
});
}
-inline int findMapEntry(const BPF_FD_TYPE map_fd, const void* key, void* value) {
+inline int findMapEntry(const borrowed_fd& map_fd, const void* key, void* value) {
return bpf(BPF_MAP_LOOKUP_ELEM, {
- .map_fd = BPF_FD_TO_U32(map_fd),
+ .map_fd = static_cast<__u32>(map_fd.get()),
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
});
}
-inline int deleteMapEntry(const BPF_FD_TYPE map_fd, const void* key) {
+inline int deleteMapEntry(const borrowed_fd& map_fd, const void* key) {
return bpf(BPF_MAP_DELETE_ELEM, {
- .map_fd = BPF_FD_TO_U32(map_fd),
+ .map_fd = static_cast<__u32>(map_fd.get()),
.key = ptr_to_u64(key),
});
}
-inline int getNextMapKey(const BPF_FD_TYPE map_fd, const void* key, void* next_key) {
+inline int getNextMapKey(const borrowed_fd& map_fd, const void* key, void* next_key) {
return bpf(BPF_MAP_GET_NEXT_KEY, {
- .map_fd = BPF_FD_TO_U32(map_fd),
+ .map_fd = static_cast<__u32>(map_fd.get()),
.key = ptr_to_u64(key),
.next_key = ptr_to_u64(next_key),
});
}
-inline int getFirstMapKey(const BPF_FD_TYPE map_fd, void* firstKey) {
+inline int getFirstMapKey(const borrowed_fd& map_fd, void* firstKey) {
return getNextMapKey(map_fd, NULL, firstKey);
}
-inline int bpfFdPin(const BPF_FD_TYPE map_fd, const char* pathname) {
+inline int bpfFdPin(const borrowed_fd& map_fd, const char* pathname) {
return bpf(BPF_OBJ_PIN, {
.pathname = ptr_to_u64(pathname),
- .bpf_fd = BPF_FD_TO_U32(map_fd),
+ .bpf_fd = static_cast<__u32>(map_fd.get()),
});
}
@@ -131,22 +128,15 @@
});
}
-int bpfGetFdMapId(const BPF_FD_TYPE map_fd);
+int bpfGetFdMapId(const borrowed_fd& map_fd);
inline int bpfLock(int fd, short type) {
if (fd < 0) return fd; // pass any errors straight through
#ifdef BPF_MAP_LOCKLESS_FOR_TEST
return fd;
#endif
-#ifdef BPF_FD_JUST_USE_INT
int mapId = bpfGetFdMapId(fd);
int saved_errno = errno;
-#else
- base::unique_fd ufd(fd);
- int mapId = bpfGetFdMapId(ufd);
- int saved_errno = errno;
- (void)ufd.release();
-#endif
// 4.14+ required to fetch map id, but we don't want to call isAtLeastKernelVersion
if (mapId == -1 && saved_errno == EINVAL) return fd;
if (mapId <= 0) abort(); // should not be possible
@@ -193,37 +183,35 @@
}
inline bool usableProgram(const char* pathname) {
- int fd = retrieveProgram(pathname);
- bool ok = (fd >= 0);
- if (ok) close(fd);
- return ok;
+ unique_fd fd(retrieveProgram(pathname));
+ return fd.ok();
}
-inline int attachProgram(bpf_attach_type type, const BPF_FD_TYPE prog_fd,
- const BPF_FD_TYPE cg_fd, uint32_t flags = 0) {
+inline int attachProgram(bpf_attach_type type, const borrowed_fd& prog_fd,
+ const borrowed_fd& cg_fd, uint32_t flags = 0) {
return bpf(BPF_PROG_ATTACH, {
- .target_fd = BPF_FD_TO_U32(cg_fd),
- .attach_bpf_fd = BPF_FD_TO_U32(prog_fd),
+ .target_fd = static_cast<__u32>(cg_fd.get()),
+ .attach_bpf_fd = static_cast<__u32>(prog_fd.get()),
.attach_type = type,
.attach_flags = flags,
});
}
-inline int detachProgram(bpf_attach_type type, const BPF_FD_TYPE cg_fd) {
+inline int detachProgram(bpf_attach_type type, const borrowed_fd& cg_fd) {
return bpf(BPF_PROG_DETACH, {
- .target_fd = BPF_FD_TO_U32(cg_fd),
+ .target_fd = static_cast<__u32>(cg_fd.get()),
.attach_type = type,
});
}
-inline int queryProgram(const BPF_FD_TYPE cg_fd,
+inline int queryProgram(const borrowed_fd& cg_fd,
enum bpf_attach_type attach_type,
__u32 query_flags = 0,
__u32 attach_flags = 0) {
int prog_id = -1; // equivalent to an array of one integer.
bpf_attr arg = {
.query = {
- .target_fd = BPF_FD_TO_U32(cg_fd),
+ .target_fd = static_cast<__u32>(cg_fd.get()),
.attach_type = attach_type,
.query_flags = query_flags,
.attach_flags = attach_flags,
@@ -237,21 +225,21 @@
return prog_id; // return actual id
}
-inline int detachSingleProgram(bpf_attach_type type, const BPF_FD_TYPE prog_fd,
- const BPF_FD_TYPE cg_fd) {
+inline int detachSingleProgram(bpf_attach_type type, const borrowed_fd& prog_fd,
+ const borrowed_fd& cg_fd) {
return bpf(BPF_PROG_DETACH, {
- .target_fd = BPF_FD_TO_U32(cg_fd),
- .attach_bpf_fd = BPF_FD_TO_U32(prog_fd),
+ .target_fd = static_cast<__u32>(cg_fd.get()),
+ .attach_bpf_fd = static_cast<__u32>(prog_fd.get()),
.attach_type = type,
});
}
// Available in 4.12 and later kernels.
-inline int runProgram(const BPF_FD_TYPE prog_fd, const void* data,
+inline int runProgram(const borrowed_fd& prog_fd, const void* data,
const uint32_t data_size) {
return bpf(BPF_PROG_RUN, {
.test = {
- .prog_fd = BPF_FD_TO_U32(prog_fd),
+ .prog_fd = static_cast<__u32>(prog_fd.get()),
.data_size_in = data_size,
.data_in = ptr_to_u64(data),
},
@@ -265,10 +253,10 @@
// supported/returned by the running kernel. We do this by checking it is fully
// within the bounds of the struct size as reported by the kernel.
#define DEFINE_BPF_GET_FD(TYPE, NAME, FIELD) \
-inline int bpfGetFd ## NAME(const BPF_FD_TYPE fd) { \
+inline int bpfGetFd ## NAME(const borrowed_fd& fd) { \
struct bpf_ ## TYPE ## _info info = {}; \
union bpf_attr attr = { .info = { \
- .bpf_fd = BPF_FD_TO_U32(fd), \
+ .bpf_fd = static_cast<__u32>(fd.get()), \
.info_len = sizeof(info), \
.info = ptr_to_u64(&info), \
}}; \
@@ -283,19 +271,16 @@
// All 7 of these fields are already present in Linux v4.14 (even ACK 4.14-P)
// while BPF_OBJ_GET_INFO_BY_FD is not implemented at all in v4.9 (even ACK 4.9-Q)
-DEFINE_BPF_GET_FD(map, MapType, type) // int bpfGetFdMapType(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(map, MapId, id) // int bpfGetFdMapId(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(map, KeySize, key_size) // int bpfGetFdKeySize(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(map, ValueSize, value_size) // int bpfGetFdValueSize(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(map, MaxEntries, max_entries) // int bpfGetFdMaxEntries(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(map, MapFlags, map_flags) // int bpfGetFdMapFlags(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(prog, ProgId, id) // int bpfGetFdProgId(const BPF_FD_TYPE prog_fd)
+DEFINE_BPF_GET_FD(map, MapType, type) // int bpfGetFdMapType(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(map, MapId, id) // int bpfGetFdMapId(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(map, KeySize, key_size) // int bpfGetFdKeySize(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(map, ValueSize, value_size) // int bpfGetFdValueSize(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(map, MaxEntries, max_entries) // int bpfGetFdMaxEntries(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(map, MapFlags, map_flags) // int bpfGetFdMapFlags(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(prog, ProgId, id) // int bpfGetFdProgId(const borrowed_fd& prog_fd)
#undef DEFINE_BPF_GET_FD
} // namespace bpf
} // namespace android
-#undef BPF_FD_TO_U32
-#undef BPF_FD_TYPE
-#undef BPF_FD_JUST_USE_INT
diff --git a/tests/mts/Android.bp b/bpf/tests/mts/Android.bp
similarity index 97%
rename from tests/mts/Android.bp
rename to bpf/tests/mts/Android.bp
index c118d0a..9d158fd 100644
--- a/tests/mts/Android.bp
+++ b/bpf/tests/mts/Android.bp
@@ -40,6 +40,6 @@
srcs: [
"bpf_existence_test.cpp",
],
- compile_multilib: "first",
+ compile_multilib: "both",
min_sdk_version: "30", // Ensure test runs on R and above.
}
diff --git a/tests/mts/OWNERS b/bpf/tests/mts/OWNERS
similarity index 100%
rename from tests/mts/OWNERS
rename to bpf/tests/mts/OWNERS
diff --git a/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
similarity index 93%
rename from tests/mts/bpf_existence_test.cpp
rename to bpf/tests/mts/bpf_existence_test.cpp
index 29f5cd2..0b5b7be 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -80,20 +80,15 @@
TETHERING "prog_offload_schedcls_tether_upstream6_rawip",
};
-// Provided by *current* mainline module for S+ devices with 5.10+ kernels
-static const set<string> MAINLINE_FOR_S_5_10_PLUS = {
- TETHERING "prog_test_xdp_drop_ipv4_udp_ether",
-};
-
// Provided by *current* mainline module for T+ devices
static const set<string> MAINLINE_FOR_T_PLUS = {
- SHARED "map_block_blocked_ports_map",
SHARED "map_clatd_clat_egress4_map",
SHARED "map_clatd_clat_ingress6_map",
SHARED "map_dscpPolicy_ipv4_dscp_policies_map",
SHARED "map_dscpPolicy_ipv6_dscp_policies_map",
SHARED "map_dscpPolicy_socket_policy_cache_map",
NETD "map_netd_app_uid_stats_map",
+ NETD "map_netd_blocked_ports_map",
NETD "map_netd_configuration_map",
NETD "map_netd_cookie_tag_map",
NETD "map_netd_data_saver_enabled_map",
@@ -124,8 +119,13 @@
// Provided by *current* mainline module for T+ devices with 5.4+ kernels
static const set<string> MAINLINE_FOR_T_4_19_PLUS = {
- NETD_RO "prog_block_bind4_block_port",
- NETD_RO "prog_block_bind6_block_port",
+ NETD "prog_netd_bind4_inet4_bind",
+ NETD "prog_netd_bind6_inet6_bind",
+};
+
+// Provided by *current* mainline module for T+ devices with 5.10+ kernels
+static const set<string> MAINLINE_FOR_T_5_10_PLUS = {
+ NETD "prog_netd_cgroupsockrelease_inet_release",
};
// Provided by *current* mainline module for T+ devices with 5.15+ kernels
@@ -159,11 +159,6 @@
NETD "prog_netd_setsockopt_prog",
};
-// Provided by *current* mainline module for U+ devices with 5.10+ kernels
-static const set<string> MAINLINE_FOR_V_5_10_PLUS = {
- NETD "prog_netd_cgroupsockrelease_inet_release",
-};
-
static void addAll(set<string>& a, const set<string>& b) {
a.insert(b.begin(), b.end());
}
@@ -194,7 +189,6 @@
// S requires Linux Kernel 4.9+ and thus requires eBPF support.
if (IsAtLeastS()) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
DO_EXPECT(IsAtLeastS(), MAINLINE_FOR_S_PLUS);
- DO_EXPECT(IsAtLeastS() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_S_5_10_PLUS);
// Nothing added or removed in SCv2.
@@ -202,6 +196,7 @@
DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
+ DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_T_5_10_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
// U requires Linux Kernel 4.14+, but nothing (as yet) added or removed in U.
@@ -213,7 +208,6 @@
if (IsAtLeastV()) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
DO_EXPECT(IsAtLeastV(), MAINLINE_FOR_V_PLUS);
DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
- DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_V_5_10_PLUS);
for (const auto& file : mustExist) {
EXPECT_EQ(0, access(file.c_str(), R_OK)) << file << " does not exist";
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
deleted file mode 100644
index 152dda6..0000000
--- a/bpf_progs/block.c
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2022 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.
- */
-
-#include <linux/types.h>
-#include <linux/bpf.h>
-#include <netinet/in.h>
-#include <stdint.h>
-
-// The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-
-#include "bpf_helpers.h"
-
-static const int ALLOW = 1;
-static const int DISALLOW = 0;
-
-DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
- 1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
-
-static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
- if (!ctx->user_port) return ALLOW;
-
- switch (ctx->protocol) {
- case IPPROTO_TCP:
- case IPPROTO_MPTCP:
- case IPPROTO_UDP:
- case IPPROTO_UDPLITE:
- case IPPROTO_DCCP:
- case IPPROTO_SCTP:
- break;
- default:
- return ALLOW; // unknown protocols are allowed
- }
-
- int key = ctx->user_port >> 6;
- int shift = ctx->user_port & 63;
-
- uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
- // Lookup should never fail in reality, but if it does return here to keep the
- // BPF verifier happy.
- if (!val) return ALLOW;
-
- if ((*val >> shift) & 1) return DISALLOW;
- return ALLOW;
-}
-
-// the program need to be accessible/loadable by netd (from netd updatable plugin)
-#define DEFINE_NETD_RO_BPF_PROG(SECTION_NAME, the_prog, min_kver) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, min_kver, KVER_INF, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
- "", "netd_readonly/", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
-
-DEFINE_NETD_RO_BPF_PROG("bind4/block_port", bind4_block_port, KVER_4_19)
-(struct bpf_sock_addr *ctx) {
- return block_port(ctx);
-}
-
-DEFINE_NETD_RO_BPF_PROG("bind6/block_port", bind6_block_port, KVER_4_19)
-(struct bpf_sock_addr *ctx) {
- return block_port(ctx);
-}
-
-LICENSE("Apache 2.0");
-CRITICAL("ConnectivityNative");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
deleted file mode 100644
index ed114e4..0000000
--- a/bpf_progs/dscpPolicy.c
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * Copyright (C) 2021 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.
- */
-
-#include <linux/bpf.h>
-#include <linux/if_ether.h>
-#include <linux/if_packet.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-#include <linux/types.h>
-#include <netinet/in.h>
-#include <netinet/udp.h>
-#include <stdint.h>
-#include <string.h>
-
-// The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-
-#include "bpf_helpers.h"
-#include "dscpPolicy.h"
-
-#define ECN_MASK 3
-#define IP4_OFFSET(field, header) ((header) + offsetof(struct iphdr, field))
-#define UPDATE_TOS(dscp, tos) ((dscp) << 2) | ((tos) & ECN_MASK)
-
-DEFINE_BPF_MAP_GRW(socket_policy_cache_map, HASH, uint64_t, RuleEntry, CACHE_MAP_SIZE, AID_SYSTEM)
-
-DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
-
-static inline __always_inline void match_policy(struct __sk_buff* skb, bool ipv4) {
- void* data = (void*)(long)skb->data;
- const void* data_end = (void*)(long)skb->data_end;
-
- const int l2_header_size = sizeof(struct ethhdr);
- struct ethhdr* eth = data;
-
- if (data + l2_header_size > data_end) return;
-
- int hdr_size = 0;
-
- // used for map lookup
- uint64_t cookie = bpf_get_socket_cookie(skb);
- if (!cookie) return;
-
- __be16 sport = 0;
- uint16_t dport = 0;
- uint8_t protocol = 0; // TODO: Use are reserved value? Or int (-1) and cast to uint below?
- struct in6_addr src_ip = {};
- struct in6_addr dst_ip = {};
- uint8_t tos = 0; // Only used for IPv4
- __be32 old_first_be32 = 0; // Only used for IPv6
- if (ipv4) {
- const struct iphdr* const iph = (void*)(eth + 1);
- hdr_size = l2_header_size + sizeof(struct iphdr);
- // Must have ipv4 header
- if (data + hdr_size > data_end) return;
-
- // IP version must be 4
- if (iph->version != 4) return;
-
- // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
- if (iph->ihl != 5) return;
-
- // V4 mapped address in in6_addr sets 10/11 position to 0xff.
- src_ip.s6_addr32[2] = htonl(0x0000ffff);
- dst_ip.s6_addr32[2] = htonl(0x0000ffff);
-
- // Copy IPv4 address into in6_addr for easy comparison below.
- src_ip.s6_addr32[3] = iph->saddr;
- dst_ip.s6_addr32[3] = iph->daddr;
- protocol = iph->protocol;
- tos = iph->tos;
- } else {
- struct ipv6hdr* ip6h = (void*)(eth + 1);
- hdr_size = l2_header_size + sizeof(struct ipv6hdr);
- // Must have ipv6 header
- if (data + hdr_size > data_end) return;
-
- if (ip6h->version != 6) return;
-
- src_ip = ip6h->saddr;
- dst_ip = ip6h->daddr;
- protocol = ip6h->nexthdr;
- old_first_be32 = *(__be32*)ip6h;
- }
-
- switch (protocol) {
- case IPPROTO_UDP:
- case IPPROTO_UDPLITE: {
- struct udphdr* udp;
- udp = data + hdr_size;
- if ((void*)(udp + 1) > data_end) return;
- sport = udp->source;
- dport = ntohs(udp->dest);
- } break;
- case IPPROTO_TCP: {
- struct tcphdr* tcp;
- tcp = data + hdr_size;
- if ((void*)(tcp + 1) > data_end) return;
- sport = tcp->source;
- dport = ntohs(tcp->dest);
- } break;
- default:
- return;
- }
-
- RuleEntry* existing_rule = bpf_socket_policy_cache_map_lookup_elem(&cookie);
-
- if (existing_rule &&
- v6_equal(src_ip, existing_rule->src_ip) &&
- v6_equal(dst_ip, existing_rule->dst_ip) &&
- skb->ifindex == existing_rule->ifindex &&
- sport == existing_rule->src_port &&
- dport == existing_rule->dst_port &&
- protocol == existing_rule->proto) {
- if (existing_rule->dscp_val < 0) return;
- if (ipv4) {
- uint8_t newTos = UPDATE_TOS(existing_rule->dscp_val, tos);
- bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(newTos),
- sizeof(uint16_t));
- bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &newTos, sizeof(newTos), 0);
- } else {
- __be32 new_first_be32 =
- htonl(ntohl(old_first_be32) & 0xF03FFFFF | (existing_rule->dscp_val << 22));
- bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
- BPF_F_RECOMPUTE_CSUM);
- }
- return;
- }
-
- // Linear scan ipv4_dscp_policies_map since no stored params match skb.
- int best_score = 0;
- int8_t new_dscp = -1;
-
- for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
- // Using a uint64 in for loop prevents infinite loop during BPF load,
- // but the key is uint32, so convert back.
- uint32_t key = i;
-
- DscpPolicy* policy;
- if (ipv4) {
- policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
- } else {
- policy = bpf_ipv6_dscp_policies_map_lookup_elem(&key);
- }
-
- // If the policy lookup failed, just continue (this should not ever happen)
- if (!policy) continue;
-
- // If policy iface index does not match skb, then skip to next policy.
- if (policy->ifindex != skb->ifindex) continue;
-
- int score = 0;
-
- if (policy->present_fields & PROTO_MASK_FLAG) {
- if (protocol != policy->proto) continue;
- score += 0xFFFF;
- }
- if (policy->present_fields & SRC_IP_MASK_FLAG) {
- if (v6_not_equal(src_ip, policy->src_ip)) continue;
- score += 0xFFFF;
- }
- if (policy->present_fields & DST_IP_MASK_FLAG) {
- if (v6_not_equal(dst_ip, policy->dst_ip)) continue;
- score += 0xFFFF;
- }
- if (policy->present_fields & SRC_PORT_MASK_FLAG) {
- if (sport != policy->src_port) continue;
- score += 0xFFFF;
- }
- if (dport < policy->dst_port_start) continue;
- if (dport > policy->dst_port_end) continue;
- score += 0xFFFF + policy->dst_port_start - policy->dst_port_end;
-
- if (score > best_score) {
- best_score = score;
- new_dscp = policy->dscp_val;
- }
- }
-
- RuleEntry value = {
- .src_ip = src_ip,
- .dst_ip = dst_ip,
- .ifindex = skb->ifindex,
- .src_port = sport,
- .dst_port = dport,
- .proto = protocol,
- .dscp_val = new_dscp,
- };
-
- // Update cache with found policy.
- bpf_socket_policy_cache_map_update_elem(&cookie, &value, BPF_ANY);
-
- if (new_dscp < 0) return;
-
- // Need to store bytes after updating map or program will not load.
- if (ipv4) {
- uint8_t new_tos = UPDATE_TOS(new_dscp, tos);
- bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(new_tos), 2);
- bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &new_tos, sizeof(new_tos), 0);
- } else {
- __be32 new_first_be32 = htonl(ntohl(old_first_be32) & 0xF03FFFFF | (new_dscp << 22));
- bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
- BPF_F_RECOMPUTE_CSUM);
- }
- return;
-}
-
-DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM, schedcls_set_dscp_ether,
- KVER_5_15)
-(struct __sk_buff* skb) {
- if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
-
- if (skb->protocol == htons(ETH_P_IP)) {
- match_policy(skb, true);
- } else if (skb->protocol == htons(ETH_P_IPV6)) {
- match_policy(skb, false);
- }
-
- // Always return TC_ACT_PIPE
- return TC_ACT_PIPE;
-}
-
-LICENSE("Apache 2.0");
-CRITICAL("Connectivity");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index 21be1d3..39ff2d4 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -22,6 +22,16 @@
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
+java_aconfig_library {
+ name: "com.android.net.flags-aconfig-java",
+ aconfig_declarations: "com.android.net.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
+
aconfig_declarations {
name: "com.android.net.thread.flags-aconfig",
package: "com.android.net.thread.flags",
@@ -30,6 +40,16 @@
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
+java_aconfig_library {
+ name: "com.android.net.thread.flags-aconfig-java",
+ aconfig_declarations: "com.android.net.thread.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
+
aconfig_declarations {
name: "nearby_flags",
package: "com.android.nearby.flags",
@@ -37,3 +57,30 @@
srcs: ["nearby_flags.aconfig"],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
+
+java_aconfig_library {
+ name: "com.android.nearby.flags-aconfig-java",
+ aconfig_declarations: "nearby_flags",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
+
+aconfig_declarations {
+ name: "com.android.networksecurity.flags-aconfig",
+ package: "com.android.net.ct.flags",
+ container: "com.android.tethering",
+ srcs: ["networksecurity_flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+java_aconfig_library {
+ name: "networksecurity_flags_java_lib",
+ aconfig_declarations: "com.android.networksecurity.flags-aconfig",
+ min_sdk_version: "30",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ apex_available: ["com.android.tethering"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/common/OWNERS b/common/OWNERS
index e7f5d11..989d286 100644
--- a/common/OWNERS
+++ b/common/OWNERS
@@ -1 +1,2 @@
per-file thread_flags.aconfig = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
+per-file networksecurity_flags.aconfig = file:platform/packages/modules/Connectivity:main:/networksecurity/OWNERS
\ No newline at end of file
diff --git a/common/flags.aconfig b/common/flags.aconfig
index b320b61..17ef94b 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -10,6 +10,7 @@
namespace: "android_core_networking"
description: "Set data saver through ConnectivityManager API"
bug: "297836825"
+ is_fixed_read_only: true
}
flag {
@@ -18,6 +19,7 @@
namespace: "android_core_networking"
description: "This flag controls whether isUidNetworkingBlocked is supported"
bug: "297836825"
+ is_fixed_read_only: true
}
flag {
@@ -26,6 +28,7 @@
namespace: "android_core_networking"
description: "Block network access for apps in a low importance background state"
bug: "304347838"
+ is_fixed_read_only: true
}
flag {
@@ -34,14 +37,16 @@
namespace: "android_core_networking_ipsec"
description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
bug: "308011229"
+ is_fixed_read_only: true
}
flag {
- name: "tethering_request_with_soft_ap_config"
+ name: "tethering_with_soft_ap_config"
is_exported: true
namespace: "android_core_networking"
description: "The flag controls the access for the parcelable TetheringRequest with getSoftApConfiguration/setSoftApConfiguration API"
bug: "216524590"
+ is_fixed_read_only: true
}
flag {
@@ -50,6 +55,7 @@
namespace: "android_core_networking"
description: "Flag for API to support requesting restricted wifi"
bug: "315835605"
+ is_fixed_read_only: true
}
flag {
@@ -58,6 +64,7 @@
namespace: "android_core_networking"
description: "Flag for local network capability API"
bug: "313000440"
+ is_fixed_read_only: true
}
flag {
@@ -66,6 +73,7 @@
namespace: "android_core_networking"
description: "Flag for satellite transport API"
bug: "320514105"
+ is_fixed_read_only: true
}
flag {
@@ -74,6 +82,7 @@
namespace: "android_core_networking"
description: "Flag for API to support nsd subtypes"
bug: "265095929"
+ is_fixed_read_only: true
}
flag {
@@ -82,6 +91,7 @@
namespace: "android_core_networking"
description: "Flag for API to register nsd offload engine"
bug: "301713539"
+ is_fixed_read_only: true
}
flag {
@@ -90,6 +100,7 @@
namespace: "android_core_networking"
description: "Flag for metered network firewall chain API"
bug: "332628891"
+ is_fixed_read_only: true
}
flag {
@@ -98,6 +109,7 @@
namespace: "android_core_networking"
description: "Flag for oem deny chains blocked reasons API"
bug: "328732146"
+ is_fixed_read_only: true
}
flag {
@@ -106,6 +118,7 @@
namespace: "android_core_networking"
description: "Flag for BLOCKED_REASON_NETWORK_RESTRICTED API"
bug: "339559837"
+ is_fixed_read_only: true
}
flag {
@@ -114,6 +127,7 @@
namespace: "android_core_networking"
description: "Flag for NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED API"
bug: "343823469"
+ is_fixed_read_only: true
}
flag {
@@ -122,4 +136,23 @@
namespace: "android_core_networking"
description: "Flag for introducing TETHERING_VIRTUAL type"
bug: "340376953"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "netstats_add_entries"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for NetworkStats#addEntries API"
+ bug: "335680025"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "tethering_active_sessions_metrics"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for collecting tethering active sessions metrics"
+ bug: "354619988"
+ is_fixed_read_only: true
}
diff --git a/common/networksecurity_flags.aconfig b/common/networksecurity_flags.aconfig
new file mode 100644
index 0000000..4a83af4
--- /dev/null
+++ b/common/networksecurity_flags.aconfig
@@ -0,0 +1,19 @@
+package: "com.android.net.ct.flags"
+container: "com.android.tethering"
+flag {
+ name: "certificate_transparency_service"
+ is_exported: true
+ namespace: "network_security"
+ description: "Enable service for certificate transparency log list data"
+ bug: "319829948"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "certificate_transparency_job"
+ is_exported: true
+ namespace: "network_security"
+ description: "Enable daily job service for certificate transparency instead of flags listener"
+ bug: "319829948"
+ is_fixed_read_only: true
+}
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
index 43acd1b..8cc2bb4 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -8,3 +8,48 @@
description: "Controls whether the Android Thread feature is enabled"
bug: "301473012"
}
+
+flag {
+ name: "configuration_enabled"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "thread_network"
+ description: "Controls whether the Android Thread configuration is enabled"
+ bug: "342519412"
+}
+
+flag {
+ name: "channel_max_powers_enabled"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "thread_network"
+ description: "Controls whether the Android Thread setting max power of channel feature is enabled"
+ bug: "346686506"
+}
+
+flag {
+ name: "epskc_enabled"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "thread_network"
+ description: "Controls whether the Android Thread Ephemeral Key feature is enabled"
+ bug: "348323500"
+}
+
+flag {
+ name: "set_nat64_configuration_enabled"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "thread_network"
+ description: "Controls whether the setConfiguration API of NAT64 feature is enabled"
+ bug: "368456504"
+}
+
+flag {
+ name: "thread_mobile_enabled"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "thread_network"
+ description: "Controls whether Thread support for mobile devices is enabled"
+ bug: "368867060"
+}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index f076f5b..26fc145 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -62,12 +62,14 @@
static_libs: [
// Cannot go to framework-connectivity because mid_sdk checks require 31.
"modules-utils-binary-xml",
+ "com.android.nearby.flags-aconfig-java",
+ "com.android.net.thread.flags-aconfig-java",
],
impl_only_libs: [
// The build system will use framework-bluetooth module_current stubs, because
// of sdk_version: "module_current" above.
- "framework-bluetooth",
- "framework-wifi",
+ "framework-bluetooth.stubs.module_lib",
+ "framework-wifi.stubs.module_lib",
// Compile against the entire implementation of framework-connectivity,
// including hidden methods. This is safe because if framework-connectivity-t is
// on the bootclasspath (i.e., T), then framework-connectivity is also on the
@@ -97,26 +99,12 @@
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
-// The filegroup lists files that are necessary for verifying building mdns as a standalone,
-// for use with service-connectivity-mdns-standalone-build-test
-// This filegroup should never be included in anywhere in the module build. It is only used for
-// building service-connectivity-mdns-standalone-build-test target. The files will be renamed by
-// copybara to prevent them from being shadowed by the bootclasspath copies.
-filegroup {
- name: "framework-connectivity-t-mdns-standalone-build-sources",
- srcs: [
- "src/android/net/nsd/OffloadEngine.java",
- "src/android/net/nsd/OffloadServiceInfo.java",
- ],
- visibility: ["//packages/modules/Connectivity:__subpackages__"],
-}
-
java_library {
name: "framework-connectivity-t-pre-jarjar",
defaults: ["framework-connectivity-t-defaults"],
libs: [
- "framework-bluetooth",
- "framework-wifi",
+ "framework-bluetooth.stubs.module_lib",
+ "framework-wifi.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"framework-location.stubs.module_lib",
],
@@ -137,6 +125,12 @@
// framework-connectivity-pre-jarjar match at runtime.
jarjar_rules: ":framework-connectivity-jarjar-rules",
stub_only_libs: [
+ // static_libs is not used to compile stubs. So libs which have
+ // been included in static_libs might still need to
+ // be in stub_only_libs to be usable when generating the API stubs.
+ "com.android.net.flags-aconfig-java",
+ "com.android.nearby.flags-aconfig-java",
+ "com.android.net.thread.flags-aconfig-java",
// Use prebuilt framework-connectivity stubs to avoid circular dependencies
"sdk_module-lib_current_framework-connectivity",
],
@@ -178,6 +172,10 @@
// Tests using hidden APIs
"//cts/tests/netlegacy22.api",
"//cts/tests/tests/app.usage", // NetworkUsageStatsTest
+
+ // TODO: b/374174952 Remove it when VCN CTS is moved to Connectivity/
+ "//cts/tests/tests/vcn",
+
"//external/sl4a:__subpackages__",
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 1f1953c..5f8f0e3 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -310,6 +310,7 @@
public final class NetworkStats implements java.lang.Iterable<android.net.NetworkStats.Entry> android.os.Parcelable {
ctor public NetworkStats(long, int);
method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
+ method @FlaggedApi("com.android.net.flags.netstats_add_entries") @NonNull public android.net.NetworkStats addEntries(@NonNull java.util.List<android.net.NetworkStats.Entry>);
method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
method public android.net.NetworkStats clone();
method public int describeContents();
@@ -497,16 +498,37 @@
field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> CREATOR;
}
+ @FlaggedApi("com.android.net.thread.flags.configuration_enabled") public final class ThreadConfiguration implements android.os.Parcelable {
+ method public int describeContents();
+ method public boolean isNat64Enabled();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ThreadConfiguration> CREATOR;
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") public static final class ThreadConfiguration.Builder {
+ ctor @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") public ThreadConfiguration.Builder();
+ ctor @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") public ThreadConfiguration.Builder(@NonNull android.net.thread.ThreadConfiguration);
+ method @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") @NonNull public android.net.thread.ThreadConfiguration build();
+ method @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") @NonNull public android.net.thread.ThreadConfiguration.Builder setNat64Enabled(boolean);
+ }
+
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void activateEphemeralKeyMode(@NonNull java.time.Duration, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void deactivateEphemeralKeyMode(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @NonNull public java.time.Duration getMaxEphemeralKeyLifetime();
method public int getThreadVersion();
method public static boolean isAttached(int);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void leave(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void registerConfigurationCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.channel_max_powers_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setChannelMaxPowers(@NonNull @Size(min=1) android.util.SparseIntArray, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void setConfiguration(@NonNull android.net.thread.ThreadConfiguration, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void unregisterConfigurationCallback(@NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
@@ -514,6 +536,10 @@
field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
+ field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_DISABLED = 0; // 0x0
+ field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_ENABLED = 1; // 0x1
+ field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_IN_USE = 2; // 0x2
+ field public static final int MAX_POWER_CHANNEL_DISABLED = -2147483648; // 0x80000000
field public static final int STATE_DISABLED = 0; // 0x0
field public static final int STATE_DISABLING = 2; // 0x2
field public static final int STATE_ENABLED = 1; // 0x1
@@ -527,6 +553,7 @@
public static interface ThreadNetworkController.StateCallback {
method public void onDeviceRoleChanged(int);
+ method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public default void onEphemeralKeyStateChanged(int, @Nullable String, @Nullable java.time.Instant);
method public default void onPartitionIdChanged(long);
method public default void onThreadEnableStateChanged(int);
}
@@ -546,6 +573,7 @@
field public static final int ERROR_UNAVAILABLE = 4; // 0x4
field public static final int ERROR_UNKNOWN = 11; // 0xb
field public static final int ERROR_UNSUPPORTED_CHANNEL = 7; // 0x7
+ field public static final int ERROR_UNSUPPORTED_FEATURE = 13; // 0xd
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkManager {
diff --git a/framework-t/src/android/net/INetworkStatsService.aidl b/framework-t/src/android/net/INetworkStatsService.aidl
index 7f0c1fe..b459a13 100644
--- a/framework-t/src/android/net/INetworkStatsService.aidl
+++ b/framework-t/src/android/net/INetworkStatsService.aidl
@@ -21,10 +21,11 @@
import android.net.Network;
import android.net.NetworkStateSnapshot;
import android.net.NetworkStats;
-import android.net.NetworkStatsHistory;
import android.net.NetworkTemplate;
import android.net.UnderlyingNetworkInfo;
import android.net.netstats.IUsageCallback;
+import android.net.netstats.StatsResult;
+import android.net.netstats.TrafficStatsRateLimitCacheConfig;
import android.net.netstats.provider.INetworkStatsProvider;
import android.net.netstats.provider.INetworkStatsProviderCallback;
import android.os.IBinder;
@@ -78,13 +79,13 @@
void unregisterUsageRequest(in DataUsageRequest request);
/** Get the uid stats information since boot */
- long getUidStats(int uid, int type);
+ StatsResult getUidStats(int uid);
/** Get the iface stats information since boot */
- long getIfaceStats(String iface, int type);
+ StatsResult getIfaceStats(String iface);
/** Get the total network stats information since boot */
- long getTotalStats(int type);
+ StatsResult getTotalStats();
/** Registers a network stats provider */
INetworkStatsProviderCallback registerNetworkStatsProvider(String tag,
@@ -104,4 +105,7 @@
/** Clear TrafficStats rate-limit caches. */
void clearTrafficStatsRateLimitCaches();
+
+ /** Get rate-limit cache config. */
+ TrafficStatsRateLimitCacheConfig getRateLimitCacheConfig();
}
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 3f74e1c..39e2b5b 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -65,13 +65,6 @@
public class IpSecManager {
private static final String TAG = "IpSecManager";
- // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
- // available here
- /** @hide */
- public static class Flags {
- static final String IPSEC_TRANSFORM_STATE = "com.android.net.flags.ipsec_transform_state";
- }
-
/**
* Feature flag to declare the kernel support of updating IPsec SAs.
*
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index 70c9bc8..35bd008 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -15,7 +15,6 @@
*/
package android.net;
-import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
import static android.net.IpSecManager.INVALID_RESOURCE_ID;
import android.annotation.CallbackExecutor;
@@ -35,6 +34,7 @@
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.flags.Flags;
import dalvik.system.CloseGuard;
@@ -220,7 +220,7 @@
* occurs.
* @see IpSecTransformState
*/
- @FlaggedApi(IPSEC_TRANSFORM_STATE)
+ @FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
public void requestIpSecTransformState(
@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
index 5b80ae2..b6628ee 100644
--- a/framework-t/src/android/net/IpSecTransformState.java
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -15,8 +15,6 @@
*/
package android.net;
-import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
-
import static com.android.internal.annotations.VisibleForTesting.Visibility;
import android.annotation.FlaggedApi;
@@ -26,6 +24,7 @@
import android.os.SystemClock;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.flags.Flags;
import com.android.net.module.util.HexDump;
import java.util.Objects;
@@ -39,7 +38,7 @@
* IpSecTransformStates at two timestamps. By comparing the changes in packet counts and sequence
* numbers, callers can estimate IPsec data loss in the inbound direction.
*/
-@FlaggedApi(IPSEC_TRANSFORM_STATE)
+@FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
public final class IpSecTransformState implements Parcelable {
private final long mTimestamp;
private final long mTxHighestSequenceNumber;
@@ -197,7 +196,7 @@
* <p>Except for testing, IPsec callers normally do not instantiate {@link IpSecTransformState}
* themselves but instead get a reference via {@link IpSecTransformState}
*/
- @FlaggedApi(IPSEC_TRANSFORM_STATE)
+ @FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
public static final class Builder {
private long mTimestamp;
private long mTxHighestSequenceNumber;
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
index e9a3f58..a2c4fc3 100644
--- a/framework-t/src/android/net/NetworkStats.java
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -18,6 +18,7 @@
import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -33,6 +34,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.net.module.util.CollectionUtils;
import libcore.util.EmptyArray;
@@ -845,6 +847,21 @@
}
/**
+ * Adds multiple entries to a copy of this NetworkStats instance.
+ *
+ * @param entries The entries to add.
+ * @return A new NetworkStats instance with the added entries.
+ */
+ @FlaggedApi(Flags.FLAG_NETSTATS_ADD_ENTRIES)
+ public @NonNull NetworkStats addEntries(@NonNull final List<Entry> entries) {
+ final NetworkStats newStats = this.clone();
+ for (final Entry entry : Objects.requireNonNull(entries)) {
+ newStats.combineValues(entry);
+ }
+ return newStats;
+ }
+
+ /**
* Add given values with an existing row, or create a new row if
* {@link #findIndex(String, int, int, int, int, int, int)} is unable to find match. Can
* also be used to subtract values from existing rows.
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index 77c8001..81f2cf9 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -18,7 +18,10 @@
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
@@ -29,18 +32,23 @@
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.media.MediaPlayer;
+import android.net.netstats.StatsResult;
import android.os.Binder;
import android.os.Build;
import android.os.RemoteException;
import android.os.StrictMode;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.Socket;
import java.net.SocketException;
+
/**
* Class that provides network traffic statistics. These statistics include
* bytes transmitted and received and network packets transmitted and received,
@@ -174,10 +182,16 @@
/** @hide */
public static final int TAG_SYSTEM_PROBE = 0xFFFFFF42;
+ @GuardedBy("TrafficStats.class")
private static INetworkStatsService sStatsService;
+ // The variable will only be accessed in the test, which is effectively
+ // single-threaded.
+ private static INetworkStatsService sStatsServiceForTest = null;
+
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
private synchronized static INetworkStatsService getStatsService() {
+ if (sStatsServiceForTest != null) return sStatsServiceForTest;
if (sStatsService == null) {
throw new IllegalStateException("TrafficStats not initialized, uid="
+ Binder.getCallingUid());
@@ -185,6 +199,21 @@
return sStatsService;
}
+ /** @hide */
+ private static int getMyUid() {
+ return android.os.Process.myUid();
+ }
+
+ /**
+ * Set the network stats service for testing, or null to reset.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public static void setServiceForTest(INetworkStatsService statsService) {
+ sStatsServiceForTest = statsService;
+ }
+
/**
* Snapshot of {@link NetworkStats} when the currently active profiling
* session started, or {@code null} if no session active.
@@ -239,8 +268,8 @@
private static class SocketTagger extends dalvik.system.SocketTagger {
- // TODO: set to false
- private static final boolean LOGD = true;
+ // Enable log with `setprop log.tag.TrafficStats DEBUG` and restart the module.
+ private static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
SocketTagger() {
}
@@ -447,7 +476,7 @@
*/
@Deprecated
public static void setThreadStatsUidSelf() {
- setThreadStatsUid(android.os.Process.myUid());
+ setThreadStatsUid(getMyUid());
}
/**
@@ -588,7 +617,7 @@
* @param operationCount Number of operations to increment count by.
*/
public static void incrementOperationCount(int tag, int operationCount) {
- final int uid = android.os.Process.myUid();
+ final int uid = getMyUid();
try {
getStatsService().incrementOperationCount(uid, tag, operationCount);
} catch (RemoteException e) {
@@ -730,11 +759,7 @@
* @return The number of transmitted packets.
*/
public static long getTxPackets(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_TX_PACKETS);
}
/**
@@ -753,11 +778,7 @@
* @return The number of received packets.
*/
public static long getRxPackets(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_RX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_RX_PACKETS);
}
/**
@@ -776,11 +797,7 @@
* @return The number of transmitted bytes.
*/
public static long getTxBytes(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_TX_BYTES);
}
/**
@@ -799,51 +816,31 @@
* @return The number of received bytes.
*/
public static long getRxBytes(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_RX_BYTES);
}
/** {@hide} */
@TestApi
public static long getLoopbackTxPackets() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS);
}
/** {@hide} */
@TestApi
public static long getLoopbackRxPackets() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS);
}
/** {@hide} */
@TestApi
public static long getLoopbackTxBytes() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES);
}
/** {@hide} */
@TestApi
public static long getLoopbackRxBytes() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES);
}
/**
@@ -856,11 +853,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalTxPackets() {
- try {
- return getStatsService().getTotalStats(TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_TX_PACKETS);
}
/**
@@ -873,11 +866,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalRxPackets() {
- try {
- return getStatsService().getTotalStats(TYPE_RX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_RX_PACKETS);
}
/**
@@ -890,11 +879,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalTxBytes() {
- try {
- return getStatsService().getTotalStats(TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_TX_BYTES);
}
/**
@@ -907,11 +892,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalRxBytes() {
- try {
- return getStatsService().getTotalStats(TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_RX_BYTES);
}
/**
@@ -933,11 +914,7 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidTxBytes(int uid) {
- try {
- return getStatsService().getUidStats(uid, TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getUidStats(uid, TYPE_TX_BYTES);
}
/**
@@ -959,11 +936,7 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidRxBytes(int uid) {
- try {
- return getStatsService().getUidStats(uid, TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getUidStats(uid, TYPE_RX_BYTES);
}
/**
@@ -985,11 +958,7 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidTxPackets(int uid) {
- try {
- return getStatsService().getUidStats(uid, TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getUidStats(uid, TYPE_TX_PACKETS);
}
/**
@@ -1011,11 +980,40 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidRxPackets(int uid) {
+ return getUidStats(uid, TYPE_RX_PACKETS);
+ }
+
+ /** @hide */
+ public static long getUidStats(int uid, int type) {
+ final StatsResult stats;
try {
- return getStatsService().getUidStats(uid, TYPE_RX_PACKETS);
+ stats = getStatsService().getUidStats(uid);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
+ return getEntryValueForType(stats, type);
+ }
+
+ /** @hide */
+ public static long getTotalStats(int type) {
+ final StatsResult stats;
+ try {
+ stats = getStatsService().getTotalStats();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return getEntryValueForType(stats, type);
+ }
+
+ /** @hide */
+ public static long getIfaceStats(String iface, int type) {
+ final StatsResult stats;
+ try {
+ stats = getStatsService().getIfaceStats(iface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return getEntryValueForType(stats, type);
}
/**
@@ -1112,7 +1110,7 @@
*/
private static NetworkStats getDataLayerSnapshotForUid(Context context) {
// TODO: take snapshot locally, since proc file is now visible
- final int uid = android.os.Process.myUid();
+ final int uid = getMyUid();
try {
return getStatsService().getDataLayerSnapshotForUid(uid);
} catch (RemoteException e) {
@@ -1143,4 +1141,37 @@
public static final int TYPE_TX_BYTES = 2;
/** {@hide} */
public static final int TYPE_TX_PACKETS = 3;
+
+ /** @hide */
+ private static long getEntryValueForType(@Nullable StatsResult stats, int type) {
+ if (stats == null) return UNSUPPORTED;
+ if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
+ switch (type) {
+ case TYPE_RX_BYTES:
+ return stats.rxBytes;
+ case TYPE_RX_PACKETS:
+ return stats.rxPackets;
+ case TYPE_TX_BYTES:
+ return stats.txBytes;
+ case TYPE_TX_PACKETS:
+ return stats.txPackets;
+ default:
+ throw new IllegalStateException("Bug: Invalid type: "
+ + type + " should not reach here.");
+ }
+ }
+
+ /** @hide */
+ private static boolean isEntryValueTypeValid(int type) {
+ switch (type) {
+ case TYPE_RX_BYTES:
+ case TYPE_RX_PACKETS:
+ case TYPE_TX_BYTES:
+ case TYPE_TX_PACKETS:
+ return true;
+ default :
+ return false;
+ }
+ }
}
+
diff --git a/framework-t/src/android/net/netstats/StatsResult.aidl b/framework-t/src/android/net/netstats/StatsResult.aidl
new file mode 100644
index 0000000..3f09566
--- /dev/null
+++ b/framework-t/src/android/net/netstats/StatsResult.aidl
@@ -0,0 +1,31 @@
+/**
+ * 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 android.net.netstats;
+
+/**
+ * A lightweight class to pass result of TrafficStats#get{Total|Iface|Uid}Stats.
+ *
+ * @hide
+ */
+@JavaDerive(equals=true, toString=true)
+@JavaOnlyImmutable
+parcelable StatsResult {
+ long rxBytes;
+ long rxPackets;
+ long txBytes;
+ long txPackets;
+}
\ No newline at end of file
diff --git a/framework-t/src/android/net/netstats/TrafficStatsRateLimitCacheConfig.aidl b/framework-t/src/android/net/netstats/TrafficStatsRateLimitCacheConfig.aidl
new file mode 100644
index 0000000..cdf0b7c
--- /dev/null
+++ b/framework-t/src/android/net/netstats/TrafficStatsRateLimitCacheConfig.aidl
@@ -0,0 +1,42 @@
+/**
+ * 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 android.net.netstats;
+
+/**
+ * Configuration for the TrafficStats rate limit cache.
+ *
+ * @hide
+ */
+@JavaDerive(equals=true, toString=true)
+@JavaOnlyImmutable
+parcelable TrafficStatsRateLimitCacheConfig {
+
+ /**
+ * Whether the cache is enabled for V+ device or target Sdk V+ apps.
+ */
+ boolean isCacheEnabled;
+
+ /**
+ * The duration for which cache entries are valid, in milliseconds.
+ */
+ int expiryDurationMs;
+
+ /**
+ * The maximum number of entries to store in the cache.
+ */
+ int maxEntries;
+}
diff --git a/framework-t/src/android/net/nsd/DiscoveryRequest.java b/framework-t/src/android/net/nsd/DiscoveryRequest.java
index b0b71ea..b344943 100644
--- a/framework-t/src/android/net/nsd/DiscoveryRequest.java
+++ b/framework-t/src/android/net/nsd/DiscoveryRequest.java
@@ -24,12 +24,14 @@
import android.os.Parcelable;
import android.text.TextUtils;
+import com.android.net.flags.Flags;
+
import java.util.Objects;
/**
* Encapsulates parameters for {@link NsdManager#discoverServices}.
*/
-@FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+@FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
public final class DiscoveryRequest implements Parcelable {
private final int mProtocolType;
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index b21e22a..116bea6 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -52,6 +52,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.net.module.util.CollectionUtils;
import java.lang.annotation.Retention;
@@ -148,22 +149,6 @@
private static final String TAG = NsdManager.class.getSimpleName();
private static final boolean DBG = false;
- // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
- // available here
- /** @hide */
- public static class Flags {
- static final String REGISTER_NSD_OFFLOAD_ENGINE_API =
- "com.android.net.flags.register_nsd_offload_engine_api";
- static final String NSD_SUBTYPES_SUPPORT_ENABLED =
- "com.android.net.flags.nsd_subtypes_support_enabled";
- static final String ADVERTISE_REQUEST_API =
- "com.android.net.flags.advertise_request_api";
- static final String NSD_CUSTOM_HOSTNAME_ENABLED =
- "com.android.net.flags.nsd_custom_hostname_enabled";
- static final String NSD_CUSTOM_TTL_ENABLED =
- "com.android.net.flags.nsd_custom_ttl_enabled";
- }
-
/**
* A regex for the acceptable format of a type or subtype label.
* @hide
@@ -451,7 +436,7 @@
*
* @hide
*/
- @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
+ @FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
@SystemApi
@RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
NETWORK_STACK})
@@ -489,7 +474,7 @@
*
* @hide
*/
- @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
+ @FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
@SystemApi
@RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
NETWORK_STACK})
@@ -1506,7 +1491,7 @@
* @param listener The listener notifies of a successful discovery and is used
* to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
*/
- @FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
public void discoverServices(@NonNull DiscoveryRequest discoveryRequest,
@NonNull Executor executor, @NonNull DiscoveryListener listener) {
int key = putListener(listener, executor, discoveryRequest);
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index d8cccb2..18c59d9 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -30,6 +30,7 @@
import android.util.ArraySet;
import android.util.Log;
+import com.android.net.flags.Flags;
import com.android.net.module.util.InetAddressUtils;
import java.io.UnsupportedEncodingException;
@@ -527,7 +528,7 @@
* Only one subtype will be registered if multiple elements of {@code subtypes} have the same
* case-insensitive value.
*/
- @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
public void setSubtypes(@NonNull Set<String> subtypes) {
mSubtypes.clear();
mSubtypes.addAll(subtypes);
@@ -540,7 +541,7 @@
* NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this
* service.
*/
- @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
@NonNull
public Set<String> getSubtypes() {
return Collections.unmodifiableSet(mSubtypes);
diff --git a/framework-t/src/android/net/nsd/OffloadEngine.java b/framework-t/src/android/net/nsd/OffloadEngine.java
index 9015985..06655fa 100644
--- a/framework-t/src/android/net/nsd/OffloadEngine.java
+++ b/framework-t/src/android/net/nsd/OffloadEngine.java
@@ -21,6 +21,8 @@
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import com.android.net.flags.Flags;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -34,7 +36,7 @@
*
* @hide
*/
-@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
+@FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
@SystemApi
public interface OffloadEngine {
/**
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index 98dc83a..e4b2f43 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -26,6 +26,7 @@
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.net.flags.Flags;
import com.android.net.module.util.HexDump;
import java.util.Arrays;
@@ -40,7 +41,7 @@
*
* @hide
*/
-@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
+@FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
@SystemApi
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public final class OffloadServiceInfo implements Parcelable {
diff --git a/framework/Android.bp b/framework/Android.bp
index 4eda0aa..a93a532 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -63,6 +63,8 @@
":framework-connectivity-sources",
":net-utils-framework-common-srcs",
":framework-connectivity-api-shared-srcs",
+ ":framework-networksecurity-sources",
+ ":statslog-framework-connectivity-java-gen",
],
aidl: {
generate_get_transaction_name: true,
@@ -86,12 +88,14 @@
"framework-wifi.stubs.module_lib",
],
static_libs: [
+ "com.android.net.flags-aconfig-java",
// Not using the latest stable version because all functions in the latest version of
// mdns_aidl_interface are deprecated.
"mdns_aidl_interface-V1-java",
"modules-utils-backgroundthread",
"modules-utils-build",
"modules-utils-preconditions",
+ "networksecurity_flags_java_lib",
"framework-connectivity-javastream-protos",
],
impl_only_static_libs: [
@@ -101,6 +105,7 @@
"androidx.annotation_annotation",
"app-compat-annotations",
"framework-connectivity-t.stubs.module_lib",
+ "framework-statsd.stubs.module_lib",
"unsupportedappusage",
],
apex_available: [
@@ -108,14 +113,13 @@
],
}
+// Library to allow Cronet to use hidden APIs
java_library {
- name: "framework-connectivity-pre-jarjar",
+ name: "framework-connectivity-pre-jarjar-without-cronet",
defaults: [
"framework-connectivity-defaults",
],
static_libs: [
- "httpclient_api",
- "httpclient_impl",
// Framework-connectivity-pre-jarjar is identical to framework-connectivity
// implementation, but without the jarjar rules. However, framework-connectivity
// is not based on framework-connectivity-pre-jarjar, it's rebuilt from source
@@ -133,12 +137,28 @@
"framework-tethering.impl",
"framework-wifi.stubs.module_lib",
],
+ visibility: ["//external/cronet:__subpackages__"],
+}
+
+java_library {
+ name: "framework-connectivity-pre-jarjar",
+ defaults: ["framework-module-defaults"],
+ min_sdk_version: "30",
+ static_libs: [
+ "framework-connectivity-pre-jarjar-without-cronet",
+ "httpclient_api",
+ "httpclient_impl",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ ],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
java_defaults {
name: "CronetJavaDefaults",
srcs: [":httpclient_api_sources"],
+ static_libs: ["com.android.net.http.flags-aconfig-java"],
libs: [
"androidx.annotation_annotation",
],
@@ -161,13 +181,19 @@
// In preparation for future move
"//packages/modules/Connectivity/apex",
"//packages/modules/Connectivity/framework-t",
+ "//packages/modules/Connectivity/remoteauth/service",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service-t",
+ "//packages/modules/Connectivity/staticlibs",
"//frameworks/base",
// Tests using hidden APIs
"//cts/tests/netlegacy22.api",
"//cts/tests/tests/app.usage", // NetworkUsageStatsTest
+
+ // TODO: b/374174952 Remove it when VCN CTS is moved to Connectivity/
+ "//cts/tests/tests/vcn",
+
"//external/sl4a:__subpackages__",
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
@@ -184,6 +210,7 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
+ "//packages/modules/NetworkStack",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
@@ -192,6 +219,8 @@
},
aconfig_declarations: [
"com.android.net.flags-aconfig",
+ "com.android.net.http.flags-aconfig",
+ "com.android.networksecurity.flags-aconfig",
],
}
@@ -292,6 +321,7 @@
srcs: [
":framework-connectivity-sources",
":framework-connectivity-tiramisu-updatable-sources",
+ ":framework-networksecurity-sources",
":framework-nearby-java-sources",
":framework-thread-sources",
],
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index cd7307f..0129e5c 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -247,3 +247,11 @@
}
+package android.net.http {
+
+ public abstract class HttpEngine {
+ method @FlaggedApi("android.net.http.preload_httpengine_in_zygote") public static void preload();
+ }
+
+}
+
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 282a11e..1760fa7 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -261,6 +261,12 @@
IBpfMap<S32, UidOwnerValue> uidOwnerMap,
IBpfMap<S32, U8> dataSaverEnabledMap
) {
+ // System uids are not blocked by firewall chains, see bpf_progs/netd.c
+ // TODO: b/348513058 - use UserHandle.isCore() once it is accessible
+ if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
+ return BLOCKED_REASON_NONE;
+ }
+
final long uidRuleConfig;
final long uidMatch;
try {
@@ -331,12 +337,6 @@
) {
throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
- // System uids are not blocked by firewall chains, see bpf_progs/netd.c
- // TODO: b/348513058 - use UserHandle.isCore() once it is accessible
- if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
- return false;
- }
-
final int blockedReasons = getUidNetworkingBlockedReasons(
uid,
configurationMap,
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index a6a967b..1ebc4a3 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -78,6 +78,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import libcore.net.event.NetworkEventDispatcher;
@@ -125,24 +126,6 @@
private static final String TAG = "ConnectivityManager";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
- // available here
- /** @hide */
- public static class Flags {
- static final String SET_DATA_SAVER_VIA_CM =
- "com.android.net.flags.set_data_saver_via_cm";
- static final String SUPPORT_IS_UID_NETWORKING_BLOCKED =
- "com.android.net.flags.support_is_uid_networking_blocked";
- static final String BASIC_BACKGROUND_RESTRICTIONS_ENABLED =
- "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";
- static final String BLOCKED_REASON_NETWORK_RESTRICTED =
- "com.android.net.flags.blocked_reason_network_restricted";
- }
-
/**
* A change in network connectivity has occurred. A default connection has either
* been established or lost. The NetworkInfo for the affected network is
@@ -919,7 +902,7 @@
*
* @hide
*/
- @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+ @FlaggedApi(Flags.FLAG_BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final int BLOCKED_REASON_APP_BACKGROUND = 1 << 6;
@@ -932,7 +915,7 @@
* @see #FIREWALL_CHAIN_OEM_DENY_3
* @hide
*/
- @FlaggedApi(Flags.BLOCKED_REASON_OEM_DENY_CHAINS)
+ @FlaggedApi(Flags.FLAG_BLOCKED_REASON_OEM_DENY_CHAINS)
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final int BLOCKED_REASON_OEM_DENY = 1 << 7;
@@ -943,7 +926,7 @@
*
* @hide
*/
- @FlaggedApi(Flags.BLOCKED_REASON_NETWORK_RESTRICTED)
+ @FlaggedApi(Flags.FLAG_BLOCKED_REASON_NETWORK_RESTRICTED)
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final int BLOCKED_REASON_NETWORK_RESTRICTED = 1 << 8;
@@ -1052,7 +1035,7 @@
* exempted for specific situations while in the background.
* @hide
*/
- @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+ @FlaggedApi(Flags.FLAG_BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_BACKGROUND = 6;
@@ -1120,7 +1103,7 @@
* @hide
*/
// TODO: Merge this chain with data saver and support setFirewallChainEnabled
- @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+ @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_METERED_ALLOW = 10;
@@ -1139,7 +1122,7 @@
* @hide
*/
// TODO: Support setFirewallChainEnabled to control this chain
- @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+ @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_METERED_DENY_USER = 11;
@@ -1158,7 +1141,7 @@
* @hide
*/
// TODO: Support setFirewallChainEnabled to control this chain
- @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+ @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_METERED_DENY_ADMIN = 12;
@@ -4163,6 +4146,8 @@
*/
@FilteredCallback(methodId = METHOD_ONAVAILABLE_5ARGS,
calledByCallbackId = CALLBACK_AVAILABLE,
+ // If this list is modified, ConnectivityService#addAvailableStateUpdateCallbacks
+ // needs to be updated too.
mayCall = { METHOD_ONAVAILABLE_4ARGS,
METHOD_ONLOCALNETWORKINFOCHANGED,
METHOD_ONBLOCKEDSTATUSCHANGED_INT })
@@ -4193,6 +4178,8 @@
*/
@FilteredCallback(methodId = METHOD_ONAVAILABLE_4ARGS,
calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY,
+ // If this list is modified, ConnectivityService#addAvailableStateUpdateCallbacks
+ // needs to be updated too.
mayCall = { METHOD_ONAVAILABLE_1ARG,
METHOD_ONNETWORKSUSPENDED,
METHOD_ONCAPABILITIESCHANGED,
@@ -6449,7 +6436,7 @@
* @throws IllegalStateException if failed.
* @hide
*/
- @FlaggedApi(Flags.SET_DATA_SAVER_VIA_CM)
+ @FlaggedApi(Flags.FLAG_SET_DATA_SAVER_VIA_CM)
@SystemApi(client = MODULE_LIBRARIES)
@RequiresPermission(anyOf = {
android.Manifest.permission.NETWORK_SETTINGS,
@@ -6710,7 +6697,7 @@
// is provided by linux file group permission AID_NET_BW_ACCT and the
// selinux context fs_bpf_net*.
// Only the system server process and the network stack have access.
- @FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
+ @FlaggedApi(Flags.FLAG_SUPPORT_IS_UID_NETWORKING_BLOCKED)
@SystemApi(client = MODULE_LIBRARIES)
// Note b/326143935 kernel bug can trigger crash on some T device.
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -6747,4 +6734,33 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Get the specified ConnectivityService feature status. This method is for test code to check
+ * whether the feature is enabled or not.
+ * Note that tests can not just read DeviceConfig since ConnectivityService reads flag at
+ * startup. For example, it's possible that the current flag value is "disable"(-1) but the
+ * feature is enabled since the flag value was "enable"(1) when ConnectivityService started up.
+ * If the ConnectivityManager needs to check the ConnectivityService feature status for non-test
+ * purpose, define feature in {@link ConnectivityManagerFeature} and use
+ * {@link #isFeatureEnabled} instead.
+ *
+ * @param featureFlag target flag for feature
+ * @return {@code true} if the feature is enabled, {@code false} if the feature is disabled.
+ * @throws IllegalArgumentException if the flag is invalid
+ *
+ * @hide
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+ })
+ public boolean isConnectivityServiceFeatureEnabledForTesting(final String featureFlag) {
+ try {
+ return mService.isConnectivityServiceFeatureEnabledForTesting(featureFlag);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 988cc92..47b3316 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -262,4 +262,6 @@
IBinder getRoutingCoordinatorService();
long getEnabledConnectivityManagerFeatures();
+
+ boolean isConnectivityServiceFeatureEnabledForTesting(String featureFlag);
}
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 574ab2f..cefa1ea 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -31,12 +31,14 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
import android.telephony.data.EpsBearerQosSessionAttributes;
import android.telephony.data.NrQosSessionAttributes;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.FrameworkConnectivityStatsLog;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -943,6 +945,19 @@
private void queueOrSendMessage(@NonNull RegistryAction action) {
synchronized (mPreConnectedQueue) {
+ if (mNetwork == null && !Process.isApplicationUid(Process.myUid())) {
+ // Theoretically, it should not be valid to queue messages here before
+ // registering the NetworkAgent. However, practically, with the way
+ // queueing works right now, it ends up working out just fine.
+ // Log a statistic so that we know if this is happening in the
+ // wild. The check for isApplicationUid is to prevent logging the
+ // metric from test code.
+
+ FrameworkConnectivityStatsLog.write(
+ FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_MESSAGE_QUEUED_BEFORE_CONNECT
+ );
+ }
if (mRegistry != null) {
try {
action.execute(mRegistry);
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 6a14bde..4a50397 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -41,6 +41,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.net.module.util.BitUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.NetworkCapabilitiesUtils;
@@ -124,22 +125,6 @@
public final class NetworkCapabilities implements Parcelable {
private static final String TAG = "NetworkCapabilities";
- // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
- // available here
- /** @hide */
- public static class Flags {
- static final String FLAG_FORBIDDEN_CAPABILITY =
- "com.android.net.flags.forbidden_capability";
- static final String FLAG_NET_CAPABILITY_LOCAL_NETWORK =
- "com.android.net.flags.net_capability_local_network";
- static final String REQUEST_RESTRICTED_WIFI =
- "com.android.net.flags.request_restricted_wifi";
- static final String SUPPORT_TRANSPORT_SATELLITE =
- "com.android.net.flags.support_transport_satellite";
- static final String NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED =
- "com.android.net.flags.net_capability_not_bandwidth_constrained";
- }
-
/**
* Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific
* app permissions.
@@ -761,7 +746,7 @@
* usage on constrained networks, such as disabling network access to apps that are not in the
* foreground.
*/
- @FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+ @FlaggedApi(Flags.FLAG_NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
public static final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37;
private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
@@ -1374,7 +1359,7 @@
/**
* Indicates this network uses a Satellite transport.
*/
- @FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE)
+ @FlaggedApi(Flags.FLAG_SUPPORT_TRANSPORT_SATELLITE)
public static final int TRANSPORT_SATELLITE = 10;
/** @hide */
@@ -2864,7 +2849,7 @@
* @return
*/
@NonNull
- @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+ @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
public Set<Integer> getSubscriptionIds() {
return new ArraySet<>(mSubIds);
}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 502ac6f..89572b3 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -50,6 +50,8 @@
import android.text.TextUtils;
import android.util.Range;
+import com.android.net.flags.Flags;
+
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -144,12 +146,6 @@
* Look up the specific capability to learn whether its usage requires this self-certification.
*/
public class NetworkRequest implements Parcelable {
-
- /** @hide */
- public static class Flags {
- static final String REQUEST_RESTRICTED_WIFI =
- "com.android.net.flags.request_restricted_wifi";
- }
/**
* The first requestId value that will be allocated.
* @hide only used by ConnectivityService.
@@ -616,7 +612,7 @@
* @param subIds A {@code Set} that represents subscription IDs.
*/
@NonNull
- @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+ @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
public Builder setSubscriptionIds(@NonNull Set<Integer> subIds) {
mNetworkCapabilities.setSubscriptionIds(subIds);
return this;
@@ -880,7 +876,7 @@
* @return Set of Integer values for this instance.
*/
@NonNull
- @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+ @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
public Set<Integer> getSubscriptionIds() {
// No need to make a defensive copy here as NC#getSubscriptionIds() already returns
// a new set.
diff --git a/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
index 6e87ed3..ba39ca0 100644
--- a/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
+++ b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
@@ -24,8 +24,8 @@
import androidx.annotation.RequiresApi;
/**
- * Utility providing limited access to module-internal APIs which are only available on Android T+,
- * as this class is only in the bootclasspath on T+ as part of framework-connectivity.
+ * Utility providing limited access to module-internal APIs which are only available on Android S+,
+ * as this class is only in the bootclasspath on S+ as part of framework-connectivity.
*
* R+ module components like Tethering cannot depend on all hidden symbols from
* framework-connectivity. They only have access to stable API stubs where newer APIs can be
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index 41a28a0..6bfa54d 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -49,11 +49,12 @@
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-bluetooth",
+ "framework-bluetooth.stubs.module_lib",
"framework-location.stubs.module_lib",
],
static_libs: [
"modules-utils-preconditions",
+ "com.android.nearby.flags-aconfig-java",
],
visibility: [
"//packages/modules/Connectivity/nearby/tests:__subpackages__",
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index cae653d..e78f999 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -32,11 +32,11 @@
import android.nearby.aidl.IOffloadCallback;
import android.os.RemoteException;
import android.os.SystemProperties;
-import android.provider.Settings;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
+import com.android.nearby.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -84,7 +84,7 @@
* Return value of {@link #getPoweredOffFindingMode()} when this powered off finding is not
* supported the device.
*/
- @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0;
/**
@@ -92,7 +92,7 @@
* #setPoweredOffFindingMode(int)} when powered off finding is supported but disabled. The
* device will not start to advertise when powered off.
*/
- @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1;
/**
@@ -100,7 +100,7 @@
* #setPoweredOffFindingMode(int)} when powered off finding is enabled. The device will start to
* advertise when powered off.
*/
- @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2;
/**
@@ -122,18 +122,11 @@
private static final int POWERED_OFF_FINDING_EID_LENGTH = 20;
- private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY =
+ private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY_RO =
"ro.bluetooth.finder.supported";
- /**
- * TODO(b/286137024): Remove this when CTS R5 is rolled out.
- * Whether allows Fast Pair to scan.
- *
- * (0 = disabled, 1 = enabled)
- *
- * @hide
- */
- public static final String FAST_PAIR_SCAN_ENABLED = "fast_pair_scan_enabled";
+ private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY_PERSIST =
+ "persist.bluetooth.finder.supported";
@GuardedBy("sScanListeners")
private static final WeakHashMap<ScanCallback, WeakReference<ScanListenerTransport>>
@@ -475,36 +468,6 @@
}
/**
- * TODO(b/286137024): Remove this when CTS R5 is rolled out.
- * Read from {@link Settings} whether Fast Pair scan is enabled.
- *
- * @param context the {@link Context} to query the setting
- * @return whether the Fast Pair is enabled
- * @hide
- */
- public static boolean getFastPairScanEnabled(@NonNull Context context) {
- final int enabled = Settings.Secure.getInt(
- context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, 0);
- return enabled != 0;
- }
-
- /**
- * TODO(b/286137024): Remove this when CTS R5 is rolled out.
- * Write into {@link Settings} whether Fast Pair scan is enabled
- *
- * @param context the {@link Context} to set the setting
- * @param enable whether the Fast Pair scan should be enabled
- * @hide
- */
- @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
- public static void setFastPairScanEnabled(@NonNull Context context, boolean enable) {
- Settings.Secure.putInt(
- context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, enable ? 1 : 0);
- Log.v(TAG, String.format(
- "successfully %s Fast Pair scan", enable ? "enables" : "disables"));
- }
-
- /**
* Sets the precomputed EIDs for advertising when the phone is powered off. The Bluetooth
* controller will store these EIDs in its memory, and will start advertising them in Find My
* Device network EID frames when powered off, only if the powered off finding mode was
@@ -526,7 +489,7 @@
*
* @throws IllegalArgumentException if the length of one of the EIDs is not 20 bytes
*/
- @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public void setPoweredOffFindingEphemeralIds(@NonNull List<byte[]> eids) {
Objects.requireNonNull(eids);
@@ -570,7 +533,7 @@
* @throws IllegalStateException if called with {@link #POWERED_OFF_FINDING_MODE_ENABLED} when
* Bluetooth or location services are disabled
*/
- @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public void setPoweredOffFindingMode(@PoweredOffFindingMode int poweredOffFindingMode) {
Preconditions.checkArgument(
@@ -602,7 +565,7 @@
* #POWERED_OFF_FINDING_MODE_ENABLED} if this was the last value set by {@link
* #setPoweredOffFindingMode(int)}
*/
- @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public @PoweredOffFindingMode int getPoweredOffFindingMode() {
if (!isPoweredOffFindingSupported()) {
@@ -617,7 +580,9 @@
}
private boolean isPoweredOffFindingSupported() {
- return Boolean.parseBoolean(SystemProperties.get(POWER_OFF_FINDING_SUPPORTED_PROPERTY));
+ return Boolean.parseBoolean(SystemProperties.get(POWER_OFF_FINDING_SUPPORTED_PROPERTY_RO))
+ || Boolean.parseBoolean(SystemProperties.get(
+ POWER_OFF_FINDING_SUPPORTED_PROPERTY_PERSIST));
}
private boolean areLocationAndBluetoothEnabled() {
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 749113d..c9c7b44 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -35,11 +35,11 @@
],
libs: [
"androidx.annotation_annotation",
- "framework-bluetooth",
+ "framework-bluetooth.stubs.module_lib",
"error_prone_annotations",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-t.impl",
- "framework-statsd",
+ "framework-statsd.stubs.module_lib",
],
static_libs: [
"androidx.core_core",
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 8009303..9d42dd1 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -30,9 +30,9 @@
"truth",
],
libs: [
- "android.test.base",
+ "android.test.base.stubs.system",
"framework-bluetooth.stubs.module_lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-t.impl",
"framework-location.stubs.module_lib",
],
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 1e36676..3291223 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
import static android.nearby.ScanCallback.ERROR_UNSUPPORTED;
@@ -121,7 +122,7 @@
@Before
public void setUp() {
mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG,
- BLUETOOTH_PRIVILEGED);
+ WRITE_ALLOWLISTED_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
String nameSpace = SdkLevel.isAtLeastU() ? DeviceConfig.NAMESPACE_NEARBY
: DeviceConfig.NAMESPACE_TETHERING;
DeviceConfig.setProperty(nameSpace,
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 2950568..4d2d1d5 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -27,9 +27,9 @@
srcs: ["src/**/*.java"],
libs: [
- "android.test.base",
- "android.test.mock",
- "android.test.runner",
+ "android.test.base.stubs.test",
+ "android.test.mock.stubs.test",
+ "android.test.runner.stubs.test",
],
compile_multilib: "both",
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
index 644e178..e0dfd31 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
import static com.android.server.nearby.NearbyConfiguration.NEARBY_MAINLINE_NANO_APP_MIN_VERSION;
@@ -42,7 +43,8 @@
@Before
public void setUp() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
- .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+ .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG,
+ READ_DEVICE_CONFIG);
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
index 5b640cc..891e941 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
@@ -71,7 +72,8 @@
when(mScanListener.asBinder()).thenReturn(mIBinder);
mUiAutomation.adoptShellPermissionIdentity(
- READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
+ READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG,
+ BLUETOOTH_PRIVILEGED);
mContext = InstrumentationRegistry.getInstrumentation().getContext();
mService = new NearbyService(mContext);
mScanRequest = createScanRequest();
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
index 7ff7b13..faa32c0 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
@@ -88,7 +89,8 @@
@Before
public void setUp() {
when(mBroadcastListener.asBinder()).thenReturn(mBinder);
- mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+ mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG, READ_DEVICE_CONFIG);
DeviceConfig.setProperty(
NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "true", false);
DeviceConfig.setProperty(
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
index ce479c8..01028bf 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
import static com.android.server.nearby.NearbyConfiguration.NEARBY_MAINLINE_NANO_APP_MIN_VERSION;
import static com.android.server.nearby.provider.ChreCommunication.INVALID_NANO_APP_VERSION;
@@ -76,7 +77,8 @@
@Before
public void setUp() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
- .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+ .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG,
+ READ_DEVICE_CONFIG);
DeviceConfig.setProperty(
NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION, "1", false);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 590a46e..7f391f1 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
@@ -84,7 +85,8 @@
@Before
public void setUp() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
- .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+ .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG,
+ READ_DEVICE_CONFIG);
MockitoAnnotations.initMocks(this);
Context context = InstrumentationRegistry.getInstrumentation().getContext();
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
deleted file mode 100644
index 0d4a5c4..0000000
--- a/netbpfload/NetBpfLoad.cpp
+++ /dev/null
@@ -1,539 +0,0 @@
-/*
- * Copyright (C) 2017-2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef LOG_TAG
-#define LOG_TAG "NetBpfLoad"
-#endif
-
-#include <arpa/inet.h>
-#include <dirent.h>
-#include <elf.h>
-#include <error.h>
-#include <fcntl.h>
-#include <inttypes.h>
-#include <linux/bpf.h>
-#include <linux/unistd.h>
-#include <net/if.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include <sys/mman.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-
-#include <android/api-level.h>
-#include <android-base/logging.h>
-#include <android-base/macros.h>
-#include <android-base/properties.h>
-#include <android-base/stringprintf.h>
-#include <android-base/strings.h>
-#include <android-base/unique_fd.h>
-#include <log/log.h>
-
-#include "BpfSyscallWrappers.h"
-#include "bpf/BpfUtils.h"
-#include "loader.h"
-
-namespace android {
-namespace bpf {
-
-using base::StartsWith;
-using base::EndsWith;
-using std::string;
-using std::vector;
-
-static bool exists(const char* const path) {
- int v = access(path, F_OK);
- if (!v) return true;
- if (errno == ENOENT) return false;
- ALOGE("FATAL: access(%s, F_OK) -> %d [%d:%s]", path, v, errno, strerror(errno));
- abort(); // can only hit this if permissions (likely selinux) are screwed up
-}
-
-
-const Location locations[] = {
- // S+ Tethering mainline module (network_stack): tether offload
- {
- .dir = "/apex/com.android.tethering/etc/bpf/",
- .prefix = "tethering/",
- },
- // T+ Tethering mainline module (shared with netd & system server)
- // netutils_wrapper (for iptables xt_bpf) has access to programs
- {
- .dir = "/apex/com.android.tethering/etc/bpf/netd_shared/",
- .prefix = "netd_shared/",
- },
- // T+ Tethering mainline module (shared with netd & system server)
- // netutils_wrapper has no access, netd has read only access
- {
- .dir = "/apex/com.android.tethering/etc/bpf/netd_readonly/",
- .prefix = "netd_readonly/",
- },
- // T+ Tethering mainline module (shared with system server)
- {
- .dir = "/apex/com.android.tethering/etc/bpf/net_shared/",
- .prefix = "net_shared/",
- },
- // T+ Tethering mainline module (not shared, just network_stack)
- {
- .dir = "/apex/com.android.tethering/etc/bpf/net_private/",
- .prefix = "net_private/",
- },
-};
-
-static int loadAllElfObjects(const unsigned int bpfloader_ver, const Location& location) {
- int retVal = 0;
- DIR* dir;
- struct dirent* ent;
-
- if ((dir = opendir(location.dir)) != NULL) {
- while ((ent = readdir(dir)) != NULL) {
- string s = ent->d_name;
- if (!EndsWith(s, ".o")) continue;
-
- string progPath(location.dir);
- progPath += s;
-
- bool critical;
- int ret = loadProg(progPath.c_str(), &critical, bpfloader_ver, location);
- if (ret) {
- if (critical) retVal = ret;
- ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
- } else {
- ALOGD("Loaded object: %s", progPath.c_str());
- }
- }
- closedir(dir);
- }
- return retVal;
-}
-
-static int createSysFsBpfSubDir(const char* const prefix) {
- if (*prefix) {
- mode_t prevUmask = umask(0);
-
- string s = "/sys/fs/bpf/";
- s += prefix;
-
- errno = 0;
- int ret = mkdir(s.c_str(), S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO);
- if (ret && errno != EEXIST) {
- const int err = errno;
- ALOGE("Failed to create directory: %s, ret: %s", s.c_str(), std::strerror(err));
- return -err;
- }
-
- umask(prevUmask);
- }
- return 0;
-}
-
-// Technically 'value' doesn't need to be newline terminated, but it's best
-// to include a newline to match 'echo "value" > /proc/sys/...foo' behaviour,
-// which is usually how kernel devs test the actual sysctl interfaces.
-static int writeProcSysFile(const char *filename, const char *value) {
- base::unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
- if (fd < 0) {
- const int err = errno;
- ALOGE("open('%s', O_WRONLY | O_CLOEXEC) -> %s", filename, strerror(err));
- return -err;
- }
- int len = strlen(value);
- int v = write(fd, value, len);
- if (v < 0) {
- const int err = errno;
- ALOGE("write('%s', '%s', %d) -> %s", filename, value, len, strerror(err));
- return -err;
- }
- if (v != len) {
- // In practice, due to us only using this for /proc/sys/... files, this can't happen.
- ALOGE("write('%s', '%s', %d) -> short write [%d]", filename, value, len, v);
- return -EINVAL;
- }
- return 0;
-}
-
-#define APEX_MOUNT_POINT "/apex/com.android.tethering"
-const char * const platformBpfLoader = "/system/bin/bpfloader";
-
-static int logTetheringApexVersion(void) {
- char * found_blockdev = NULL;
- FILE * f = NULL;
- char buf[4096];
-
- f = fopen("/proc/mounts", "re");
- if (!f) return 1;
-
- // /proc/mounts format: block_device [space] mount_point [space] other stuff... newline
- while (fgets(buf, sizeof(buf), f)) {
- char * blockdev = buf;
- char * space = strchr(blockdev, ' ');
- if (!space) continue;
- *space = '\0';
- char * mntpath = space + 1;
- space = strchr(mntpath, ' ');
- if (!space) continue;
- *space = '\0';
- if (strcmp(mntpath, APEX_MOUNT_POINT)) continue;
- found_blockdev = strdup(blockdev);
- break;
- }
- fclose(f);
- f = NULL;
-
- if (!found_blockdev) return 2;
- ALOGV("Found Tethering Apex mounted from blockdev %s", found_blockdev);
-
- f = fopen("/proc/mounts", "re");
- if (!f) { free(found_blockdev); return 3; }
-
- while (fgets(buf, sizeof(buf), f)) {
- char * blockdev = buf;
- char * space = strchr(blockdev, ' ');
- if (!space) continue;
- *space = '\0';
- char * mntpath = space + 1;
- space = strchr(mntpath, ' ');
- if (!space) continue;
- *space = '\0';
- if (strcmp(blockdev, found_blockdev)) continue;
- if (strncmp(mntpath, APEX_MOUNT_POINT "@", strlen(APEX_MOUNT_POINT "@"))) continue;
- char * at = strchr(mntpath, '@');
- if (!at) continue;
- char * ver = at + 1;
- ALOGI("Tethering APEX version %s", ver);
- }
- fclose(f);
- free(found_blockdev);
- return 0;
-}
-
-static bool hasGSM() {
- static string ph = base::GetProperty("gsm.current.phone-type", "");
- static bool gsm = (ph != "");
- static bool logged = false;
- if (!logged) {
- logged = true;
- ALOGI("hasGSM(gsm.current.phone-type='%s'): %s", ph.c_str(), gsm ? "true" : "false");
- }
- return gsm;
-}
-
-static bool isTV() {
- if (hasGSM()) return false; // TVs don't do GSM
-
- static string key = base::GetProperty("ro.oem.key1", "");
- static bool tv = StartsWith(key, "ATV00");
- static bool logged = false;
- if (!logged) {
- logged = true;
- ALOGI("isTV(ro.oem.key1='%s'): %s.", key.c_str(), tv ? "true" : "false");
- }
- return tv;
-}
-
-static bool isWear() {
- static string wearSdkStr = base::GetProperty("ro.cw_build.wear_sdk.version", "");
- static int wearSdkInt = base::GetIntProperty("ro.cw_build.wear_sdk.version", 0);
- static string buildChars = base::GetProperty("ro.build.characteristics", "");
- static vector<string> v = base::Tokenize(buildChars, ",");
- static bool watch = (std::find(v.begin(), v.end(), "watch") != v.end());
- static bool wear = (wearSdkInt > 0) || watch;
- static bool logged = false;
- if (!logged) {
- logged = true;
- ALOGI("isWear(ro.cw_build.wear_sdk.version=%d[%s] ro.build.characteristics='%s'): %s",
- wearSdkInt, wearSdkStr.c_str(), buildChars.c_str(), wear ? "true" : "false");
- }
- return wear;
-}
-
-static int doLoad(char** argv, char * const envp[]) {
- const bool runningAsRoot = !getuid(); // true iff U QPR3 or V+
-
- // Any released device will have codename REL instead of a 'real' codename.
- // For safety: default to 'REL' so we default to unreleased=false on failure.
- const bool unreleased = (base::GetProperty("ro.build.version.codename", "REL") != "REL");
-
- // goog/main device_api_level is bumped *way* before aosp/main api level
- // (the latter only gets bumped during the push of goog/main to aosp/main)
- //
- // Since we develop in AOSP, we want it to behave as if it was bumped too.
- //
- // Note that AOSP doesn't really have a good api level (for example during
- // early V dev cycle, it would have *all* of T, some but not all of U, and some V).
- // One could argue that for our purposes AOSP api level should be infinite or 10000.
- //
- // This could also cause api to be increased in goog/main or other branches,
- // but I can't imagine a case where this would be a problem: the problem
- // is rather a too low api level, rather than some ill defined high value.
- // For example as I write this aosp is 34/U, and goog is 35/V,
- // we want to treat both goog & aosp as 35/V, but it's harmless if we
- // treat goog as 36 because that value isn't yet defined to mean anything,
- // and we thus never compare against it.
- //
- // Also note that 'android_get_device_api_level()' is what the
- // //system/core/init/apex_init_util.cpp
- // apex init .XXrc parsing code uses for XX filtering.
- //
- // That code has a hack to bump <35 to 35 (to force aosp/main to parse .35rc),
- // but could (should?) perhaps be adjusted to match this.
- const int effective_api_level = android_get_device_api_level() + (int)unreleased;
- const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
- const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
- const bool isAtLeastV = (effective_api_level >= __ANDROID_API_V__);
-
- // last in U QPR2 beta1
- const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
- // first in U QPR2 beta~2
- const bool has_platform_netbpfload_rc = exists("/system/etc/init/netbpfload.rc");
-
- // Version of Network BpfLoader depends on the Android OS version
- unsigned int bpfloader_ver = 42u; // [42] BPFLOADER_MAINLINE_VERSION
- if (isAtLeastT) ++bpfloader_ver; // [43] BPFLOADER_MAINLINE_T_VERSION
- if (isAtLeastU) ++bpfloader_ver; // [44] BPFLOADER_MAINLINE_U_VERSION
- if (runningAsRoot) ++bpfloader_ver; // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
- if (isAtLeastV) ++bpfloader_ver; // [46] BPFLOADER_MAINLINE_V_VERSION
-
- ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
- bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
- kernelVersion(), describeArch(), getuid(),
- has_platform_bpfloader_rc, has_platform_netbpfload_rc);
-
- if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
- ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
- return 1;
- }
-
- if (has_platform_bpfloader_rc && has_platform_netbpfload_rc) {
- ALOGE("Platform has *both* bpfloader & netbpfload init scripts.");
- return 1;
- }
-
- logTetheringApexVersion();
-
- if (!isAtLeastT) {
- ALOGE("Impossible - not reachable on Android <T.");
- return 1;
- }
-
- // both S and T require kernel 4.9 (and eBpf support)
- if (isAtLeastT && !isAtLeastKernelVersion(4, 9, 0)) {
- ALOGE("Android T requires kernel 4.9.");
- return 1;
- }
-
- // U bumps the kernel requirement up to 4.14
- if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
- ALOGE("Android U requires kernel 4.14.");
- return 1;
- }
-
- // V bumps the kernel requirement up to 4.19
- // see also: //system/netd/tests/kernel_test.cpp TestKernel419
- if (isAtLeastV && !isAtLeastKernelVersion(4, 19, 0)) {
- ALOGE("Android V requires kernel 4.19.");
- return 1;
- }
-
- // Technically already required by U, but only enforce on V+
- // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
- if (isAtLeastV && isKernel32Bit() && isAtLeastKernelVersion(5, 16, 0)) {
- ALOGE("Android V+ platform with 32 bit kernel version >= 5.16.0 is unsupported");
- if (!isTV()) return 1;
- }
-
- // Various known ABI layout issues, particularly wrt. bpf and ipsec/xfrm.
- if (isAtLeastV && isKernel32Bit() && isX86()) {
- ALOGE("Android V requires X86 kernel to be 64-bit.");
- if (!isTV()) return 1;
- }
-
- if (isAtLeastV) {
- bool bad = false;
-
- if (!isLtsKernel()) {
- ALOGW("Android V only supports LTS kernels.");
- bad = true;
- }
-
-#define REQUIRE(maj, min, sub) \
- if (isKernelVersion(maj, min) && !isAtLeastKernelVersion(maj, min, sub)) { \
- ALOGW("Android V requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
- bad = true; \
- }
-
- REQUIRE(4, 19, 236)
- REQUIRE(5, 4, 186)
- REQUIRE(5, 10, 199)
- REQUIRE(5, 15, 136)
- REQUIRE(6, 1, 57)
- REQUIRE(6, 6, 0)
-
-#undef REQUIRE
-
- if (bad) {
- ALOGE("Unsupported kernel version (%07x).", kernelVersion());
- }
- }
-
- if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
- /* Android 14/U should only launch on 64-bit kernels
- * T launches on 5.10/5.15
- * U launches on 5.15/6.1
- * So >=5.16 implies isKernel64Bit()
- *
- * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
- *
- * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
- * we also require 64-bit userspace.
- *
- * There are various known issues with 32-bit userspace talking to various
- * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
- * Some of these have userspace or kernel workarounds/hacks.
- * Some of them don't...
- * We're going to be removing the hacks.
- * (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
- * Note: this check/enforcement only applies to *system* userspace code,
- * it does not affect unprivileged apps, the 32-on-64 compatibility
- * problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
- *
- * Additionally the 32-bit kernel jit support is poor,
- * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
- */
- ALOGE("64-bit userspace required on 6.2+ kernels.");
- // Stuff won't work reliably, but exempt TVs & Arm Wear devices
- if (!isTV() && !(isWear() && isArm())) return 1;
- }
-
- // Ensure we can determine the Android build type.
- if (!isEng() && !isUser() && !isUserdebug()) {
- ALOGE("Failed to determine the build type: got %s, want 'eng', 'user', or 'userdebug'",
- getBuildType().c_str());
- return 1;
- }
-
- if (runningAsRoot) {
- // Note: writing this proc file requires being root (always the case on V+)
-
- // Linux 5.16-rc1 changed the default to 2 (disabled but changeable),
- // but we need 0 (enabled)
- // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on
- // pre-5.13, on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
- if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
- isAtLeastKernelVersion(5, 13, 0)) return 1;
- }
-
- if (isAtLeastU) {
- // Note: writing these proc files requires CAP_NET_ADMIN
- // and sepolicy which is only present on U+,
- // on Android T and earlier versions they're written from the 'load_bpf_programs'
- // trigger (ie. by init itself) instead.
-
- // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
- // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
- // (Note: this (open) will fail with ENOENT 'No such file or directory' if
- // kernel does not have CONFIG_BPF_JIT=y)
- // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
- // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
- if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
-
- // Enable JIT kallsyms export for privileged users only
- // (Note: this (open) will fail with ENOENT 'No such file or directory' if
- // kernel does not have CONFIG_HAVE_EBPF_JIT=y)
- if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
- }
-
- // Create all the pin subdirectories
- // (this must be done first to allow selinux_context and pin_subdir functionality,
- // which could otherwise fail with ENOENT during object pinning or renaming,
- // due to ordering issues)
- for (const auto& location : locations) {
- if (createSysFsBpfSubDir(location.prefix)) return 1;
- }
-
- // Note: there's no actual src dir for fs_bpf_loader .o's,
- // so it is not listed in 'locations[].prefix'.
- // This is because this is primarily meant for triggering genfscon rules,
- // and as such this will likely always be the case.
- // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
- if (createSysFsBpfSubDir("loader")) return 1;
-
- // Load all ELF objects, create programs and maps, and pin them
- for (const auto& location : locations) {
- if (loadAllElfObjects(bpfloader_ver, location) != 0) {
- ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
- ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
- ALOGE("If this triggers randomly, you might be hitting some memory allocation "
- "problems or startup script race.");
- ALOGE("--- DO NOT EXPECT SYSTEM TO BOOT SUCCESSFULLY ---");
- sleep(20);
- return 2;
- }
- }
-
- int key = 1;
- int value = 123;
- base::unique_fd map(
- createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
- if (writeToMapEntry(map, &key, &value, BPF_ANY)) {
- ALOGE("Critical kernel bug - failure to write into index 1 of 2 element bpf map array.");
- return 1;
- }
-
- // leave a flag that we're done
- if (createSysFsBpfSubDir("netd_shared/mainline_done")) return 1;
-
- // platform bpfloader will only succeed when run as root
- if (!runningAsRoot) {
- // unreachable on U QPR3+ which always runs netbpfload as root
-
- ALOGI("mainline done, no need to transfer control to platform bpf loader.");
- return 0;
- }
-
- // unreachable before U QPR3
- ALOGI("done, transferring control to platform bpfloader.");
-
- // platform BpfLoader *needs* to run as root
- const char * args[] = { platformBpfLoader, NULL, };
- execve(args[0], (char**)args, envp);
- ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
- return 1;
-}
-
-} // namespace bpf
-} // namespace android
-
-int main(int argc, char** argv, char * const envp[]) {
- android::base::InitLogging(argv, &android::base::KernelLogger);
-
- if (argc == 2 && !strcmp(argv[1], "done")) {
- // we're being re-exec'ed from platform bpfloader to 'finalize' things
- if (!android::base::SetProperty("bpf.progs_loaded", "1")) {
- ALOGE("Failed to set bpf.progs_loaded property to 1.");
- return 125;
- }
- ALOGI("success.");
- return 0;
- }
-
- return android::bpf::doLoad(argv, envp);
-}
diff --git a/netbpfload/initrc-doc/README.txt b/netbpfload/initrc-doc/README.txt
deleted file mode 100644
index 42e1fc2..0000000
--- a/netbpfload/initrc-doc/README.txt
+++ /dev/null
@@ -1,62 +0,0 @@
-This directory contains comment stripped versions of
- //system/bpf/bpfloader/bpfloader.rc
-from previous versions of Android.
-
-Generated via:
- (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
- (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
- (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
- (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
- (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/main:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
-
-this is entirely equivalent to:
- (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/main:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
-
-it is also equivalent to:
- (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-v2-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
- (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
-
-ie. there were no changes between R/S/T and R/S/T QPR3, and no change between U and U QPR1.
-
-Note: Sv2 sdk/api level is actually 32, it just didn't change anything wrt. bpf, so doesn't matter.
-
-
-Key takeaways:
-
-= R bpfloader:
- - CHOWN + SYS_ADMIN
- - asynchronous startup
- - platform only
- - proc file setup handled by initrc
-
-= S bpfloader
- - adds NET_ADMIN
- - synchronous startup
- - platform + mainline tethering offload
-
-= T bpfloader
- - platform + mainline networking (including tethering offload)
- - supported btf for maps via exec of btfloader
-
-= U bpfloader
- - proc file setup moved into bpfloader binary
- - explicitly specified user and groups:
- group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
- user root
-
-= U QPR2 bpfloader
- - drops support of btf for maps
- - invocation of /system/bin/netbpfload binary, which after handling *all*
- networking bpf related things executes the platform /system/bin/bpfloader
- which handles non-networking bpf.
-
-Note that there is now a copy of 'netbpfload' provided by the tethering apex
-mainline module at /apex/com.android.tethering/bin/netbpfload, which due
-to the use of execve("/system/bin/bpfloader") relies on T+ selinux which was
-added for btf map support (specifically the ability to exec the "btfloader").
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
deleted file mode 100644
index 5141095..0000000
--- a/netbpfload/loader.cpp
+++ /dev/null
@@ -1,1194 +0,0 @@
-/*
- * Copyright (C) 2018-2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_TAG "NetBpfLoad"
-
-#include <errno.h>
-#include <fcntl.h>
-#include <linux/bpf.h>
-#include <linux/elf.h>
-#include <log/log.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-#include <sys/stat.h>
-#include <sys/utsname.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-#include "BpfSyscallWrappers.h"
-#include "bpf/BpfUtils.h"
-#include "bpf/bpf_map_def.h"
-#include "loader.h"
-
-#include <cstdlib>
-#include <fstream>
-#include <iostream>
-#include <optional>
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-#include <android-base/cmsg.h>
-#include <android-base/file.h>
-#include <android-base/properties.h>
-#include <android-base/strings.h>
-#include <android-base/unique_fd.h>
-
-#define BPF_FS_PATH "/sys/fs/bpf/"
-
-// Size of the BPF log buffer for verifier logging
-#define BPF_LOAD_LOG_SZ 0xfffff
-
-// Unspecified attach type is 0 which is BPF_CGROUP_INET_INGRESS.
-#define BPF_ATTACH_TYPE_UNSPEC BPF_CGROUP_INET_INGRESS
-
-using android::base::StartsWith;
-using android::base::unique_fd;
-using std::ifstream;
-using std::ios;
-using std::optional;
-using std::string;
-using std::vector;
-
-namespace android {
-namespace bpf {
-
-const std::string& getBuildType() {
- static std::string t = android::base::GetProperty("ro.build.type", "unknown");
- return t;
-}
-
-static unsigned int page_size = static_cast<unsigned int>(getpagesize());
-
-constexpr const char* lookupSelinuxContext(const domain d, const char* const unspecified = "") {
- switch (d) {
- case domain::unspecified: return unspecified;
- case domain::tethering: return "fs_bpf_tethering";
- case domain::net_private: return "fs_bpf_net_private";
- case domain::net_shared: return "fs_bpf_net_shared";
- case domain::netd_readonly: return "fs_bpf_netd_readonly";
- case domain::netd_shared: return "fs_bpf_netd_shared";
- default: return "(unrecognized)";
- }
-}
-
-domain getDomainFromSelinuxContext(const char s[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE]) {
- for (domain d : AllDomains) {
- // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
- if (strlen(lookupSelinuxContext(d)) >= BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE) abort();
- if (!strncmp(s, lookupSelinuxContext(d), BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE)) return d;
- }
- ALOGW("ignoring unrecognized selinux_context '%-32s'", s);
- // We should return 'unrecognized' here, however: returning unspecified will
- // result in the system simply using the default context, which in turn
- // will allow future expansion by adding more restrictive selinux types.
- // Older bpfloader will simply ignore that, and use the less restrictive default.
- // This does mean you CANNOT later add a *less* restrictive type than the default.
- //
- // Note: we cannot just abort() here as this might be a mainline module shipped optional update
- return domain::unspecified;
-}
-
-constexpr const char* lookupPinSubdir(const domain d, const char* const unspecified = "") {
- switch (d) {
- case domain::unspecified: return unspecified;
- case domain::tethering: return "tethering/";
- case domain::net_private: return "net_private/";
- case domain::net_shared: return "net_shared/";
- case domain::netd_readonly: return "netd_readonly/";
- case domain::netd_shared: return "netd_shared/";
- default: return "(unrecognized)";
- }
-};
-
-domain getDomainFromPinSubdir(const char s[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE]) {
- for (domain d : AllDomains) {
- // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
- if (strlen(lookupPinSubdir(d)) >= BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE) abort();
- if (!strncmp(s, lookupPinSubdir(d), BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE)) return d;
- }
- ALOGE("unrecognized pin_subdir '%-32s'", s);
- // pin_subdir affects the object's full pathname,
- // and thus using the default would change the location and thus our code's ability to find it,
- // hence this seems worth treating as a true error condition.
- //
- // Note: we cannot just abort() here as this might be a mainline module shipped optional update
- // However, our callers will treat this as an error, and stop loading the specific .o,
- // which will fail bpfloader if the .o is marked critical.
- return domain::unrecognized;
-}
-
-static string pathToObjName(const string& path) {
- // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
- string filename = android::base::Split(path, "/").back();
- // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
- string name = filename.substr(0, filename.find_last_of('.'));
- // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
- // this can be used to provide duplicate programs (mux based on the bpfloader version)
- return name.substr(0, name.find_last_of('@'));
-}
-
-typedef struct {
- const char* name;
- enum bpf_prog_type type;
- enum bpf_attach_type expected_attach_type;
-} sectionType;
-
-/*
- * Map section name prefixes to program types, the section name will be:
- * SECTION(<prefix>/<name-of-program>)
- * For example:
- * SECTION("tracepoint/sched_switch_func") where sched_switch_funcs
- * is the name of the program, and tracepoint is the type.
- *
- * However, be aware that you should not be directly using the SECTION() macro.
- * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
- *
- * Programs shipped inside the tethering apex should be limited to networking stuff,
- * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
- * since they are less stable abi/api and may conflict with platform uses of bpf.
- */
-sectionType sectionNameTypes[] = {
- {"bind4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
- {"bind6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
- {"cgroupskb/", BPF_PROG_TYPE_CGROUP_SKB, BPF_ATTACH_TYPE_UNSPEC},
- {"cgroupsock/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_ATTACH_TYPE_UNSPEC},
- {"cgroupsockcreate/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET_SOCK_CREATE},
- {"cgroupsockrelease/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET_SOCK_RELEASE},
- {"connect4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
- {"connect6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
- {"egress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_EGRESS},
- {"getsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_GETSOCKOPT},
- {"ingress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_INGRESS},
- {"lwt_in/", BPF_PROG_TYPE_LWT_IN, BPF_ATTACH_TYPE_UNSPEC},
- {"lwt_out/", BPF_PROG_TYPE_LWT_OUT, BPF_ATTACH_TYPE_UNSPEC},
- {"lwt_seg6local/", BPF_PROG_TYPE_LWT_SEG6LOCAL, BPF_ATTACH_TYPE_UNSPEC},
- {"lwt_xmit/", BPF_PROG_TYPE_LWT_XMIT, BPF_ATTACH_TYPE_UNSPEC},
- {"postbind4/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET4_POST_BIND},
- {"postbind6/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET6_POST_BIND},
- {"recvmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
- {"recvmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
- {"schedact/", BPF_PROG_TYPE_SCHED_ACT, BPF_ATTACH_TYPE_UNSPEC},
- {"schedcls/", BPF_PROG_TYPE_SCHED_CLS, BPF_ATTACH_TYPE_UNSPEC},
- {"sendmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
- {"sendmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
- {"setsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_SETSOCKOPT},
- {"skfilter/", BPF_PROG_TYPE_SOCKET_FILTER, BPF_ATTACH_TYPE_UNSPEC},
- {"sockops/", BPF_PROG_TYPE_SOCK_OPS, BPF_CGROUP_SOCK_OPS},
- {"sysctl", BPF_PROG_TYPE_CGROUP_SYSCTL, BPF_CGROUP_SYSCTL},
- {"xdp/", BPF_PROG_TYPE_XDP, BPF_ATTACH_TYPE_UNSPEC},
-};
-
-typedef struct {
- enum bpf_prog_type type;
- enum bpf_attach_type expected_attach_type;
- string name;
- vector<char> data;
- vector<char> rel_data;
- optional<struct bpf_prog_def> prog_def;
-
- unique_fd prog_fd; /* fd after loading */
-} codeSection;
-
-static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
- elfFile.seekg(0);
- if (elfFile.fail()) return -1;
-
- if (!elfFile.read((char*)eh, sizeof(*eh))) return -1;
-
- return 0;
-}
-
-/* Reads all section header tables into an Shdr array */
-static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
- Elf64_Ehdr eh;
- int ret = 0;
-
- ret = readElfHeader(elfFile, &eh);
- if (ret) return ret;
-
- elfFile.seekg(eh.e_shoff);
- if (elfFile.fail()) return -1;
-
- /* Read shdr table entries */
- shTable.resize(eh.e_shnum);
-
- if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
-
- return 0;
-}
-
-/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
- vector<Elf64_Shdr> shTable;
- int ret = readSectionHeadersAll(elfFile, shTable);
- if (ret) return ret;
-
- elfFile.seekg(shTable[id].sh_offset);
- if (elfFile.fail()) return -1;
-
- sec.resize(shTable[id].sh_size);
- if (!elfFile.read(sec.data(), shTable[id].sh_size)) return -1;
-
- return 0;
-}
-
-/* Read whole section header string table */
-static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
- Elf64_Ehdr eh;
- int ret = readElfHeader(elfFile, &eh);
- if (ret) return ret;
-
- ret = readSectionByIdx(elfFile, eh.e_shstrndx, strtab);
- if (ret) return ret;
-
- return 0;
-}
-
-/* Get name from offset in strtab */
-static int getSymName(ifstream& elfFile, int nameOff, string& name) {
- int ret;
- vector<char> secStrTab;
-
- ret = readSectionHeaderStrtab(elfFile, secStrTab);
- if (ret) return ret;
-
- if (nameOff >= (int)secStrTab.size()) return -1;
-
- name = string((char*)secStrTab.data() + nameOff);
- return 0;
-}
-
-/* Reads a full section by name - example to get the GPL license */
-static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
- vector<char> secStrTab;
- vector<Elf64_Shdr> shTable;
- int ret;
-
- ret = readSectionHeadersAll(elfFile, shTable);
- if (ret) return ret;
-
- ret = readSectionHeaderStrtab(elfFile, secStrTab);
- if (ret) return ret;
-
- for (int i = 0; i < (int)shTable.size(); i++) {
- char* secname = secStrTab.data() + shTable[i].sh_name;
- if (!secname) continue;
-
- if (!strcmp(secname, name)) {
- vector<char> dataTmp;
- dataTmp.resize(shTable[i].sh_size);
-
- elfFile.seekg(shTable[i].sh_offset);
- if (elfFile.fail()) return -1;
-
- if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
-
- data = dataTmp;
- return 0;
- }
- }
- return -2;
-}
-
-unsigned int readSectionUint(const char* name, ifstream& elfFile, unsigned int defVal) {
- vector<char> theBytes;
- int ret = readSectionByName(name, elfFile, theBytes);
- if (ret) {
- ALOGD("Couldn't find section %s (defaulting to %u [0x%x]).", name, defVal, defVal);
- return defVal;
- } else if (theBytes.size() < sizeof(unsigned int)) {
- ALOGE("Section %s too short (defaulting to %u [0x%x]).", name, defVal, defVal);
- return defVal;
- } else {
- // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
- unsigned int value = static_cast<unsigned char>(theBytes[3]);
- value <<= 8;
- value += static_cast<unsigned char>(theBytes[2]);
- value <<= 8;
- value += static_cast<unsigned char>(theBytes[1]);
- value <<= 8;
- value += static_cast<unsigned char>(theBytes[0]);
- ALOGI("Section %s value is %u [0x%x]", name, value, value);
- return value;
- }
-}
-
-static int readSectionByType(ifstream& elfFile, int type, vector<char>& data) {
- int ret;
- vector<Elf64_Shdr> shTable;
-
- ret = readSectionHeadersAll(elfFile, shTable);
- if (ret) return ret;
-
- for (int i = 0; i < (int)shTable.size(); i++) {
- if ((int)shTable[i].sh_type != type) continue;
-
- vector<char> dataTmp;
- dataTmp.resize(shTable[i].sh_size);
-
- elfFile.seekg(shTable[i].sh_offset);
- if (elfFile.fail()) return -1;
-
- if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
-
- data = dataTmp;
- return 0;
- }
- return -2;
-}
-
-static bool symCompare(Elf64_Sym a, Elf64_Sym b) {
- return (a.st_value < b.st_value);
-}
-
-static int readSymTab(ifstream& elfFile, int sort, vector<Elf64_Sym>& data) {
- int ret, numElems;
- Elf64_Sym* buf;
- vector<char> secData;
-
- ret = readSectionByType(elfFile, SHT_SYMTAB, secData);
- if (ret) return ret;
-
- buf = (Elf64_Sym*)secData.data();
- numElems = (secData.size() / sizeof(Elf64_Sym));
- data.assign(buf, buf + numElems);
-
- if (sort) std::sort(data.begin(), data.end(), symCompare);
- return 0;
-}
-
-static enum bpf_prog_type getSectionType(string& name) {
- for (auto& snt : sectionNameTypes)
- if (StartsWith(name, snt.name)) return snt.type;
-
- return BPF_PROG_TYPE_UNSPEC;
-}
-
-static enum bpf_attach_type getExpectedAttachType(string& name) {
- for (auto& snt : sectionNameTypes)
- if (StartsWith(name, snt.name)) return snt.expected_attach_type;
- return BPF_ATTACH_TYPE_UNSPEC;
-}
-
-/*
-static string getSectionName(enum bpf_prog_type type)
-{
- for (auto& snt : sectionNameTypes)
- if (snt.type == type)
- return string(snt.name);
-
- return "UNKNOWN SECTION NAME " + std::to_string(type);
-}
-*/
-
-static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd,
- size_t sizeOfBpfProgDef) {
- vector<char> pdData;
- int ret = readSectionByName("progs", elfFile, pdData);
- if (ret) return ret;
-
- if (pdData.size() % sizeOfBpfProgDef) {
- ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
- pdData.size(), sizeOfBpfProgDef);
- return -1;
- };
-
- int progCount = pdData.size() / sizeOfBpfProgDef;
- pd.resize(progCount);
- size_t trimmedSize = std::min(sizeOfBpfProgDef, sizeof(struct bpf_prog_def));
-
- const char* dataPtr = pdData.data();
- for (auto& p : pd) {
- // First we zero initialize
- memset(&p, 0, sizeof(p));
- // Then we set non-zero defaults
- p.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER; // v1.0
- // Then we copy over the structure prefix from the ELF file.
- memcpy(&p, dataPtr, trimmedSize);
- // Move to next struct in the ELF file
- dataPtr += sizeOfBpfProgDef;
- }
- return 0;
-}
-
-static int getSectionSymNames(ifstream& elfFile, const string& sectionName, vector<string>& names,
- optional<unsigned> symbolType = std::nullopt) {
- int ret;
- string name;
- vector<Elf64_Sym> symtab;
- vector<Elf64_Shdr> shTable;
-
- ret = readSymTab(elfFile, 1 /* sort */, symtab);
- if (ret) return ret;
-
- /* Get index of section */
- ret = readSectionHeadersAll(elfFile, shTable);
- if (ret) return ret;
-
- int sec_idx = -1;
- for (int i = 0; i < (int)shTable.size(); i++) {
- ret = getSymName(elfFile, shTable[i].sh_name, name);
- if (ret) return ret;
-
- if (!name.compare(sectionName)) {
- sec_idx = i;
- break;
- }
- }
-
- /* No section found with matching name*/
- if (sec_idx == -1) {
- ALOGW("No %s section could be found in elf object", sectionName.c_str());
- return -1;
- }
-
- for (int i = 0; i < (int)symtab.size(); i++) {
- if (symbolType.has_value() && ELF_ST_TYPE(symtab[i].st_info) != symbolType) continue;
-
- if (symtab[i].st_shndx == sec_idx) {
- string s;
- ret = getSymName(elfFile, symtab[i].st_name, s);
- if (ret) return ret;
- names.push_back(s);
- }
- }
-
- return 0;
-}
-
-/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef) {
- vector<Elf64_Shdr> shTable;
- int entries, ret = 0;
-
- ret = readSectionHeadersAll(elfFile, shTable);
- if (ret) return ret;
- entries = shTable.size();
-
- vector<struct bpf_prog_def> pd;
- ret = readProgDefs(elfFile, pd, sizeOfBpfProgDef);
- if (ret) return ret;
- vector<string> progDefNames;
- ret = getSectionSymNames(elfFile, "progs", progDefNames);
- if (!pd.empty() && ret) return ret;
-
- for (int i = 0; i < entries; i++) {
- string name;
- codeSection cs_temp;
- cs_temp.type = BPF_PROG_TYPE_UNSPEC;
-
- ret = getSymName(elfFile, shTable[i].sh_name, name);
- if (ret) return ret;
-
- enum bpf_prog_type ptype = getSectionType(name);
-
- if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
-
- // This must be done before '/' is replaced with '_'.
- cs_temp.expected_attach_type = getExpectedAttachType(name);
-
- string oldName = name;
-
- // convert all slashes to underscores
- std::replace(name.begin(), name.end(), '/', '_');
-
- cs_temp.type = ptype;
- cs_temp.name = name;
-
- ret = readSectionByIdx(elfFile, i, cs_temp.data);
- if (ret) return ret;
- ALOGV("Loaded code section %d (%s)", i, name.c_str());
-
- vector<string> csSymNames;
- ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
- if (ret || !csSymNames.size()) return ret;
- for (size_t i = 0; i < progDefNames.size(); ++i) {
- if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
- cs_temp.prog_def = pd[i];
- break;
- }
- }
-
- /* Check for rel section */
- if (cs_temp.data.size() > 0 && i < entries) {
- ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
- if (ret) return ret;
-
- if (name == (".rel" + oldName)) {
- ret = readSectionByIdx(elfFile, i + 1, cs_temp.rel_data);
- if (ret) return ret;
- ALOGV("Loaded relo section %d (%s)", i, name.c_str());
- }
- }
-
- if (cs_temp.data.size() > 0) {
- cs.push_back(std::move(cs_temp));
- ALOGV("Adding section %d to cs list", i);
- }
- }
- return 0;
-}
-
-static int getSymNameByIdx(ifstream& elfFile, int index, string& name) {
- vector<Elf64_Sym> symtab;
- int ret = 0;
-
- ret = readSymTab(elfFile, 0 /* !sort */, symtab);
- if (ret) return ret;
-
- if (index >= (int)symtab.size()) return -1;
-
- return getSymName(elfFile, symtab[index].st_name, name);
-}
-
-static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
- const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
- // bpfGetFd... family of functions require at minimum a 4.14 kernel,
- // so on 4.9-T kernels just pretend the map matches our expectations.
- // Additionally we'll get almost equivalent test coverage on newer devices/kernels.
- // This is because the primary failure mode we're trying to detect here
- // is either a source code misconfiguration (which is likely kernel independent)
- // or a newly introduced kernel feature/bug (which is unlikely to get backported to 4.9).
- if (!isAtLeastKernelVersion(4, 14, 0)) return true;
-
- // Assuming fd is a valid Bpf Map file descriptor then
- // all the following should always succeed on a 4.14+ kernel.
- // If they somehow do fail, they'll return -1 (and set errno),
- // which should then cause (among others) a key_size mismatch.
- int fd_type = bpfGetFdMapType(fd);
- int fd_key_size = bpfGetFdKeySize(fd);
- int fd_value_size = bpfGetFdValueSize(fd);
- int fd_max_entries = bpfGetFdMaxEntries(fd);
- int fd_map_flags = bpfGetFdMapFlags(fd);
-
- // DEVMAPs are readonly from the bpf program side's point of view, as such
- // the kernel in kernel/bpf/devmap.c dev_map_init_map() will set the flag
- int desired_map_flags = (int)mapDef.map_flags;
- if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
- desired_map_flags |= BPF_F_RDONLY_PROG;
-
- // The .h file enforces that this is a power of two, and page size will
- // also always be a power of two, so this logic is actually enough to
- // force it to be a multiple of the page size, as required by the kernel.
- unsigned int desired_max_entries = mapDef.max_entries;
- if (type == BPF_MAP_TYPE_RINGBUF) {
- if (desired_max_entries < page_size) desired_max_entries = page_size;
- }
-
- // The following checks should *never* trigger, if one of them somehow does,
- // it probably means a bpf .o file has been changed/replaced at runtime
- // and bpfloader was manually rerun (normally it should only run *once*
- // early during the boot process).
- // Another possibility is that something is misconfigured in the code:
- // most likely a shared map is declared twice differently.
- // But such a change should never be checked into the source tree...
- if ((fd_type == type) &&
- (fd_key_size == (int)mapDef.key_size) &&
- (fd_value_size == (int)mapDef.value_size) &&
- (fd_max_entries == (int)desired_max_entries) &&
- (fd_map_flags == desired_map_flags)) {
- return true;
- }
-
- ALOGE("bpf map name %s mismatch: desired/found: "
- "type:%d/%d key:%u/%d value:%u/%d entries:%u/%d flags:%u/%d",
- mapName.c_str(), type, fd_type, mapDef.key_size, fd_key_size, mapDef.value_size,
- fd_value_size, mapDef.max_entries, fd_max_entries, desired_map_flags, fd_map_flags);
- return false;
-}
-
-static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
- const char* prefix, const size_t sizeOfBpfMapDef,
- const unsigned int bpfloader_ver) {
- int ret;
- vector<char> mdData;
- vector<struct bpf_map_def> md;
- vector<string> mapNames;
- string objName = pathToObjName(string(elfPath));
-
- ret = readSectionByName("maps", elfFile, mdData);
- if (ret == -2) return 0; // no maps to read
- if (ret) return ret;
-
- if (mdData.size() % sizeOfBpfMapDef) {
- ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
- mdData.size(), sizeOfBpfMapDef);
- return -1;
- };
-
- int mapCount = mdData.size() / sizeOfBpfMapDef;
- md.resize(mapCount);
- size_t trimmedSize = std::min(sizeOfBpfMapDef, sizeof(struct bpf_map_def));
-
- const char* dataPtr = mdData.data();
- for (auto& m : md) {
- // First we zero initialize
- memset(&m, 0, sizeof(m));
- // Then we set non-zero defaults
- m.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER; // v1.0
- m.max_kver = 0xFFFFFFFFu; // matches KVER_INF from bpf_helpers.h
- // Then we copy over the structure prefix from the ELF file.
- memcpy(&m, dataPtr, trimmedSize);
- // Move to next struct in the ELF file
- dataPtr += sizeOfBpfMapDef;
- }
-
- ret = getSectionSymNames(elfFile, "maps", mapNames);
- if (ret) return ret;
-
- unsigned kvers = kernelVersion();
-
- for (int i = 0; i < (int)mapNames.size(); i++) {
- if (md[i].zero != 0) abort();
-
- if (bpfloader_ver < md[i].bpfloader_min_ver) {
- ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
- md[i].bpfloader_min_ver);
- mapFds.push_back(unique_fd());
- continue;
- }
-
- if (bpfloader_ver >= md[i].bpfloader_max_ver) {
- ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
- md[i].bpfloader_max_ver);
- mapFds.push_back(unique_fd());
- continue;
- }
-
- if (kvers < md[i].min_kver) {
- ALOGI("skipping map %s which requires kernel version 0x%x >= 0x%x",
- mapNames[i].c_str(), kvers, md[i].min_kver);
- mapFds.push_back(unique_fd());
- continue;
- }
-
- if (kvers >= md[i].max_kver) {
- ALOGI("skipping map %s which requires kernel version 0x%x < 0x%x",
- mapNames[i].c_str(), kvers, md[i].max_kver);
- mapFds.push_back(unique_fd());
- continue;
- }
-
- if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
- (md[i].ignore_on_userdebug && isUserdebug())) {
- ALOGI("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
- getBuildType().c_str());
- mapFds.push_back(unique_fd());
- continue;
- }
-
- if ((isArm() && isKernel32Bit() && md[i].ignore_on_arm32) ||
- (isArm() && isKernel64Bit() && md[i].ignore_on_aarch64) ||
- (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
- (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
- (isRiscV() && md[i].ignore_on_riscv64)) {
- ALOGI("skipping map %s which is ignored on %s", mapNames[i].c_str(),
- describeArch());
- mapFds.push_back(unique_fd());
- continue;
- }
-
- enum bpf_map_type type = md[i].type;
- if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
- // On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
- // of be approximated: ARRAY has the same userspace api, though it is not usable
- // by the same ebpf programs. However, that's okay because the bpf_redirect_map()
- // helper doesn't exist on 4.9-T anyway (so the bpf program would fail to load,
- // and thus needs to be tagged as 4.14+ either way), so there's nothing useful you
- // could do with a DEVMAP anyway (that isn't already provided by an ARRAY)...
- // Hence using an ARRAY instead of a DEVMAP simply makes life easier for userspace.
- type = BPF_MAP_TYPE_ARRAY;
- }
- if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
- // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
- // of be approximated: HASH has the same userspace visible api.
- // However it cannot be used by ebpf programs in the same way.
- // Since bpf_redirect_map() only requires 4.14, a program using a DEVMAP_HASH map
- // would fail to load (due to trying to redirect to a HASH instead of DEVMAP_HASH).
- // One must thus tag any BPF_MAP_TYPE_DEVMAP_HASH + bpf_redirect_map() using
- // programs as being 5.4+...
- type = BPF_MAP_TYPE_HASH;
- }
-
- // The .h file enforces that this is a power of two, and page size will
- // also always be a power of two, so this logic is actually enough to
- // force it to be a multiple of the page size, as required by the kernel.
- unsigned int max_entries = md[i].max_entries;
- if (type == BPF_MAP_TYPE_RINGBUF) {
- if (max_entries < page_size) max_entries = page_size;
- }
-
- domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
- if (specified(selinux_context)) {
- ALOGI("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
- md[i].selinux_context, static_cast<int>(selinux_context),
- lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
- }
-
- domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
- if (unrecognized(pin_subdir)) return -ENOTDIR;
- if (specified(pin_subdir)) {
- ALOGI("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
- static_cast<int>(pin_subdir), lookupPinSubdir(pin_subdir));
- }
-
- // Format of pin location is /sys/fs/bpf/<pin_subdir|prefix>map_<objName>_<mapName>
- // except that maps shared across .o's have empty <objName>
- // Note: <objName> refers to the extension-less basename of the .o file (without @ suffix).
- string mapPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "map_" +
- (md[i].shared ? "" : objName) + "_" + mapNames[i];
- bool reuse = false;
- unique_fd fd;
- int saved_errno;
-
- if (access(mapPinLoc.c_str(), F_OK) == 0) {
- fd.reset(mapRetrieveRO(mapPinLoc.c_str()));
- saved_errno = errno;
- ALOGD("bpf_create_map reusing map %s, ret: %d", mapNames[i].c_str(), fd.get());
- reuse = true;
- } else {
- union bpf_attr req = {
- .map_type = type,
- .key_size = md[i].key_size,
- .value_size = md[i].value_size,
- .max_entries = max_entries,
- .map_flags = md[i].map_flags,
- };
- if (isAtLeastKernelVersion(4, 15, 0))
- strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
- fd.reset(bpf(BPF_MAP_CREATE, req));
- saved_errno = errno;
- ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
- }
-
- if (!fd.ok()) return -saved_errno;
-
- // When reusing a pinned map, we need to check the map type/sizes/etc match, but for
- // safety (since reuse code path is rare) run these checks even if we just created it.
- // We assume failure is due to pinned map mismatch, hence the 'NOT UNIQUE' return code.
- if (!mapMatchesExpectations(fd, mapNames[i], md[i], type)) return -ENOTUNIQ;
-
- if (!reuse) {
- if (specified(selinux_context)) {
- string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
- "tmp_map_" + objName + "_" + mapNames[i];
- ret = bpfFdPin(fd, createLoc.c_str());
- if (ret) {
- int err = errno;
- ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
- return -err;
- }
- ret = renameat2(AT_FDCWD, createLoc.c_str(),
- AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
- if (ret) {
- int err = errno;
- ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
- err, strerror(err));
- return -err;
- }
- } else {
- ret = bpfFdPin(fd, mapPinLoc.c_str());
- if (ret) {
- int err = errno;
- ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
- return -err;
- }
- }
- ret = chmod(mapPinLoc.c_str(), md[i].mode);
- if (ret) {
- int err = errno;
- ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
- strerror(err));
- return -err;
- }
- ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
- if (ret) {
- int err = errno;
- ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
- ret, err, strerror(err));
- return -err;
- }
- }
-
- int mapId = bpfGetFdMapId(fd);
- if (mapId == -1) {
- ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
- } else {
- ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
- }
-
- mapFds.push_back(std::move(fd));
- }
-
- return ret;
-}
-
-/* For debugging, dump all instructions */
-static void dumpIns(char* ins, int size) {
- for (int row = 0; row < size / 8; row++) {
- ALOGE("%d: ", row);
- for (int j = 0; j < 8; j++) {
- ALOGE("%3x ", ins[(row * 8) + j]);
- }
- ALOGE("\n");
- }
-}
-
-/* For debugging, dump all code sections from cs list */
-static void dumpAllCs(vector<codeSection>& cs) {
- for (int i = 0; i < (int)cs.size(); i++) {
- ALOGE("Dumping cs %d, name %s", int(i), cs[i].name.c_str());
- dumpIns((char*)cs[i].data.data(), cs[i].data.size());
- ALOGE("-----------");
- }
-}
-
-static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
- int insnIndex;
- struct bpf_insn *insn, *insns;
-
- insns = (struct bpf_insn*)(insnsPtr);
-
- insnIndex = offset / sizeof(struct bpf_insn);
- insn = &insns[insnIndex];
-
- // Occasionally might be useful for relocation debugging, but pretty spammy
- if (0) {
- ALOGV("applying relo to instruction at byte offset: %llu, "
- "insn offset %d, insn %llx",
- (unsigned long long)offset, insnIndex, *(unsigned long long*)insn);
- }
-
- if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
- ALOGE("Dumping all instructions till ins %d", insnIndex);
- ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
- dumpIns((char*)insnsPtr, (insnIndex + 3) * 8);
- return;
- }
-
- insn->imm = fd;
- insn->src_reg = BPF_PSEUDO_MAP_FD;
-}
-
-static void applyMapRelo(ifstream& elfFile, vector<unique_fd> &mapFds, vector<codeSection>& cs) {
- vector<string> mapNames;
-
- int ret = getSectionSymNames(elfFile, "maps", mapNames);
- if (ret) return;
-
- for (int k = 0; k != (int)cs.size(); k++) {
- Elf64_Rel* rel = (Elf64_Rel*)(cs[k].rel_data.data());
- int n_rel = cs[k].rel_data.size() / sizeof(*rel);
-
- for (int i = 0; i < n_rel; i++) {
- int symIndex = ELF64_R_SYM(rel[i].r_info);
- string symName;
-
- ret = getSymNameByIdx(elfFile, symIndex, symName);
- if (ret) return;
-
- /* Find the map fd and apply relo */
- for (int j = 0; j < (int)mapNames.size(); j++) {
- if (!mapNames[j].compare(symName)) {
- applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
- break;
- }
- }
- }
- }
-}
-
-static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
- const char* prefix, const unsigned int bpfloader_ver) {
- unsigned kvers = kernelVersion();
-
- if (!kvers) {
- ALOGE("unable to get kernel version");
- return -EINVAL;
- }
-
- string objName = pathToObjName(string(elfPath));
-
- for (int i = 0; i < (int)cs.size(); i++) {
- unique_fd& fd = cs[i].prog_fd;
- int ret;
- string name = cs[i].name;
-
- if (!cs[i].prog_def.has_value()) {
- ALOGE("[%d] '%s' missing program definition! bad bpf.o build?", i, name.c_str());
- return -EINVAL;
- }
-
- unsigned min_kver = cs[i].prog_def->min_kver;
- unsigned max_kver = cs[i].prog_def->max_kver;
- ALOGD("cs[%d].name:%s min_kver:%x .max_kver:%x (kvers:%x)", i, name.c_str(), min_kver,
- max_kver, kvers);
- if (kvers < min_kver) continue;
- if (kvers >= max_kver) continue;
-
- unsigned bpfMinVer = cs[i].prog_def->bpfloader_min_ver;
- unsigned bpfMaxVer = cs[i].prog_def->bpfloader_max_ver;
- domain selinux_context = getDomainFromSelinuxContext(cs[i].prog_def->selinux_context);
- domain pin_subdir = getDomainFromPinSubdir(cs[i].prog_def->pin_subdir);
- // Note: make sure to only check for unrecognized *after* verifying bpfloader
- // version limits include this bpfloader's version.
-
- ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
- bpfMinVer, bpfMaxVer);
- if (bpfloader_ver < bpfMinVer) continue;
- if (bpfloader_ver >= bpfMaxVer) continue;
-
- if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
- (cs[i].prog_def->ignore_on_user && isUser()) ||
- (cs[i].prog_def->ignore_on_userdebug && isUserdebug())) {
- ALOGD("cs[%d].name:%s is ignored on %s builds", i, name.c_str(),
- getBuildType().c_str());
- continue;
- }
-
- if ((isArm() && isKernel32Bit() && cs[i].prog_def->ignore_on_arm32) ||
- (isArm() && isKernel64Bit() && cs[i].prog_def->ignore_on_aarch64) ||
- (isX86() && isKernel32Bit() && cs[i].prog_def->ignore_on_x86_32) ||
- (isX86() && isKernel64Bit() && cs[i].prog_def->ignore_on_x86_64) ||
- (isRiscV() && cs[i].prog_def->ignore_on_riscv64)) {
- ALOGD("cs[%d].name:%s is ignored on %s", i, name.c_str(), describeArch());
- continue;
- }
-
- if (unrecognized(pin_subdir)) return -ENOTDIR;
-
- if (specified(selinux_context)) {
- ALOGI("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
- cs[i].prog_def->selinux_context, static_cast<int>(selinux_context),
- lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
- }
-
- if (specified(pin_subdir)) {
- ALOGI("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
- cs[i].prog_def->pin_subdir, static_cast<int>(pin_subdir),
- lookupPinSubdir(pin_subdir));
- }
-
- // strip any potential $foo suffix
- // this can be used to provide duplicate programs
- // conditionally loaded based on running kernel version
- name = name.substr(0, name.find_last_of('$'));
-
- bool reuse = false;
- // Format of pin location is
- // /sys/fs/bpf/<prefix>prog_<objName>_<progName>
- string progPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "prog_" +
- objName + '_' + string(name);
- if (access(progPinLoc.c_str(), F_OK) == 0) {
- fd.reset(retrieveProgram(progPinLoc.c_str()));
- ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
- (!fd.ok() ? std::strerror(errno) : "no error"));
- reuse = true;
- } else {
- vector<char> log_buf(BPF_LOAD_LOG_SZ, 0);
-
- union bpf_attr req = {
- .prog_type = cs[i].type,
- .kern_version = kvers,
- .license = ptr_to_u64(license.c_str()),
- .insns = ptr_to_u64(cs[i].data.data()),
- .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
- .log_level = 1,
- .log_buf = ptr_to_u64(log_buf.data()),
- .log_size = static_cast<__u32>(log_buf.size()),
- .expected_attach_type = cs[i].expected_attach_type,
- };
- if (isAtLeastKernelVersion(4, 15, 0))
- strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
- fd.reset(bpf(BPF_PROG_LOAD, req));
-
- ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
- cs[i].name.c_str(), fd.get(), (!fd.ok() ? std::strerror(errno) : "no error"));
-
- if (!fd.ok()) {
- vector<string> lines = android::base::Split(log_buf.data(), "\n");
-
- ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
- for (const auto& line : lines) ALOGW("%s", line.c_str());
- ALOGW("BPF_PROG_LOAD - END log_buf contents.");
-
- if (cs[i].prog_def->optional) {
- ALOGW("failed program is marked optional - continuing...");
- continue;
- }
- ALOGE("non-optional program failed to load.");
- }
- }
-
- if (!fd.ok()) return fd.get();
-
- if (!reuse) {
- if (specified(selinux_context)) {
- string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
- "tmp_prog_" + objName + '_' + string(name);
- ret = bpfFdPin(fd, createLoc.c_str());
- if (ret) {
- int err = errno;
- ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
- return -err;
- }
- ret = renameat2(AT_FDCWD, createLoc.c_str(),
- AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
- if (ret) {
- int err = errno;
- ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
- err, strerror(err));
- return -err;
- }
- } else {
- ret = bpfFdPin(fd, progPinLoc.c_str());
- if (ret) {
- int err = errno;
- ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
- return -err;
- }
- }
- if (chmod(progPinLoc.c_str(), 0440)) {
- int err = errno;
- ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
- return -err;
- }
- if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
- (gid_t)cs[i].prog_def->gid)) {
- int err = errno;
- ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
- cs[i].prog_def->gid, err, strerror(err));
- return -err;
- }
- }
-
- int progId = bpfGetFdProgId(fd);
- if (progId == -1) {
- ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
- } else {
- ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
- }
- }
-
- return 0;
-}
-
-int loadProg(const char* const elfPath, bool* const isCritical, const unsigned int bpfloader_ver,
- const Location& location) {
- vector<char> license;
- vector<char> critical;
- vector<codeSection> cs;
- vector<unique_fd> mapFds;
- int ret;
-
- if (!isCritical) return -1;
- *isCritical = false;
-
- ifstream elfFile(elfPath, ios::in | ios::binary);
- if (!elfFile.is_open()) return -1;
-
- ret = readSectionByName("critical", elfFile, critical);
- *isCritical = !ret;
-
- ret = readSectionByName("license", elfFile, license);
- if (ret) {
- ALOGE("Couldn't find license in %s", elfPath);
- return ret;
- } else {
- ALOGD("Loading %s%s ELF object %s with license %s",
- *isCritical ? "critical for " : "optional", *isCritical ? (char*)critical.data() : "",
- elfPath, (char*)license.data());
- }
-
- // the following default values are for bpfloader V0.0 format which does not include them
- unsigned int bpfLoaderMinVer =
- readSectionUint("bpfloader_min_ver", elfFile, DEFAULT_BPFLOADER_MIN_VER);
- unsigned int bpfLoaderMaxVer =
- readSectionUint("bpfloader_max_ver", elfFile, DEFAULT_BPFLOADER_MAX_VER);
- unsigned int bpfLoaderMinRequiredVer =
- readSectionUint("bpfloader_min_required_ver", elfFile, 0);
- size_t sizeOfBpfMapDef =
- readSectionUint("size_of_bpf_map_def", elfFile, DEFAULT_SIZEOF_BPF_MAP_DEF);
- size_t sizeOfBpfProgDef =
- readSectionUint("size_of_bpf_prog_def", elfFile, DEFAULT_SIZEOF_BPF_PROG_DEF);
-
- // inclusive lower bound check
- if (bpfloader_ver < bpfLoaderMinVer) {
- ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
- bpfloader_ver, elfPath, bpfLoaderMinVer);
- return 0;
- }
-
- // exclusive upper bound check
- if (bpfloader_ver >= bpfLoaderMaxVer) {
- ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
- bpfloader_ver, elfPath, bpfLoaderMaxVer);
- return 0;
- }
-
- if (bpfloader_ver < bpfLoaderMinRequiredVer) {
- ALOGI("BpfLoader version 0x%05x failing due to ELF object %s with required min ver 0x%05x",
- bpfloader_ver, elfPath, bpfLoaderMinRequiredVer);
- return -1;
- }
-
- ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
- bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
-
- if (sizeOfBpfMapDef < DEFAULT_SIZEOF_BPF_MAP_DEF) {
- ALOGE("sizeof(bpf_map_def) of %zu is too small (< %d)", sizeOfBpfMapDef,
- DEFAULT_SIZEOF_BPF_MAP_DEF);
- return -1;
- }
-
- if (sizeOfBpfProgDef < DEFAULT_SIZEOF_BPF_PROG_DEF) {
- ALOGE("sizeof(bpf_prog_def) of %zu is too small (< %d)", sizeOfBpfProgDef,
- DEFAULT_SIZEOF_BPF_PROG_DEF);
- return -1;
- }
-
- ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef);
- if (ret) {
- ALOGE("Couldn't read all code sections in %s", elfPath);
- return ret;
- }
-
- /* Just for future debugging */
- if (0) dumpAllCs(cs);
-
- ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef, bpfloader_ver);
- if (ret) {
- ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
- return ret;
- }
-
- for (int i = 0; i < (int)mapFds.size(); i++)
- ALOGV("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
-
- applyMapRelo(elfFile, mapFds, cs);
-
- ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix, bpfloader_ver);
- if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
-
- return ret;
-}
-
-} // namespace bpf
-} // namespace android
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
deleted file mode 100644
index 4da6830..0000000
--- a/netbpfload/loader.h
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2018-2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <linux/bpf.h>
-
-#include <fstream>
-
-namespace android {
-namespace bpf {
-
-// Bpf programs may specify per-program & per-map selinux_context and pin_subdir.
-//
-// The BpfLoader needs to convert these bpf.o specified strings into an enum
-// for internal use (to check that valid values were specified for the specific
-// location of the bpf.o file).
-//
-// It also needs to map selinux_context's into pin_subdir's.
-// This is because of how selinux_context is actually implemented via pin+rename.
-//
-// Thus 'domain' enumerates all selinux_context's/pin_subdir's that the BpfLoader
-// is aware of. Thus there currently needs to be a 1:1 mapping between the two.
-//
-enum class domain : int {
- unrecognized = -1, // invalid for this version of the bpfloader
- unspecified = 0, // means just use the default for that specific pin location
- tethering, // (S+) fs_bpf_tethering /sys/fs/bpf/tethering
- net_private, // (T+) fs_bpf_net_private /sys/fs/bpf/net_private
- net_shared, // (T+) fs_bpf_net_shared /sys/fs/bpf/net_shared
- netd_readonly, // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
- netd_shared, // (T+) fs_bpf_netd_shared /sys/fs/bpf/netd_shared
-};
-
-// Note: this does not include domain::unrecognized, but does include domain::unspecified
-static constexpr domain AllDomains[] = {
- domain::unspecified,
- domain::tethering,
- domain::net_private,
- domain::net_shared,
- domain::netd_readonly,
- domain::netd_shared,
-};
-
-static constexpr bool unrecognized(domain d) {
- return d == domain::unrecognized;
-}
-
-// Note: this doesn't handle unrecognized, handle it first.
-static constexpr bool specified(domain d) {
- return d != domain::unspecified;
-}
-
-struct Location {
- const char* const dir = "";
- const char* const prefix = "";
-};
-
-// BPF loader implementation. Loads an eBPF ELF object
-int loadProg(const char* elfPath, bool* isCritical, const unsigned int bpfloader_ver,
- const Location &location = {});
-
-// Exposed for testing
-unsigned int readSectionUint(const char* name, std::ifstream& elfFile, unsigned int defVal);
-
-// Returns the build type string (from ro.build.type).
-const std::string& getBuildType();
-
-// The following functions classify the 3 Android build types.
-inline bool isEng() {
- return getBuildType() == "eng";
-}
-inline bool isUser() {
- return getBuildType() == "user";
-}
-inline bool isUserdebug() {
- return getBuildType() == "userdebug";
-}
-
-} // namespace bpf
-} // namespace android
diff --git a/networksecurity/OWNERS b/networksecurity/OWNERS
new file mode 100644
index 0000000..0c838c0
--- /dev/null
+++ b/networksecurity/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 1479456
+
+bessiej@google.com
+sandrom@google.com
+tweek@google.com
diff --git a/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
new file mode 100644
index 0000000..20ecbce
--- /dev/null
+++ b/networksecurity/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "postsubmit": [
+ {
+ "name": "NetworkSecurityUnitTests"
+ }
+ ]
+}
diff --git a/tests/cts/hostside-network-policy/aidl/Android.bp b/networksecurity/framework/Android.bp
similarity index 72%
rename from tests/cts/hostside-network-policy/aidl/Android.bp
rename to networksecurity/framework/Android.bp
index b182090..2b77926 100644
--- a/tests/cts/hostside-network-policy/aidl/Android.bp
+++ b/networksecurity/framework/Android.bp
@@ -1,3 +1,4 @@
+//
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -11,16 +12,20 @@
// 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_framework_backstage_power",
+ default_team: "trendy_team_platform_security",
default_applicable_licenses: ["Android-Apache-2.0"],
}
-java_test_helper_library {
- name: "CtsHostsideNetworkPolicyTestsAidl",
- sdk_version: "current",
+filegroup {
+ name: "framework-networksecurity-sources",
srcs: [
- "com/android/cts/netpolicy/hostside/*.aidl",
+ "src/**/*.java",
+ "src/**/*.aidl",
+ ],
+ path: "src",
+ visibility: [
+ "//packages/modules/Connectivity:__subpackages__",
],
}
diff --git a/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java b/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java
new file mode 100644
index 0000000..94521ae
--- /dev/null
+++ b/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java
@@ -0,0 +1,40 @@
+/*
+ * 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 android.net.ct;
+
+import android.annotation.FlaggedApi;
+import android.annotation.SystemService;
+
+import com.android.net.ct.flags.Flags;
+
+/**
+ * Provides the primary API for the Certificate Transparency Manager.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CERTIFICATE_TRANSPARENCY_SERVICE)
+@SystemService(CertificateTransparencyManager.SERVICE_NAME)
+public final class CertificateTransparencyManager {
+
+ public static final String SERVICE_NAME = "certificate_transparency";
+
+ /**
+ * Creates a new CertificateTransparencyManager instance.
+ *
+ * @hide
+ */
+ public CertificateTransparencyManager() {}
+}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl b/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
similarity index 61%
copy from tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
copy to networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
index 7aac2ab..b5bce7f 100644
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
+++ b/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
@@ -1,11 +1,11 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
+/**
+ * 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
+ * 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,
@@ -13,14 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package android.net.ct;
-package com.android.cts.netpolicy.hostside;
-
-import android.net.NetworkInfo;
-
-@JavaDerive(toString=true)
-parcelable NetworkCheckResult {
- boolean connected;
- String details;
- NetworkInfo networkInfo;
-}
\ No newline at end of file
+/**
+* Interface for communicating with CertificateTransparencyService.
+* @hide
+*/
+interface ICertificateTransparencyManager {}
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
new file mode 100644
index 0000000..a41e6a0
--- /dev/null
+++ b/networksecurity/service/Android.bp
@@ -0,0 +1,50 @@
+// 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_platform_security",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Main lib for Certificate Transparency services.
+java_library {
+ name: "service-networksecurity-pre-jarjar",
+ defaults: ["framework-system-server-module-defaults"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ libs: [
+ "framework-configinfrastructure.stubs.module_lib",
+ "framework-connectivity-pre-jarjar",
+ "service-connectivity-pre-jarjar",
+ ],
+
+ static_libs: [
+ "auto_value_annotations",
+ ],
+
+ plugins: [
+ "auto_value_plugin",
+ "auto_annotation_plugin",
+ ],
+
+ // This is included in service-connectivity which is 30+
+ // TODO (b/293613362): allow APEXes to have service jars with higher min_sdk than the APEX
+ // (service-connectivity is only used on 31+) and use 31 here
+ min_sdk_version: "30",
+ sdk_version: "system_server_current",
+ apex_available: ["com.android.tethering"],
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
new file mode 100644
index 0000000..56a5ee5
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -0,0 +1,296 @@
+/*
+ * 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.net.ct;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.RequiresApi;
+import android.app.DownloadManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.net.ct.DownloadHelper.DownloadStatus;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+
+/** Helper class to download certificate transparency log files. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+class CertificateTransparencyDownloader extends BroadcastReceiver {
+
+ private static final String TAG = "CertificateTransparencyDownloader";
+
+ private final Context mContext;
+ private final DataStore mDataStore;
+ private final DownloadHelper mDownloadHelper;
+ private final SignatureVerifier mSignatureVerifier;
+ private final CertificateTransparencyInstaller mInstaller;
+
+ CertificateTransparencyDownloader(
+ Context context,
+ DataStore dataStore,
+ DownloadHelper downloadHelper,
+ SignatureVerifier signatureVerifier,
+ CertificateTransparencyInstaller installer) {
+ mContext = context;
+ mSignatureVerifier = signatureVerifier;
+ mDataStore = dataStore;
+ mDownloadHelper = downloadHelper;
+ mInstaller = installer;
+ }
+
+ void initialize() {
+ mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
+ mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
+ }
+ }
+
+ long startPublicKeyDownload() {
+ long downloadId = download(mDataStore.getProperty(Config.PUBLIC_KEY_URL));
+ if (downloadId != -1) {
+ mDataStore.setPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, downloadId);
+ mDataStore.store();
+ }
+ return downloadId;
+ }
+
+ long startMetadataDownload() {
+ long downloadId = download(mDataStore.getProperty(Config.METADATA_URL));
+ if (downloadId != -1) {
+ mDataStore.setPropertyLong(Config.METADATA_DOWNLOAD_ID, downloadId);
+ mDataStore.store();
+ }
+ return downloadId;
+ }
+
+ long startContentDownload() {
+ long downloadId = download(mDataStore.getProperty(Config.CONTENT_URL));
+ if (downloadId != -1) {
+ mDataStore.setPropertyLong(Config.CONTENT_DOWNLOAD_ID, downloadId);
+ mDataStore.store();
+ }
+ return downloadId;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (!DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
+ Log.w(TAG, "Received unexpected broadcast with action " + action);
+ return;
+ }
+
+ long completedId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
+ if (completedId == -1) {
+ Log.e(TAG, "Invalid completed download Id");
+ return;
+ }
+
+ if (isPublicKeyDownloadId(completedId)) {
+ handlePublicKeyDownloadCompleted(completedId);
+ return;
+ }
+
+ if (isMetadataDownloadId(completedId)) {
+ handleMetadataDownloadCompleted(completedId);
+ return;
+ }
+
+ if (isContentDownloadId(completedId)) {
+ handleContentDownloadCompleted(completedId);
+ return;
+ }
+
+ Log.i(TAG, "Download id " + completedId + " is not recognized.");
+ }
+
+ private void handlePublicKeyDownloadCompleted(long downloadId) {
+ DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
+ if (!status.isSuccessful()) {
+ handleDownloadFailed(status);
+ return;
+ }
+
+ Uri publicKeyUri = getPublicKeyDownloadUri();
+ if (publicKeyUri == null) {
+ Log.e(TAG, "Invalid public key URI");
+ return;
+ }
+
+ try {
+ mSignatureVerifier.setPublicKeyFrom(publicKeyUri);
+ } catch (GeneralSecurityException | IOException | IllegalArgumentException e) {
+ Log.e(TAG, "Error setting the public Key", e);
+ return;
+ }
+
+ if (startMetadataDownload() == -1) {
+ Log.e(TAG, "Metadata download not started.");
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Metadata download started successfully.");
+ }
+ }
+
+ private void handleMetadataDownloadCompleted(long downloadId) {
+ DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
+ if (!status.isSuccessful()) {
+ handleDownloadFailed(status);
+ return;
+ }
+ if (startContentDownload() == -1) {
+ Log.e(TAG, "Content download not started.");
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Content download started successfully.");
+ }
+ }
+
+ private void handleContentDownloadCompleted(long downloadId) {
+ DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
+ if (!status.isSuccessful()) {
+ handleDownloadFailed(status);
+ return;
+ }
+
+ Uri contentUri = getContentDownloadUri();
+ Uri metadataUri = getMetadataDownloadUri();
+ if (contentUri == null || metadataUri == null) {
+ Log.e(TAG, "Invalid URIs");
+ return;
+ }
+
+ boolean success = false;
+ try {
+ success = mSignatureVerifier.verify(contentUri, metadataUri);
+ } catch (IOException | GeneralSecurityException e) {
+ Log.e(TAG, "Could not verify new log list", e);
+ }
+ if (!success) {
+ Log.w(TAG, "Log list did not pass verification");
+ return;
+ }
+
+ String version = null;
+ try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
+ version =
+ new JSONObject(new String(inputStream.readAllBytes(), UTF_8))
+ .getString("version");
+ } catch (JSONException | IOException e) {
+ Log.e(TAG, "Could not extract version from log list", e);
+ return;
+ }
+
+ try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
+ success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not install new content", e);
+ return;
+ }
+
+ if (success) {
+ // Update information about the stored version on successful install.
+ mDataStore.setProperty(Config.VERSION, version);
+ mDataStore.store();
+ }
+ }
+
+ private void handleDownloadFailed(DownloadStatus status) {
+ Log.e(TAG, "Download failed with " + status);
+ // TODO(378626065): Report failure via statsd.
+ }
+
+ private long download(String url) {
+ try {
+ return mDownloadHelper.startDownload(url);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Download request failed", e);
+ return -1;
+ }
+ }
+
+ @VisibleForTesting
+ long getPublicKeyDownloadId() {
+ return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, -1);
+ }
+
+ @VisibleForTesting
+ long getMetadataDownloadId() {
+ return mDataStore.getPropertyLong(Config.METADATA_DOWNLOAD_ID, -1);
+ }
+
+ @VisibleForTesting
+ long getContentDownloadId() {
+ return mDataStore.getPropertyLong(Config.CONTENT_DOWNLOAD_ID, -1);
+ }
+
+ @VisibleForTesting
+ boolean hasPublicKeyDownloadId() {
+ return getPublicKeyDownloadId() != -1;
+ }
+
+ @VisibleForTesting
+ boolean hasMetadataDownloadId() {
+ return getMetadataDownloadId() != -1;
+ }
+
+ @VisibleForTesting
+ boolean hasContentDownloadId() {
+ return getContentDownloadId() != -1;
+ }
+
+ @VisibleForTesting
+ boolean isPublicKeyDownloadId(long downloadId) {
+ return getPublicKeyDownloadId() == downloadId;
+ }
+
+ @VisibleForTesting
+ boolean isMetadataDownloadId(long downloadId) {
+ return getMetadataDownloadId() == downloadId;
+ }
+
+ @VisibleForTesting
+ boolean isContentDownloadId(long downloadId) {
+ return getContentDownloadId() == downloadId;
+ }
+
+ private Uri getPublicKeyDownloadUri() {
+ return mDownloadHelper.getUri(getPublicKeyDownloadId());
+ }
+
+ private Uri getMetadataDownloadUri() {
+ return mDownloadHelper.getUri(getMetadataDownloadId());
+ }
+
+ private Uri getContentDownloadUri() {
+ return mDownloadHelper.getUri(getContentDownloadId());
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
new file mode 100644
index 0000000..3138ea7
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -0,0 +1,128 @@
+/*
+ * 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.net.ct;
+
+import android.annotation.RequiresApi;
+import android.os.Build;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.security.GeneralSecurityException;
+import java.util.concurrent.Executors;
+
+/** Listener class for the Certificate Transparency Phenotype flags. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
+
+ private static final String TAG = "CertificateTransparencyFlagsListener";
+
+ private final DataStore mDataStore;
+ private final SignatureVerifier mSignatureVerifier;
+ private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+
+ CertificateTransparencyFlagsListener(
+ DataStore dataStore,
+ SignatureVerifier signatureVerifier,
+ CertificateTransparencyDownloader certificateTransparencyDownloader) {
+ mDataStore = dataStore;
+ mSignatureVerifier = signatureVerifier;
+ mCertificateTransparencyDownloader = certificateTransparencyDownloader;
+ }
+
+ void initialize() {
+ mDataStore.load();
+ mCertificateTransparencyDownloader.initialize();
+ DeviceConfig.addOnPropertiesChangedListener(
+ Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
+ }
+ // TODO: handle property changes triggering on boot before registering this listener.
+ }
+
+ @Override
+ public void onPropertiesChanged(Properties properties) {
+ if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
+ return;
+ }
+
+ String newPublicKey =
+ DeviceConfig.getString(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Config.FLAG_PUBLIC_KEY,
+ /* defaultValue= */ "");
+ String newVersion =
+ DeviceConfig.getString(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Config.FLAG_VERSION,
+ /* defaultValue= */ "");
+ String newContentUrl =
+ DeviceConfig.getString(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Config.FLAG_CONTENT_URL,
+ /* defaultValue= */ "");
+ String newMetadataUrl =
+ DeviceConfig.getString(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Config.FLAG_METADATA_URL,
+ /* defaultValue= */ "");
+ if (TextUtils.isEmpty(newPublicKey)
+ || TextUtils.isEmpty(newVersion)
+ || TextUtils.isEmpty(newContentUrl)
+ || TextUtils.isEmpty(newMetadataUrl)) {
+ return;
+ }
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "newPublicKey=" + newPublicKey);
+ Log.d(TAG, "newVersion=" + newVersion);
+ Log.d(TAG, "newContentUrl=" + newContentUrl);
+ Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
+ }
+
+ String oldVersion = mDataStore.getProperty(Config.VERSION);
+ String oldContentUrl = mDataStore.getProperty(Config.CONTENT_URL);
+ String oldMetadataUrl = mDataStore.getProperty(Config.METADATA_URL);
+
+ if (TextUtils.equals(newVersion, oldVersion)
+ && TextUtils.equals(newContentUrl, oldContentUrl)
+ && TextUtils.equals(newMetadataUrl, oldMetadataUrl)) {
+ Log.i(TAG, "No flag changed, ignoring update");
+ return;
+ }
+
+ try {
+ mSignatureVerifier.setPublicKey(newPublicKey);
+ } catch (GeneralSecurityException | IllegalArgumentException e) {
+ Log.e(TAG, "Error setting the public Key", e);
+ return;
+ }
+
+ // TODO: handle the case where there is already a pending download.
+
+ mDataStore.setProperty(Config.CONTENT_URL, newContentUrl);
+ mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
+ mDataStore.store();
+
+ if (mCertificateTransparencyDownloader.startMetadataDownload() == -1) {
+ Log.e(TAG, "Metadata download not started.");
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Metadata download started successfully.");
+ }
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
new file mode 100644
index 0000000..4ca97eb
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
@@ -0,0 +1,92 @@
+/*
+ * 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.net.ct;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Installer of CT log lists. */
+public class CertificateTransparencyInstaller {
+
+ private static final String TAG = "CertificateTransparencyInstaller";
+
+ private final Map<String, CompatibilityVersion> mCompatVersions = new HashMap<>();
+
+ // The CT root directory.
+ private final File mRootDirectory;
+
+ public CertificateTransparencyInstaller(File rootDirectory) {
+ mRootDirectory = rootDirectory;
+ }
+
+ public CertificateTransparencyInstaller(String rootDirectoryPath) {
+ this(new File(rootDirectoryPath));
+ }
+
+ public CertificateTransparencyInstaller() {
+ this(Config.CT_ROOT_DIRECTORY_PATH);
+ }
+
+ void addCompatibilityVersion(String versionName) {
+ removeCompatibilityVersion(versionName);
+ CompatibilityVersion newCompatVersion =
+ new CompatibilityVersion(new File(mRootDirectory, versionName));
+ mCompatVersions.put(versionName, newCompatVersion);
+ }
+
+ void removeCompatibilityVersion(String versionName) {
+ CompatibilityVersion compatVersion = mCompatVersions.remove(versionName);
+ if (compatVersion != null && !compatVersion.delete()) {
+ Log.w(TAG, "Could not delete compatibility version directory.");
+ }
+ }
+
+ CompatibilityVersion getCompatibilityVersion(String versionName) {
+ return mCompatVersions.get(versionName);
+ }
+
+ /**
+ * Install a new log list to use during SCT verification.
+ *
+ * @param compatibilityVersion the compatibility version of the new log list
+ * @param newContent an input stream providing the log list
+ * @param version the minor version of the new log list
+ * @return true if the log list was installed successfully, false otherwise.
+ * @throws IOException if the list cannot be saved in the CT directory.
+ */
+ public boolean install(String compatibilityVersion, InputStream newContent, String version)
+ throws IOException {
+ CompatibilityVersion compatVersion = mCompatVersions.get(compatibilityVersion);
+ if (compatVersion == null) {
+ Log.e(TAG, "No compatibility version for " + compatibilityVersion);
+ return false;
+ }
+ // Ensure root directory exists and is readable.
+ DirectoryUtils.makeDir(mRootDirectory);
+
+ if (!compatVersion.install(newContent, version)) {
+ Log.e(TAG, "Failed to install logs for compatibility version " + compatibilityVersion);
+ return false;
+ }
+ Log.i(TAG, "New logs installed at " + compatVersion.getLogsDir());
+ return true;
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
new file mode 100644
index 0000000..bf23cb0
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -0,0 +1,92 @@
+/*
+ * 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.net.ct;
+
+import android.annotation.RequiresApi;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+
+/** Implementation of the Certificate Transparency job */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class CertificateTransparencyJob extends BroadcastReceiver {
+
+ private static final String TAG = "CertificateTransparencyJob";
+
+ private static final String ACTION_JOB_START = "com.android.server.net.ct.action.JOB_START";
+
+ private final Context mContext;
+ private final DataStore mDataStore;
+ private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+ private final AlarmManager mAlarmManager;
+
+ /** Creates a new {@link CertificateTransparencyJob} object. */
+ public CertificateTransparencyJob(
+ Context context,
+ DataStore dataStore,
+ CertificateTransparencyDownloader certificateTransparencyDownloader) {
+ mContext = context;
+ mDataStore = dataStore;
+ mCertificateTransparencyDownloader = certificateTransparencyDownloader;
+ mAlarmManager = context.getSystemService(AlarmManager.class);
+ }
+
+ void initialize() {
+ mDataStore.load();
+ mCertificateTransparencyDownloader.initialize();
+
+ mContext.registerReceiver(
+ this, new IntentFilter(ACTION_JOB_START), Context.RECEIVER_EXPORTED);
+ mAlarmManager.setInexactRepeating(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime(), // schedule first job at earliest convenient time.
+ AlarmManager.INTERVAL_DAY,
+ PendingIntent.getBroadcast(
+ mContext, 0, new Intent(ACTION_JOB_START), PendingIntent.FLAG_IMMUTABLE));
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob scheduled successfully.");
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!ACTION_JOB_START.equals(intent.getAction())) {
+ Log.w(TAG, "Received unexpected broadcast with action " + intent);
+ return;
+ }
+ if (Config.DEBUG) {
+ Log.d(TAG, "Starting CT daily job.");
+ }
+
+ mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
+ mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
+ mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
+ mDataStore.store();
+
+ if (mCertificateTransparencyDownloader.startPublicKeyDownload() == -1) {
+ Log.e(TAG, "Public key download not started.");
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Public key download started successfully.");
+ }
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
new file mode 100644
index 0000000..92b2b09
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -0,0 +1,80 @@
+/*
+ * 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.net.ct;
+
+import android.annotation.RequiresApi;
+import android.content.Context;
+import android.net.ct.ICertificateTransparencyManager;
+import android.os.Build;
+import android.provider.DeviceConfig;
+
+import com.android.net.ct.flags.Flags;
+import com.android.server.SystemService;
+
+/** Implementation of the Certificate Transparency service. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+
+ private final CertificateTransparencyFlagsListener mFlagsListener;
+ private final CertificateTransparencyJob mCertificateTransparencyJob;
+
+ /**
+ * @return true if the CertificateTransparency service is enabled.
+ */
+ public static boolean enabled(Context context) {
+ return DeviceConfig.getBoolean(
+ Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_SERVICE_ENABLED, false)
+ && Flags.certificateTransparencyService();
+ }
+
+ /** Creates a new {@link CertificateTransparencyService} object. */
+ public CertificateTransparencyService(Context context) {
+ DataStore dataStore = new DataStore(Config.PREFERENCES_FILE);
+ DownloadHelper downloadHelper = new DownloadHelper(context);
+ SignatureVerifier signatureVerifier = new SignatureVerifier(context);
+ CertificateTransparencyDownloader downloader =
+ new CertificateTransparencyDownloader(
+ context,
+ dataStore,
+ downloadHelper,
+ signatureVerifier,
+ new CertificateTransparencyInstaller());
+
+ mFlagsListener =
+ new CertificateTransparencyFlagsListener(dataStore, signatureVerifier, downloader);
+ mCertificateTransparencyJob =
+ new CertificateTransparencyJob(context, dataStore, downloader);
+ }
+
+ /**
+ * Called by {@link com.android.server.ConnectivityServiceInitializer}.
+ *
+ * @see com.android.server.SystemService#onBootPhase
+ */
+ public void onBootPhase(int phase) {
+
+ switch (phase) {
+ case SystemService.PHASE_BOOT_COMPLETED:
+ if (Flags.certificateTransparencyJob()) {
+ mCertificateTransparencyJob.initialize();
+ } else {
+ mFlagsListener.initialize();
+ }
+ break;
+ default:
+ }
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
new file mode 100644
index 0000000..27488b5
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -0,0 +1,135 @@
+/*
+ * 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.net.ct;
+
+import android.system.ErrnoException;
+import android.system.Os;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+/** Represents a compatibility version directory. */
+class CompatibilityVersion {
+
+ static final String LOGS_DIR_PREFIX = "logs-";
+ static final String LOGS_LIST_FILE_NAME = "log_list.json";
+
+ private static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
+
+ private final File mRootDirectory;
+ private final File mCurrentLogsDirSymlink;
+
+ private File mCurrentLogsDir = null;
+
+ CompatibilityVersion(File rootDirectory) {
+ mRootDirectory = rootDirectory;
+ mCurrentLogsDirSymlink = new File(mRootDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+ }
+
+ /**
+ * Installs a log list within this compatibility version directory.
+ *
+ * @param newContent an input stream providing the log list
+ * @param version the version number of the log list
+ * @return true if the log list was installed successfully, false otherwise.
+ * @throws IOException if the list cannot be saved in the CT directory.
+ */
+ boolean install(InputStream newContent, String version) throws IOException {
+ // To support atomically replacing the old configuration directory with the new there's a
+ // bunch of steps. We create a new directory with the logs and then do an atomic update of
+ // the current symlink to point to the new directory.
+ // 1. Ensure that the root directory exists and is readable.
+ DirectoryUtils.makeDir(mRootDirectory);
+
+ File newLogsDir = new File(mRootDirectory, LOGS_DIR_PREFIX + version);
+ // 2. Handle the corner case where the new directory already exists.
+ if (newLogsDir.exists()) {
+ // If the symlink has already been updated then the update died between steps 6 and 7
+ // and so we cannot delete the directory since it is in use.
+ if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
+ deleteOldLogDirectories();
+ return false;
+ }
+ // If the symlink has not been updated then the previous installation failed and this is
+ // a re-attempt. Clean-up leftover files and try again.
+ DirectoryUtils.removeDir(newLogsDir);
+ }
+ try {
+ // 3. Create a new logs-<new_version>/ directory to store the new list.
+ DirectoryUtils.makeDir(newLogsDir);
+
+ // 4. Move the log list json file in logs-<new_version>/ .
+ File logListFile = new File(newLogsDir, LOGS_LIST_FILE_NAME);
+ if (Files.copy(newContent, logListFile.toPath()) == 0) {
+ throw new IOException("The log list appears empty");
+ }
+ DirectoryUtils.setWorldReadable(logListFile);
+
+ // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
+ File tempSymlink = new File(mRootDirectory, "new_symlink");
+ try {
+ Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
+ } catch (ErrnoException e) {
+ throw new IOException("Failed to create symlink", e);
+ }
+
+ // 6. Update the symlink target, this is the actual update step.
+ tempSymlink.renameTo(mCurrentLogsDirSymlink.getAbsoluteFile());
+ } catch (IOException | RuntimeException e) {
+ DirectoryUtils.removeDir(newLogsDir);
+ throw e;
+ }
+ // 7. Cleanup
+ mCurrentLogsDir = newLogsDir;
+ deleteOldLogDirectories();
+ return true;
+ }
+
+ File getRootDir() {
+ return mRootDirectory;
+ }
+
+ File getLogsDir() {
+ return mCurrentLogsDir;
+ }
+
+ File getLogsDirSymlink() {
+ return mCurrentLogsDirSymlink;
+ }
+
+ File getLogsFile() {
+ return new File(mCurrentLogsDir, LOGS_LIST_FILE_NAME);
+ }
+
+ boolean delete() {
+ return DirectoryUtils.removeDir(mRootDirectory);
+ }
+
+ private void deleteOldLogDirectories() throws IOException {
+ if (!mRootDirectory.exists()) {
+ return;
+ }
+ File currentTarget = mCurrentLogsDirSymlink.getCanonicalFile();
+ for (File file : mRootDirectory.listFiles()) {
+ if (!currentTarget.equals(file.getCanonicalFile())
+ && file.getName().startsWith(LOGS_DIR_PREFIX)) {
+ DirectoryUtils.removeDir(file);
+ }
+ }
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
new file mode 100644
index 0000000..70d8e42
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -0,0 +1,63 @@
+/*
+ * 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.net.ct;
+
+import android.content.ApexEnvironment;
+
+import com.android.net.module.util.DeviceConfigUtils;
+
+import java.io.File;
+
+/** Class holding the constants used by the CT feature. */
+final class Config {
+
+ static final boolean DEBUG = false;
+
+ // preferences file
+ private static final File DEVICE_PROTECTED_DATA_DIR =
+ ApexEnvironment.getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME)
+ .getDeviceProtectedDataDir();
+ private static final String PREFERENCES_FILE_NAME = "ct.preferences";
+ static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
+
+ // CT directory
+ static final String CT_ROOT_DIRECTORY_PATH = "/data/misc/keychain/ct/";
+ static final String COMPATIBILITY_VERSION = "v1";
+
+ // Phenotype flags
+ static final String NAMESPACE_NETWORK_SECURITY = "network_security";
+ private static final String FLAGS_PREFIX = "CertificateTransparencyLogList__";
+ static final String FLAG_SERVICE_ENABLED = FLAGS_PREFIX + "service_enabled";
+ static final String FLAG_CONTENT_URL = FLAGS_PREFIX + "content_url";
+ static final String FLAG_METADATA_URL = FLAGS_PREFIX + "metadata_url";
+ static final String FLAG_VERSION = FLAGS_PREFIX + "version";
+ static final String FLAG_PUBLIC_KEY = FLAGS_PREFIX + "public_key";
+
+ // properties
+ static final String VERSION = "version";
+ static final String CONTENT_URL = "content_url";
+ static final String CONTENT_DOWNLOAD_ID = "content_download_id";
+ static final String METADATA_URL = "metadata_url";
+ static final String METADATA_DOWNLOAD_ID = "metadata_download_id";
+ static final String PUBLIC_KEY_URL = "public_key_url";
+ static final String PUBLIC_KEY_DOWNLOAD_ID = "public_key_download_id";
+
+ // URLs
+ static final String URL_PREFIX = "https://www.gstatic.com/android/certificate_transparency/";
+ static final String URL_LOG_LIST = URL_PREFIX + "log_list.json";
+ static final String URL_SIGNATURE = URL_PREFIX + "log_list.sig";
+ static final String URL_PUBLIC_KEY = URL_PREFIX + "log_list.pub";
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
new file mode 100644
index 0000000..cd6aebf
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -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.
+ */
+package com.android.server.net.ct;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Optional;
+import java.util.Properties;
+
+/** Class to persist data needed by CT. */
+class DataStore extends Properties {
+
+ private static final String TAG = "CertificateTransparency";
+
+ private final File mPropertyFile;
+
+ DataStore(File file) {
+ super();
+ mPropertyFile = file;
+ }
+
+ void load() {
+ if (!mPropertyFile.exists()) {
+ return;
+ }
+ try (InputStream in = new FileInputStream(mPropertyFile)) {
+ load(in);
+ } catch (IOException e) {
+ Log.e(TAG, "Error loading property store", e);
+ }
+ }
+
+ void store() {
+ try (OutputStream out = new FileOutputStream(mPropertyFile)) {
+ store(out, "");
+ } catch (IOException e) {
+ Log.e(TAG, "Error storing property store", e);
+ }
+ }
+
+ long getPropertyLong(String key, long defaultValue) {
+ return Optional.ofNullable(getProperty(key)).map(Long::parseLong).orElse(defaultValue);
+ }
+
+ Object setPropertyLong(String key, long value) {
+ return setProperty(key, Long.toString(value));
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
new file mode 100644
index 0000000..ba42a82
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
@@ -0,0 +1,87 @@
+/*
+ * 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.net.ct;
+
+import android.annotation.SuppressLint;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Utility class to manipulate CT directories. */
+class DirectoryUtils {
+
+ static void makeDir(File dir) throws IOException {
+ dir.mkdir();
+ if (!dir.isDirectory()) {
+ throw new IOException("Unable to make directory " + dir.getCanonicalPath());
+ }
+ setWorldReadable(dir);
+ // Needed for the log list file to be accessible.
+ setWorldExecutable(dir);
+ }
+
+ // CT files and directories are readable by all apps.
+ @SuppressLint("SetWorldReadable")
+ static void setWorldReadable(File file) throws IOException {
+ if (!file.setReadable(/* readable= */ true, /* ownerOnly= */ false)) {
+ throw new IOException("Failed to set " + file.getCanonicalPath() + " readable");
+ }
+ }
+
+ // CT directories are executable by all apps, to allow access to the log list by anything on the
+ // device.
+ static void setWorldExecutable(File file) throws IOException {
+ if (!file.isDirectory()) {
+ // Only directories need to be marked as executable to allow for access
+ // to the files inside.
+ // See https://www.redhat.com/en/blog/linux-file-permissions-explained for more details.
+ return;
+ }
+
+ if (!file.setExecutable(/* executable= */ true, /* ownerOnly= */ false)) {
+ throw new IOException("Failed to set " + file.getCanonicalPath() + " executable");
+ }
+ }
+
+ static boolean removeDir(File dir) {
+ return deleteContentsAndDir(dir);
+ }
+
+ private static boolean deleteContentsAndDir(File dir) {
+ if (deleteContents(dir)) {
+ return dir.delete();
+ } else {
+ return false;
+ }
+ }
+
+ private static boolean deleteContents(File dir) {
+ File[] files = dir.listFiles();
+ boolean success = true;
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ success &= deleteContents(file);
+ }
+ if (!file.delete()) {
+ success = false;
+ }
+ }
+ }
+ return success;
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
new file mode 100644
index 0000000..5748416
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
@@ -0,0 +1,143 @@
+/*
+ * 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.net.ct;
+
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.google.auto.value.AutoValue;
+
+/** Class to handle downloads for Certificate Transparency. */
+public class DownloadHelper {
+
+ private final DownloadManager mDownloadManager;
+
+ @VisibleForTesting
+ DownloadHelper(DownloadManager downloadManager) {
+ mDownloadManager = downloadManager;
+ }
+
+ DownloadHelper(Context context) {
+ this(context.getSystemService(DownloadManager.class));
+ }
+
+ /**
+ * Sends a request to start the download of a provided url.
+ *
+ * @param url the url to download
+ * @return a downloadId if the request was created successfully, -1 otherwise.
+ */
+ public long startDownload(String url) {
+ return mDownloadManager.enqueue(
+ new Request(Uri.parse(url))
+ .setAllowedOverRoaming(false)
+ .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
+ .setRequiresCharging(true));
+ }
+
+ /**
+ * Returns the status of the provided download id.
+ *
+ * @param downloadId the download.
+ * @return {@link DownloadStatus} of the download.
+ */
+ public DownloadStatus getDownloadStatus(long downloadId) {
+ DownloadStatus.Builder builder = DownloadStatus.builder().setDownloadId(downloadId);
+ try (Cursor cursor = mDownloadManager.query(new Query().setFilterById(downloadId))) {
+ if (cursor != null && cursor.moveToFirst()) {
+ builder.setStatus(
+ cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)));
+ builder.setReason(
+ cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON)));
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns the URI of the specified download, or null if the download did not complete
+ * successfully.
+ *
+ * @param downloadId the download.
+ * @return the {@link Uri} if the download completed successfully, null otherwise.
+ */
+ public Uri getUri(long downloadId) {
+ if (downloadId == -1) {
+ return null;
+ }
+ return mDownloadManager.getUriForDownloadedFile(downloadId);
+ }
+
+ /** A wrapper around the status and reason Ids returned by the {@link DownloadManager}. */
+ @AutoValue
+ public abstract static class DownloadStatus {
+
+ abstract long downloadId();
+
+ abstract int status();
+
+ abstract int reason();
+
+ boolean isSuccessful() {
+ return status() == DownloadManager.STATUS_SUCCESSFUL;
+ }
+
+ boolean isStorageError() {
+ int status = status();
+ int reason = reason();
+ return status == DownloadManager.STATUS_FAILED
+ && (reason == DownloadManager.ERROR_DEVICE_NOT_FOUND
+ || reason == DownloadManager.ERROR_FILE_ERROR
+ || reason == DownloadManager.ERROR_FILE_ALREADY_EXISTS
+ || reason == DownloadManager.ERROR_INSUFFICIENT_SPACE);
+ }
+
+ boolean isHttpError() {
+ int status = status();
+ int reason = reason();
+ return status == DownloadManager.STATUS_FAILED
+ && (reason == DownloadManager.ERROR_HTTP_DATA_ERROR
+ || reason == DownloadManager.ERROR_TOO_MANY_REDIRECTS
+ || reason == DownloadManager.ERROR_UNHANDLED_HTTP_CODE
+ // If an HTTP error occurred, reason will hold the HTTP status code.
+ || (400 <= reason && reason < 600));
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setDownloadId(long downloadId);
+
+ abstract Builder setStatus(int status);
+
+ abstract Builder setReason(int reason);
+
+ abstract DownloadStatus build();
+ }
+
+ static Builder builder() {
+ return new AutoValue_DownloadHelper_DownloadStatus.Builder()
+ .setDownloadId(-1)
+ .setStatus(-1)
+ .setReason(-1);
+ }
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
new file mode 100644
index 0000000..0b775ca
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -0,0 +1,91 @@
+/*
+ * 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.net.ct;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import java.util.Optional;
+
+/** Verifier of the log list signature. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class SignatureVerifier {
+
+ private final Context mContext;
+
+ @NonNull private Optional<PublicKey> mPublicKey = Optional.empty();
+
+ public SignatureVerifier(Context context) {
+ mContext = context;
+ }
+
+ @VisibleForTesting
+ Optional<PublicKey> getPublicKey() {
+ return mPublicKey;
+ }
+
+ void resetPublicKey() {
+ mPublicKey = Optional.empty();
+ }
+
+ void setPublicKeyFrom(Uri file) throws GeneralSecurityException, IOException {
+ try (InputStream fileStream = mContext.getContentResolver().openInputStream(file)) {
+ setPublicKey(new String(fileStream.readAllBytes()));
+ }
+ }
+
+ void setPublicKey(String publicKey) throws GeneralSecurityException {
+ setPublicKey(
+ KeyFactory.getInstance("RSA")
+ .generatePublic(
+ new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))));
+ }
+
+ @VisibleForTesting
+ void setPublicKey(PublicKey publicKey) {
+ mPublicKey = Optional.of(publicKey);
+ }
+
+ boolean verify(Uri file, Uri signature) throws GeneralSecurityException, IOException {
+ if (!mPublicKey.isPresent()) {
+ throw new InvalidKeyException("Missing public key for signature verification");
+ }
+ Signature verifier = Signature.getInstance("SHA256withRSA");
+ verifier.initVerify(mPublicKey.get());
+ ContentResolver contentResolver = mContext.getContentResolver();
+
+ try (InputStream fileStream = contentResolver.openInputStream(file);
+ InputStream signatureStream = contentResolver.openInputStream(signature)) {
+ verifier.update(fileStream.readAllBytes());
+ return verifier.verify(signatureStream.readAllBytes());
+ }
+ }
+}
diff --git a/networksecurity/tests/unit/Android.bp b/networksecurity/tests/unit/Android.bp
new file mode 100644
index 0000000..11263cf
--- /dev/null
+++ b/networksecurity/tests/unit/Android.bp
@@ -0,0 +1,44 @@
+// 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_platform_security",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "NetworkSecurityUnitTests",
+ defaults: ["mts-target-sdk-version-current"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
+
+ srcs: ["src/**/*.java"],
+
+ libs: [
+ "android.test.base.stubs.test",
+ "android.test.mock.stubs.test",
+ "android.test.runner.stubs.test",
+ ],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "junit",
+ "mockito-target-minus-junit4",
+ "service-networksecurity-pre-jarjar",
+ "truth",
+ ],
+
+ sdk_version: "test_current",
+}
diff --git a/networksecurity/tests/unit/AndroidManifest.xml b/networksecurity/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..7a3f4b7
--- /dev/null
+++ b/networksecurity/tests/unit/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.net.ct">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.net.ct"
+ android:label="NetworkSecurity Mainline Module Tests" />
+</manifest>
diff --git a/networksecurity/tests/unit/AndroidTest.xml b/networksecurity/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..3c94df7
--- /dev/null
+++ b/networksecurity/tests/unit/AndroidTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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="Runs NetworkSecurity Mainline unit Tests.">
+ <option name="test-tag" value="NetworkSecurityUnitTests" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="NetworkSecurityUnitTests.apk" />
+ </target_preparer>
+
+ <option name="config-descriptor:metadata" key="mainline-param"
+ value="com.google.android.tethering.next.apex" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.net.ct" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+
+ <!-- Only run in MTS if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+</configuration>
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
new file mode 100644
index 0000000..ffa1283
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -0,0 +1,453 @@
+/*
+ * 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.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.util.Base64;
+
+/** Tests for the {@link CertificateTransparencyDownloader}. */
+@RunWith(JUnit4.class)
+public class CertificateTransparencyDownloaderTest {
+
+ @Mock private DownloadManager mDownloadManager;
+ @Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
+
+ private PrivateKey mPrivateKey;
+ private PublicKey mPublicKey;
+ private Context mContext;
+ private File mTempFile;
+ private DataStore mDataStore;
+ private SignatureVerifier mSignatureVerifier;
+ private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+
+ private long mNextDownloadId = 666;
+
+ @Before
+ public void setUp() throws IOException, GeneralSecurityException {
+ MockitoAnnotations.initMocks(this);
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ KeyPair keyPair = instance.generateKeyPair();
+ mPrivateKey = keyPair.getPrivate();
+ mPublicKey = keyPair.getPublic();
+
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mTempFile = File.createTempFile("datastore-test", ".properties");
+ mDataStore = new DataStore(mTempFile);
+ mSignatureVerifier = new SignatureVerifier(mContext);
+ mCertificateTransparencyDownloader =
+ new CertificateTransparencyDownloader(
+ mContext,
+ mDataStore,
+ new DownloadHelper(mDownloadManager),
+ mSignatureVerifier,
+ mCertificateTransparencyInstaller);
+
+ prepareDataStore();
+ prepareDownloadManager();
+ }
+
+ @After
+ public void tearDown() {
+ mTempFile.delete();
+ mSignatureVerifier.resetPublicKey();
+ }
+
+ @Test
+ public void testDownloader_startPublicKeyDownload() {
+ assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isFalse();
+ long downloadId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+ assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isTrue();
+ assertThat(mCertificateTransparencyDownloader.isPublicKeyDownloadId(downloadId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_startMetadataDownload() {
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ long downloadId = mCertificateTransparencyDownloader.startMetadataDownload();
+
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
+ assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_startContentDownload() {
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
+ long downloadId = mCertificateTransparencyDownloader.startContentDownload();
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
+ assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_publicKeyDownloadSuccess_updatePublicKey_startMetadataDownload()
+ throws Exception {
+ long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ setSuccessfulDownload(publicKeyId, writePublicKeyToFile(mPublicKey));
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(publicKeyId));
+
+ assertThat(mSignatureVerifier.getPublicKey()).hasValue(mPublicKey);
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
+ }
+
+ @Test
+ public void
+ testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
+ throws Exception {
+ long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ setSuccessfulDownload(
+ publicKeyId, writeToFile("i_am_not_a_base64_encoded_public_key".getBytes()));
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(publicKeyId));
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ }
+
+ @Test
+ public void testDownloader_publicKeyDownloadFail_doNotUpdatePublicKey() throws Exception {
+ long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ setFailedDownload(
+ publicKeyId, // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
+ Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ }
+
+ @Test
+ public void testDownloader_metadataDownloadSuccess_startContentDownload() {
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, new File("log_list.sig"));
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(metadataId));
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
+ }
+
+ @Test
+ public void testDownloader_metadataDownloadFail_doNotStartContentDownload() {
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setFailedDownload(
+ metadataId,
+ // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
+ Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_installSuccess_updateDataStore()
+ throws Exception {
+ String newVersion = "456";
+ File logListFile = makeLogListFile(newVersion);
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+ when(mCertificateTransparencyInstaller.install(
+ eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
+ .thenReturn(true);
+
+ assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ assertInstallSuccessful(newVersion);
+ }
+
+ @Test
+ public void testDownloader_contentDownloadFail_doNotInstall() throws Exception {
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setFailedDownload(
+ contentId,
+ // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
+ Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
+
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+
+ verify(mCertificateTransparencyInstaller, never()).install(any(), any(), any());
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_installFail_doNotUpdateDataStore()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+ when(mCertificateTransparencyInstaller.install(
+ eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
+ .thenReturn(false);
+
+ assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_verificationFail_doNotInstall()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = File.createTempFile("log_list-wrong_metadata", "sig");
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+
+ assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ verify(mCertificateTransparencyInstaller, never())
+ .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_missingVerificationPublicKey_doNotInstall()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.resetPublicKey();
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+
+ assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ verify(mCertificateTransparencyInstaller, never())
+ .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void testDownloader_endToEndSuccess_installNewVersion() throws Exception {
+ String newVersion = "456";
+ File logListFile = makeLogListFile(newVersion);
+ File metadataFile = sign(logListFile);
+ File publicKeyFile = writePublicKeyToFile(mPublicKey);
+
+ assertNoVersionIsInstalled();
+
+ // 1. Start download of public key.
+ long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+ // 2. On successful public key download, set the key and start the metatadata download.
+ setSuccessfulDownload(publicKeyId, publicKeyFile);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(publicKeyId));
+
+ // 3. On successful metadata download, start the content download.
+ long metadataId = mCertificateTransparencyDownloader.getMetadataDownloadId();
+ setSuccessfulDownload(metadataId, metadataFile);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(metadataId));
+
+ // 4. On successful content download, verify the signature and install the new version.
+ long contentId = mCertificateTransparencyDownloader.getContentDownloadId();
+ setSuccessfulDownload(contentId, logListFile);
+ when(mCertificateTransparencyInstaller.install(
+ eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
+ .thenReturn(true);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ assertInstallSuccessful(newVersion);
+ }
+
+ private void assertNoVersionIsInstalled() {
+ assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ }
+
+ private void assertInstallSuccessful(String version) {
+ assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
+ }
+
+ private Intent makeDownloadCompleteIntent(long downloadId) {
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
+ }
+
+ private void prepareDataStore() {
+ mDataStore.load();
+ mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
+ mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
+ mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
+ }
+
+ private void prepareDownloadManager() {
+ when(mDownloadManager.enqueue(any(Request.class)))
+ .thenAnswer(invocation -> mNextDownloadId++);
+ }
+
+ private Cursor makeSuccessfulDownloadCursor() {
+ MatrixCursor cursor =
+ new MatrixCursor(
+ new String[] {
+ DownloadManager.COLUMN_STATUS, DownloadManager.COLUMN_REASON
+ });
+ cursor.addRow(new Object[] {DownloadManager.STATUS_SUCCESSFUL, -1});
+ return cursor;
+ }
+
+ private void setSuccessfulDownload(long downloadId, File file) {
+ when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
+ }
+
+ private Cursor makeFailedDownloadCursor(int error) {
+ MatrixCursor cursor =
+ new MatrixCursor(
+ new String[] {
+ DownloadManager.COLUMN_STATUS, DownloadManager.COLUMN_REASON
+ });
+ cursor.addRow(new Object[] {DownloadManager.STATUS_FAILED, error});
+ return cursor;
+ }
+
+ private void setFailedDownload(long downloadId, int... downloadManagerErrors) {
+ Cursor first = makeFailedDownloadCursor(downloadManagerErrors[0]);
+ Cursor[] others = new Cursor[downloadManagerErrors.length - 1];
+ for (int i = 1; i < downloadManagerErrors.length; i++) {
+ others[i - 1] = makeFailedDownloadCursor(downloadManagerErrors[i]);
+ }
+ when(mDownloadManager.query(any())).thenReturn(first, others);
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
+ }
+
+ private File writePublicKeyToFile(PublicKey publicKey)
+ throws IOException, GeneralSecurityException {
+ return writeToFile(Base64.getEncoder().encode(publicKey.getEncoded()));
+ }
+
+ private File writeToFile(byte[] bytes) throws IOException, GeneralSecurityException {
+ File file = File.createTempFile("temp_file", "tmp");
+
+ try (OutputStream outputStream = new FileOutputStream(file)) {
+ outputStream.write(bytes);
+ }
+
+ return file;
+ }
+
+ private File makeLogListFile(String version) throws IOException, JSONException {
+ File logListFile = File.createTempFile("log_list", "json");
+
+ try (OutputStream outputStream = new FileOutputStream(logListFile)) {
+ outputStream.write(new JSONObject().put("version", version).toString().getBytes(UTF_8));
+ }
+
+ return logListFile;
+ }
+
+ private File sign(File file) throws IOException, GeneralSecurityException {
+ File signatureFile = File.createTempFile("log_list-metadata", "sig");
+ Signature signer = Signature.getInstance("SHA256withRSA");
+ signer.initSign(mPrivateKey);
+
+ try (InputStream fileStream = new FileInputStream(file);
+ OutputStream outputStream = new FileOutputStream(signatureFile)) {
+ signer.update(fileStream.readAllBytes());
+ outputStream.write(signer.sign());
+ }
+
+ return signatureFile;
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
new file mode 100644
index 0000000..50d3f23
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** Tests for the {@link CertificateTransparencyInstaller}. */
+@RunWith(JUnit4.class)
+public class CertificateTransparencyInstallerTest {
+
+ private static final String TEST_VERSION = "test-v1";
+
+ private File mTestDir =
+ new File(
+ InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
+ "test-dir");
+ private CertificateTransparencyInstaller mCertificateTransparencyInstaller =
+ new CertificateTransparencyInstaller(mTestDir);
+
+ @Before
+ public void setUp() {
+ mCertificateTransparencyInstaller.addCompatibilityVersion(TEST_VERSION);
+ }
+
+ @After
+ public void tearDown() {
+ mCertificateTransparencyInstaller.removeCompatibilityVersion(TEST_VERSION);
+ DirectoryUtils.removeDir(mTestDir);
+ }
+
+ @Test
+ public void testCompatibilityVersion_installSuccessful() throws IOException {
+ assertThat(mTestDir.mkdir()).isTrue();
+ String content = "i_am_compatible";
+ String version = "i_am_version";
+ CompatibilityVersion compatVersion =
+ mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+
+ try (InputStream inputStream = asStream(content)) {
+ assertThat(compatVersion.install(inputStream, version)).isTrue();
+ }
+ File logsDir = compatVersion.getLogsDir();
+ assertThat(logsDir.exists()).isTrue();
+ assertThat(logsDir.isDirectory()).isTrue();
+ assertThat(logsDir.getAbsolutePath())
+ .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
+ File logsListFile = compatVersion.getLogsFile();
+ assertThat(logsListFile.exists()).isTrue();
+ assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
+ assertThat(readAsString(logsListFile)).isEqualTo(content);
+ File logsSymlink = compatVersion.getLogsDirSymlink();
+ assertThat(logsSymlink.exists()).isTrue();
+ assertThat(logsSymlink.isDirectory()).isTrue();
+ assertThat(logsSymlink.getAbsolutePath())
+ .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION + "/current");
+ assertThat(logsSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
+
+ assertThat(compatVersion.delete()).isTrue();
+ assertThat(logsDir.exists()).isFalse();
+ assertThat(logsSymlink.exists()).isFalse();
+ assertThat(logsListFile.exists()).isFalse();
+ }
+
+ @Test
+ public void testCompatibilityVersion_versionInstalledFailed() throws IOException {
+ assertThat(mTestDir.mkdir()).isTrue();
+
+ CompatibilityVersion compatVersion =
+ mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+ File rootDir = compatVersion.getRootDir();
+ assertThat(rootDir.mkdir()).isTrue();
+
+ String existingVersion = "666";
+ File existingLogDir =
+ new File(rootDir, CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
+ assertThat(existingLogDir.mkdir()).isTrue();
+
+ String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
+ File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+ assertThat(logsListFile.createNewFile()).isTrue();
+ writeToFile(logsListFile, existingContent);
+
+ String newContent = "i_am_the_real_content";
+ try (InputStream inputStream = asStream(newContent)) {
+ assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
+ }
+
+ assertThat(readAsString(logsListFile)).isEqualTo(newContent);
+ }
+
+ @Test
+ public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException {
+ String content = "i_am_a_certificate_and_i_am_transparent";
+ String version = "666";
+
+ try (InputStream inputStream = asStream(content)) {
+ assertThat(
+ mCertificateTransparencyInstaller.install(
+ TEST_VERSION, inputStream, version))
+ .isTrue();
+ }
+
+ assertThat(mTestDir.exists()).isTrue();
+ assertThat(mTestDir.isDirectory()).isTrue();
+ CompatibilityVersion compatVersion =
+ mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+ File logsDir = compatVersion.getLogsDir();
+ assertThat(logsDir.exists()).isTrue();
+ assertThat(logsDir.isDirectory()).isTrue();
+ assertThat(logsDir.getAbsolutePath())
+ .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
+ File logsListFile = compatVersion.getLogsFile();
+ assertThat(logsListFile.exists()).isTrue();
+ assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
+ assertThat(readAsString(logsListFile)).isEqualTo(content);
+ }
+
+ @Test
+ public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled()
+ throws IOException {
+ String existingVersion = "666";
+ String existingContent = "i_was_already_installed_successfully";
+ CompatibilityVersion compatVersion =
+ mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+
+ DirectoryUtils.makeDir(mTestDir);
+ try (InputStream inputStream = asStream(existingContent)) {
+ assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
+ }
+
+ try (InputStream inputStream = asStream("i_will_be_ignored")) {
+ assertThat(
+ mCertificateTransparencyInstaller.install(
+ TEST_VERSION, inputStream, existingVersion))
+ .isFalse();
+ }
+
+ assertThat(readAsString(compatVersion.getLogsFile())).isEqualTo(existingContent);
+ }
+
+ private static InputStream asStream(String string) throws IOException {
+ return new ByteArrayInputStream(string.getBytes());
+ }
+
+ private static String readAsString(File file) throws IOException {
+ return new String(new FileInputStream(file).readAllBytes());
+ }
+
+ private static void writeToFile(File file, String string) throws IOException {
+ try (OutputStream out = new FileOutputStream(file)) {
+ out.write(string.getBytes());
+ }
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
new file mode 100644
index 0000000..3e670d4
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Tests for the {@link DataStore}. */
+@RunWith(JUnit4.class)
+public class DataStoreTest {
+
+ private File mTempFile;
+ private DataStore mDataStore;
+
+ @Before
+ public void setUp() throws IOException {
+ mTempFile = File.createTempFile("datastore-test", ".properties");
+ mDataStore = new DataStore(mTempFile);
+ }
+
+ @After
+ public void tearDown() {
+ mTempFile.delete();
+ }
+
+ @Test
+ public void testDataStore_propertyFileCreatedSuccessfully() {
+ assertThat(mTempFile.exists()).isTrue();
+ assertThat(mDataStore.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testDataStore_propertySet() {
+ String stringProperty = "prop1";
+ String stringValue = "i_am_a_string";
+ String longProperty = "prop3";
+ long longValue = 9000;
+
+ assertThat(mDataStore.getProperty(stringProperty)).isNull();
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(-1);
+
+ mDataStore.setProperty(stringProperty, stringValue);
+ mDataStore.setPropertyLong(longProperty, longValue);
+
+ assertThat(mDataStore.getProperty(stringProperty)).isEqualTo(stringValue);
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(longValue);
+ }
+
+ @Test
+ public void testDataStore_propertyStore() {
+ String stringProperty = "prop1";
+ String stringValue = "i_am_a_string";
+ String longProperty = "prop3";
+ long longValue = 9000;
+
+ mDataStore.setProperty(stringProperty, stringValue);
+ mDataStore.setPropertyLong(longProperty, longValue);
+ mDataStore.store();
+
+ mDataStore.clear();
+ assertThat(mDataStore.getProperty(stringProperty)).isNull();
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(-1);
+
+ mDataStore.load();
+ assertThat(mDataStore.getProperty(stringProperty)).isEqualTo(stringValue);
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(longValue);
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java
new file mode 100644
index 0000000..0b65e3c
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.app.DownloadManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for the {@link DownloadHelper}. */
+@RunWith(JUnit4.class)
+public class DownloadHelperTest {
+
+ @Mock private DownloadManager mDownloadManager;
+
+ private DownloadHelper mDownloadHelper;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mDownloadHelper = new DownloadHelper(mDownloadManager);
+ }
+
+ @Test
+ public void testDownloadHelper_scheduleDownload() {
+ long downloadId = 666;
+ when(mDownloadManager.enqueue(any())).thenReturn(downloadId);
+
+ assertThat(mDownloadHelper.startDownload("http://test.org")).isEqualTo(downloadId);
+ }
+
+ @Test
+ public void testDownloadHelper_wrongUri() {
+ when(mDownloadManager.enqueue(any())).thenReturn(666L);
+
+ assertThrows(
+ IllegalArgumentException.class, () -> mDownloadHelper.startDownload("not_a_uri"));
+ }
+}
diff --git a/remoteauth/framework/Android.bp b/remoteauth/framework/Android.bp
index 2f1737f..33de139 100644
--- a/remoteauth/framework/Android.bp
+++ b/remoteauth/framework/Android.bp
@@ -47,7 +47,7 @@
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-bluetooth",
+ "framework-bluetooth.stubs.module_lib",
],
static_libs: [
"modules-utils-preconditions",
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index 32ae54f..52f301a 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -33,13 +33,13 @@
],
libs: [
"androidx.annotation_annotation",
- "framework-bluetooth",
- "framework-connectivity",
+ "framework-bluetooth.stubs.module_lib",
+ "framework-connectivity.impl",
"error_prone_annotations",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
- "framework-statsd",
+ "framework-statsd.stubs.module_lib",
],
static_libs: [
"modules-utils-build",
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index fc91e0c..57e3ec9 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -13,7 +13,6 @@
rustlibs: [
"libbinder_rs",
"libjni_legacy",
- "liblazy_static",
"liblog_rust",
"liblogger",
"libnum_traits",
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
index 421fe7e..9add6df 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -21,12 +21,11 @@
use jni::signature::TypeSignature;
use jni::sys::{jbyteArray, jint, jlong, jvalue};
use jni::{JNIEnv, JavaVM};
-use lazy_static::lazy_static;
use log::{debug, error, info};
use std::collections::HashMap;
use std::sync::{
atomic::{AtomicI64, Ordering},
- Arc, Mutex,
+ Arc, LazyLock, Mutex,
};
/// Macro capturing the name of the function calling this macro.
@@ -51,11 +50,9 @@
}};
}
-lazy_static! {
- static ref HANDLE_MAPPING: Mutex<HashMap<i64, Arc<Mutex<JavaPlatform>>>> =
- Mutex::new(HashMap::new());
- static ref HANDLE_RN: AtomicI64 = AtomicI64::new(0);
-}
+static HANDLE_MAPPING: LazyLock<Mutex<HashMap<i64, Arc<Mutex<JavaPlatform>>>>> =
+ LazyLock::new(|| Mutex::new(HashMap::new()));
+static HANDLE_RN: AtomicI64 = AtomicI64::new(0);
fn generate_platform_handle() -> i64 {
HANDLE_RN.fetch_add(1, Ordering::SeqCst)
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 47b9e31..f784b8e 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -30,9 +30,9 @@
srcs: [],
libs: [
- "android.test.base",
- "android.test.mock",
- "android.test.runner",
+ "android.test.base.stubs.test",
+ "android.test.mock.stubs.test",
+ "android.test.runner.stubs.test",
"framework-annotations-lib",
],
compile_multilib: "both",
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 779f354..787e94e 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -51,14 +51,15 @@
],
libs: [
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
// TODO: use framework-tethering-pre-jarjar when it is separated from framework-tethering
"framework-tethering.impl",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"service-connectivity-pre-jarjar",
"service-nearby-pre-jarjar",
+ "service-networksecurity-pre-jarjar",
"service-thread-pre-jarjar",
service_remoteauth_pre_jarjar_lib,
"ServiceConnectivityResources",
@@ -104,7 +105,6 @@
},
srcs: [
"src/com/android/server/connectivity/mdns/**/*.java",
- ":framework-connectivity-t-mdns-standalone-build-sources",
":service-mdns-droidstubs",
],
exclude_srcs: [
diff --git a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
index 450f380..9cca078 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
@@ -39,12 +39,9 @@
uint32_t poll_ms) {
// Always schedule another run of ourselves to recursively poll periodically.
// The task runner is sequential so these can't run on top of each other.
- runner->PostDelayedTask([=]() { PollAndSchedule(runner, poll_ms); }, poll_ms);
+ runner->PostDelayedTask([=, this]() { PollAndSchedule(runner, poll_ms); }, poll_ms);
- if (mMutex.try_lock()) {
- ConsumeAllLocked();
- mMutex.unlock();
- }
+ ConsumeAll();
}
bool NetworkTracePoller::Start(uint32_t pollMs) {
@@ -76,7 +73,10 @@
return false;
}
- mRingBuffer = std::move(*rb);
+ {
+ std::scoped_lock<std::mutex> block(mBufferMutex);
+ mRingBuffer = std::move(*rb);
+ }
auto res = mConfigurationMap.writeValue(0, true, BPF_ANY);
if (!res.ok()) {
@@ -114,10 +114,14 @@
// Drain remaining events from the ring buffer now that tracing is disabled.
// This prevents the next trace from seeing stale events and allows writing
// the last batch of events to Perfetto.
- ConsumeAllLocked();
+ ConsumeAll();
mTaskRunner.reset();
- mRingBuffer.reset();
+
+ {
+ std::scoped_lock<std::mutex> block(mBufferMutex);
+ mRingBuffer.reset();
+ }
return res.ok();
}
@@ -145,22 +149,20 @@
}
bool NetworkTracePoller::ConsumeAll() {
- std::scoped_lock<std::mutex> lock(mMutex);
- return ConsumeAllLocked();
-}
-
-bool NetworkTracePoller::ConsumeAllLocked() {
- if (mRingBuffer == nullptr) {
- ALOGW("Tracing is not active");
- return false;
- }
-
std::vector<PacketTrace> packets;
- base::Result<int> ret = mRingBuffer->ConsumeAll(
- [&](const PacketTrace& pkt) { packets.push_back(pkt); });
- if (!ret.ok()) {
- ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str());
- return false;
+ {
+ std::scoped_lock<std::mutex> lock(mBufferMutex);
+ if (mRingBuffer == nullptr) {
+ ALOGW("Tracing is not active");
+ return false;
+ }
+
+ base::Result<int> ret = mRingBuffer->ConsumeAll(
+ [&](const PacketTrace& pkt) { packets.push_back(pkt); });
+ if (!ret.ok()) {
+ ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str());
+ return false;
+ }
}
ATRACE_INT("NetworkTracePackets", packets.size());
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
index 6bf186a..dd6ed2e 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
@@ -88,6 +88,13 @@
// Connects to the system Perfetto daemon and registers the trace handler.
static void InitPerfettoTracing();
+ // This prevents Perfetto from holding the data source lock when calling
+ // OnSetup, OnStart, or OnStop. The lock is still held by the LockedHandle
+ // returned by GetDataSourceLocked. Disabling this lock prevents a deadlock
+ // where OnStop holds this lock waiting for the poller to stop, but the poller
+ // is running the callback that is trying to acquire the lock.
+ static constexpr bool kRequiresCallbacksUnderLock = false;
+
// When isTest is true, skip non-hermetic code.
NetworkTraceHandler(bool isTest = false) : mIsTest(isTest) {}
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
index 092ab64..72fa66e 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
@@ -50,7 +50,7 @@
bool Stop() EXCLUDES(mMutex);
// Consumes all available events from the ringbuffer.
- bool ConsumeAll() EXCLUDES(mMutex);
+ bool ConsumeAll() EXCLUDES(mBufferMutex);
private:
// Poll the ring buffer for new data and schedule another run of ourselves
@@ -59,15 +59,19 @@
// and thus a deadlock while resetting the TaskRunner. The runner pointer is
// always valid within tasks run by that runner.
void PollAndSchedule(perfetto::base::TaskRunner* runner, uint32_t poll_ms);
- bool ConsumeAllLocked() REQUIRES(mMutex);
// Record sparse iface stats via atrace. This queries the per-iface stats maps
// for any iface present in the vector of packets. This is inexact, but should
// have sufficient coverage given these are cumulative counters.
- void TraceIfaces(const std::vector<PacketTrace>& packets) REQUIRES(mMutex);
+ static void TraceIfaces(const std::vector<PacketTrace>& packets);
std::mutex mMutex;
+ // The mBufferMutex protects the ring buffer. This allows separate protected
+ // access of mTaskRunner in Stop (to terminate) and mRingBuffer in ConsumeAll.
+ // Without this separation, Stop() can deadlock.
+ std::mutex mBufferMutex;
+
// Records the number of successfully started active sessions so that only the
// first active session attempts setup and only the last cleans up. Note that
// the session count will remain zero if Start fails. It is expected that Stop
@@ -78,10 +82,10 @@
uint32_t mPollMs GUARDED_BY(mMutex);
// The function to process PacketTrace, typically a Perfetto sink.
- EventSink mCallback GUARDED_BY(mMutex);
+ const EventSink mCallback;
// The BPF ring buffer handle.
- std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer GUARDED_BY(mMutex);
+ std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer GUARDED_BY(mBufferMutex);
// The packet tracing config map (really a 1-element array).
BpfMap<uint32_t, bool> mConfigurationMap GUARDED_BY(mMutex);
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 1ac2f6e..5d23fdc 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -28,6 +28,7 @@
import com.android.server.ethernet.EthernetService;
import com.android.server.ethernet.EthernetServiceImpl;
import com.android.server.nearby.NearbyService;
+import com.android.server.net.ct.CertificateTransparencyService;
import com.android.server.thread.ThreadNetworkService;
/**
@@ -43,6 +44,7 @@
private final NearbyService mNearbyService;
private final EthernetServiceImpl mEthernetServiceImpl;
private final ThreadNetworkService mThreadNetworkService;
+ private final CertificateTransparencyService mCertificateTransparencyService;
public ConnectivityServiceInitializer(Context context) {
super(context);
@@ -55,6 +57,7 @@
mNsdService = createNsdService(context);
mNearbyService = createNearbyService(context);
mThreadNetworkService = createThreadNetworkService(context);
+ mCertificateTransparencyService = createCertificateTransparencyService(context);
}
@Override
@@ -111,6 +114,10 @@
if (mThreadNetworkService != null) {
mThreadNetworkService.onBootPhase(phase);
}
+
+ if (SdkLevel.isAtLeastV() && mCertificateTransparencyService != null) {
+ mCertificateTransparencyService.onBootPhase(phase);
+ }
}
/**
@@ -186,4 +193,13 @@
}
return new ThreadNetworkService(context);
}
+
+ /** Return CertificateTransparencyService instance if enable, otherwise null. */
+ @Nullable
+ private CertificateTransparencyService createCertificateTransparencyService(
+ final Context context) {
+ return SdkLevel.isAtLeastV() && CertificateTransparencyService.enabled(context)
+ ? new CertificateTransparencyService(context)
+ : null;
+ }
}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 64624ae..0adb290 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -93,6 +93,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.InetAddressUtils;
import com.android.net.module.util.PermissionUtils;
@@ -768,7 +769,7 @@
private Set<String> dedupSubtypeLabels(Collection<String> subtypes) {
final Map<String, String> subtypeMap = new LinkedHashMap<>(subtypes.size());
for (String subtype : subtypes) {
- subtypeMap.put(MdnsUtils.toDnsLowerCase(subtype), subtype);
+ subtypeMap.put(DnsUtils.toDnsUpperCase(subtype), subtype);
}
return new ArraySet<>(subtypeMap.values());
}
@@ -1935,8 +1936,27 @@
mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE))
.setIsQueryWithKnownAnswerEnabled(mDeps.isFeatureEnabled(
mContext, MdnsFeatureFlags.NSD_QUERY_WITH_KNOWN_ANSWER))
- .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
- mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
+ .setAvoidAdvertisingEmptyTxtRecords(mDeps.isTetheringFeatureNotChickenedOut(
+ mContext, MdnsFeatureFlags.NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS))
+ .setIsCachedServicesRemovalEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_CACHED_SERVICES_REMOVAL))
+ .setCachedServicesRetentionTime(mDeps.getDeviceConfigPropertyInt(
+ MdnsFeatureFlags.NSD_CACHED_SERVICES_RETENTION_TIME,
+ MdnsFeatureFlags.DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS))
+ .setOverrideProvider(new MdnsFeatureFlags.FlagOverrideProvider() {
+ @Override
+ public boolean isForceEnabledForTest(@NonNull String flag) {
+ return mDeps.isFeatureEnabled(
+ mContext,
+ FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag);
+ }
+
+ @Override
+ public int getIntValueForTest(@NonNull String flag, int defaultValue) {
+ return mDeps.getDeviceConfigPropertyInt(
+ FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag, defaultValue);
+ }
+ })
.build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
@@ -2003,6 +2023,14 @@
}
/**
+ * @see DeviceConfigUtils#getDeviceConfigPropertyInt
+ */
+ public int getDeviceConfigPropertyInt(String feature, int defaultValue) {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(
+ NAMESPACE_TETHERING, feature, defaultValue);
+ }
+
+ /**
* @see MdnsDiscoveryManager
*/
public MdnsDiscoveryManager makeMdnsDiscoveryManager(
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index 54943c7..f55db93 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -24,6 +24,7 @@
import android.util.Pair;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -193,7 +194,7 @@
// Ignore any PTR records that don't match the current query.
if (!CollectionUtils.any(questions,
q -> q instanceof MdnsPointerRecord
- && MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+ && DnsUtils.equalsDnsLabelIgnoreDnsCase(
q.getName(), ptrRecord.getName()))) {
continue;
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index b870477..9c52eca 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -41,6 +41,7 @@
import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -272,7 +273,7 @@
return true;
}
// Check if it conflicts with the default hostname.
- return MdnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
+ return DnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
}
private void updateRegistrationUntilNoConflict(
@@ -436,8 +437,8 @@
continue;
}
final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
- if (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
- && MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
+ if (DnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
+ && DnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
other.getServiceType())) {
return mPendingRegistrations.keyAt(i);
}
@@ -463,13 +464,13 @@
final NsdServiceInfo otherInfo = otherRegistration.getServiceInfo();
final int otherServiceId = mPendingRegistrations.keyAt(i);
if (clientUid != otherRegistration.mClientUid
- && MdnsUtils.equalsIgnoreDnsCase(
+ && DnsUtils.equalsIgnoreDnsCase(
info.getHostname(), otherInfo.getHostname())) {
return otherServiceId;
}
if (!info.getHostAddresses().isEmpty()
&& !otherInfo.getHostAddresses().isEmpty()
- && MdnsUtils.equalsIgnoreDnsCase(
+ && DnsUtils.equalsIgnoreDnsCase(
info.getHostname(), otherInfo.getHostname())) {
return otherServiceId;
}
@@ -849,7 +850,7 @@
sharedLog.wtf("Invalid priority in config_nsdOffloadServicesPriority: " + entry);
continue;
}
- priorities.put(MdnsUtils.toDnsLowerCase(priorityAndType[1]), priority);
+ priorities.put(DnsUtils.toDnsUpperCase(priorityAndType[1]), priority);
}
return priorities;
}
@@ -995,7 +996,7 @@
@NonNull Registration registration, byte[] rawOffloadPacket) {
final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
final Integer mapPriority = mServiceTypeToOffloadPriority.get(
- MdnsUtils.toDnsLowerCase(nsdServiceInfo.getServiceType()));
+ DnsUtils.toDnsUpperCase(nsdServiceInfo.getServiceType()));
// Higher values of priority are less prioritized
final int priority = mapPriority == null ? Integer.MAX_VALUE : mapPriority;
final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 0ab7a76..c833422 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns;
+import static com.android.internal.annotations.VisibleForTesting.Visibility;
+
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -30,8 +32,9 @@
import androidx.annotation.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.DnsUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.io.PrintWriter;
@@ -66,8 +69,8 @@
public void put(@NonNull String serviceType, @NonNull SocketKey socketKey,
@NonNull MdnsServiceTypeClient client) {
- final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
- final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+ final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
+ final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
socketKey);
clients.put(perSocketServiceType, client);
}
@@ -75,18 +78,18 @@
@Nullable
public MdnsServiceTypeClient get(
@NonNull String serviceType, @NonNull SocketKey socketKey) {
- final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
- final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+ final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
+ final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
socketKey);
return clients.getOrDefault(perSocketServiceType, null);
}
public List<MdnsServiceTypeClient> getByServiceType(@NonNull String serviceType) {
- final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
+ final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
final List<MdnsServiceTypeClient> list = new ArrayList<>();
for (int i = 0; i < clients.size(); i++) {
final Pair<String, SocketKey> perSocketServiceType = clients.keyAt(i);
- if (dnsLowerServiceType.equals(perSocketServiceType.first)) {
+ if (dnsUpperServiceType.equals(perSocketServiceType.first)) {
list.add(clients.valueAt(i));
}
}
@@ -133,13 +136,20 @@
this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper());
}
- private static class DiscoveryExecutor implements Executor {
+ /**
+ * A utility class to generate a handler, optionally with a looper, and to run functions on the
+ * newly created handler.
+ */
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ static class DiscoveryExecutor implements Executor {
private final HandlerThread handlerThread;
@GuardedBy("pendingTasks")
@Nullable private Handler handler;
+ // Store pending tasks and associated delay time. Each Pair represents a pending task
+ // (first) and its delay time (second).
@GuardedBy("pendingTasks")
- @NonNull private final ArrayList<Runnable> pendingTasks = new ArrayList<>();
+ @NonNull private final ArrayList<Pair<Runnable, Long>> pendingTasks = new ArrayList<>();
DiscoveryExecutor(@Nullable Looper defaultLooper) {
if (defaultLooper != null) {
@@ -153,8 +163,8 @@
protected void onLooperPrepared() {
synchronized (pendingTasks) {
handler = new Handler(getLooper());
- for (Runnable pendingTask : pendingTasks) {
- handler.post(pendingTask);
+ for (Pair<Runnable, Long> pendingTask : pendingTasks) {
+ handler.postDelayed(pendingTask.first, pendingTask.second);
}
pendingTasks.clear();
}
@@ -176,16 +186,20 @@
@Override
public void execute(Runnable function) {
+ executeDelayed(function, 0L /* delayMillis */);
+ }
+
+ public void executeDelayed(Runnable function, long delayMillis) {
final Handler handler;
synchronized (pendingTasks) {
if (this.handler == null) {
- pendingTasks.add(function);
+ pendingTasks.add(Pair.create(function, delayMillis));
return;
} else {
handler = this.handler;
}
}
- handler.post(function);
+ handler.postDelayed(function, delayMillis);
}
void shutDown() {
@@ -196,9 +210,22 @@
void ensureRunningOnHandlerThread() {
synchronized (pendingTasks) {
- MdnsUtils.ensureRunningOnHandlerThread(handler);
+ HandlerUtils.ensureRunningOnHandlerThread(handler);
}
}
+
+ public void runWithScissorsForDumpIfReady(@NonNull Runnable function) {
+ final Handler handler;
+ synchronized (pendingTasks) {
+ if (this.handler == null) {
+ Log.d(TAG, "The handler is not ready. Ignore the DiscoveryManager dump");
+ return;
+ } else {
+ handler = this.handler;
+ }
+ }
+ HandlerUtils.runWithScissorsForDump(handler, function, 10_000);
+ }
}
/**
@@ -287,6 +314,17 @@
serviceTypeClient.notifySocketDestroyed();
executorProvider.shutdownExecutorService(serviceTypeClient.getExecutor());
perSocketServiceTypeClients.remove(serviceTypeClient);
+ // The cached services may not be reliable after the socket is disconnected,
+ // the service type client won't receive any updates for them. Therefore,
+ // remove these cached services after exceeding the retention time
+ // (currently 10s) if no service type client requires them.
+ if (mdnsFeatureFlags.isCachedServicesRemovalEnabled()) {
+ final MdnsServiceCache.CacheKey cacheKey =
+ serviceTypeClient.getCacheKey();
+ discoveryExecutor.executeDelayed(
+ () -> handleRemoveCachedServices(cacheKey),
+ mdnsFeatureFlags.getCachedServicesRetentionTime());
+ }
}
});
}
@@ -323,6 +361,42 @@
// of the service type clients.
executorProvider.shutdownExecutorService(serviceTypeClient.getExecutor());
perSocketServiceTypeClients.remove(serviceTypeClient);
+ // The cached services may not be reliable after the socket is disconnected, the
+ // service type client won't receive any updates for them. Therefore, remove these
+ // cached services after exceeding the retention time (currently 10s) if no service
+ // type client requires them.
+ // Note: This removal is only called if the requested socket is still active for
+ // other requests. If the requested socket is no longer needed after the listener
+ // is unregistered, SocketCreationCallback#onSocketDestroyed callback will remove
+ // both the service type client and cached services there.
+ //
+ // List some multiple listener cases for the cached service removal flow.
+ //
+ // Case 1 - Same service type, different network requests
+ // - Register Listener A (service type X, requesting all networks: Y and Z)
+ // - Create service type clients X-Y and X-Z
+ // - Register Listener B (service type X, requesting network Y)
+ // - Reuse service type client X-Y
+ // - Unregister Listener A
+ // - Socket destroyed on network Z; remove the X-Z client. Unregister the listener
+ // from the X-Y client and keep it, as it's still being used by Listener B.
+ // - Remove cached services associated with the X-Z client after 10 seconds.
+ //
+ // Case 2 - Different service types, same network request
+ // - Register Listener A (service type X, requesting network Y)
+ // - Create service type client X-Y
+ // - Register Listener B (service type Z, requesting network Y)
+ // - Create service type client Z-Y
+ // - Unregister Listener A
+ // - No socket is destroyed because network Y is still being used by Listener B.
+ // - Unregister the listener from the X-Y client, then remove it.
+ // - Remove cached services associated with the X-Y client after 10 seconds.
+ if (mdnsFeatureFlags.isCachedServicesRemovalEnabled()) {
+ final MdnsServiceCache.CacheKey cacheKey = serviceTypeClient.getCacheKey();
+ discoveryExecutor.executeDelayed(
+ () -> handleRemoveCachedServices(cacheKey),
+ mdnsFeatureFlags.getCachedServicesRetentionTime());
+ }
}
}
if (perSocketServiceTypeClients.isEmpty()) {
@@ -367,6 +441,26 @@
}
}
+ private void handleRemoveCachedServices(@NonNull MdnsServiceCache.CacheKey cacheKey) {
+ // Check if there is an active service type client that requires the cached services. If so,
+ // do not remove associated services from cache.
+ for (MdnsServiceTypeClient client : getMdnsServiceTypeClient(cacheKey.mSocketKey)) {
+ if (client.getCacheKey().equals(cacheKey)) {
+ // Found a client that has same CacheKey.
+ return;
+ }
+ }
+ sharedLog.log("Remove cached services for " + cacheKey);
+ // No client has same CacheKey. Remove associated services.
+ getServiceCache().removeServices(cacheKey);
+ }
+
+ @VisibleForTesting
+ @NonNull
+ MdnsServiceCache getServiceCache() {
+ return serviceCache;
+ }
+
@VisibleForTesting
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@NonNull SocketKey socketKey) {
@@ -388,13 +482,19 @@
* Dump DiscoveryManager state.
*/
public void dump(PrintWriter pw) {
- discoveryExecutor.checkAndRunOnHandlerThread(() -> {
- pw.println();
+ discoveryExecutor.runWithScissorsForDumpIfReady(() -> {
+ pw.println("Clients:");
// Dump ServiceTypeClients
for (MdnsServiceTypeClient serviceTypeClient
: perSocketServiceTypeClients.getAllMdnsServiceTypeClient()) {
serviceTypeClient.dump(pw);
}
+ pw.println();
+ // Dump ServiceCache
+ pw.println("Cached services:");
+ if (serviceCache != null) {
+ serviceCache.dump(pw, " ");
+ }
});
}
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index c264f25..4e27fef 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -67,6 +67,28 @@
*/
public static final String NSD_QUERY_WITH_KNOWN_ANSWER = "nsd_query_with_known_answer";
+ /**
+ * A feature flag to avoid advertising empty TXT records, as per RFC 6763 6.1.
+ */
+ public static final String NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS =
+ "nsd_avoid_advertising_empty_txt_records";
+
+ /**
+ * A feature flag to control whether the cached services removal should be enabled.
+ * The removal will be triggered if the retention time has elapsed after all listeners have been
+ * unregistered from the service type client or the interface has been destroyed.
+ */
+ public static final String NSD_CACHED_SERVICES_REMOVAL = "nsd_cached_services_removal";
+
+ /**
+ * A feature flag to control the retention time for cached services.
+ *
+ * <p> Making the retention time configurable allows for testing and future adjustments.
+ */
+ public static final String NSD_CACHED_SERVICES_RETENTION_TIME =
+ "nsd_cached_services_retention_time";
+ public static final int DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS = 10000;
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -91,6 +113,15 @@
// Flag for query with known-answer
public final boolean mIsQueryWithKnownAnswerEnabled;
+ // Flag for avoiding advertising empty TXT records
+ public final boolean mAvoidAdvertisingEmptyTxtRecords;
+
+ // Flag for cached services removal
+ public final boolean mIsCachedServicesRemovalEnabled;
+
+ // Retention Time for cached services
+ public final long mCachedServicesRetentionTime;
+
@Nullable
private final FlagOverrideProvider mOverrideProvider;
@@ -102,6 +133,12 @@
* Indicates whether the flag should be force-enabled for testing purposes.
*/
boolean isForceEnabledForTest(@NonNull String flag);
+
+
+ /**
+ * Get the int value of the flag for testing purposes.
+ */
+ int getIntValueForTest(@NonNull String flag, int defaultValue);
}
/**
@@ -112,6 +149,19 @@
}
/**
+ * Get the int value of the flag for testing purposes.
+ *
+ * @return the test int value, or given default value if it is unset or the OverrideProvider
+ * doesn't exist.
+ */
+ private int getIntValueForTest(@NonNull String flag, int defaultValue) {
+ if (mOverrideProvider == null) {
+ return defaultValue;
+ }
+ return mOverrideProvider.getIntValueForTest(flag, defaultValue);
+ }
+
+ /**
* Indicates whether {@link #NSD_UNICAST_REPLY_ENABLED} is enabled, including for testing.
*/
public boolean isUnicastReplyEnabled() {
@@ -142,6 +192,32 @@
}
/**
+ * Indicates whether {@link #NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS} is enabled, including for
+ * testing.
+ */
+ public boolean avoidAdvertisingEmptyTxtRecords() {
+ return mAvoidAdvertisingEmptyTxtRecords
+ || isForceEnabledForTest(NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS);
+ }
+
+ /**
+ * Indicates whether {@link #NSD_CACHED_SERVICES_REMOVAL} is enabled, including for testing.
+ */
+ public boolean isCachedServicesRemovalEnabled() {
+ return mIsCachedServicesRemovalEnabled
+ || isForceEnabledForTest(NSD_CACHED_SERVICES_REMOVAL);
+ }
+
+ /**
+ * Get the value which is set to {@link #NSD_CACHED_SERVICES_RETENTION_TIME}, including for
+ * testing.
+ */
+ public long getCachedServicesRetentionTime() {
+ return getIntValueForTest(
+ NSD_CACHED_SERVICES_RETENTION_TIME, (int) mCachedServicesRetentionTime);
+ }
+
+ /**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -152,6 +228,9 @@
boolean isUnicastReplyEnabled,
boolean isAggressiveQueryModeEnabled,
boolean isQueryWithKnownAnswerEnabled,
+ boolean avoidAdvertisingEmptyTxtRecords,
+ boolean isCachedServicesRemovalEnabled,
+ long cachedServicesRetentionTime,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -161,6 +240,9 @@
mIsUnicastReplyEnabled = isUnicastReplyEnabled;
mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+ mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
+ mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
+ mCachedServicesRetentionTime = cachedServicesRetentionTime;
mOverrideProvider = overrideProvider;
}
@@ -181,6 +263,9 @@
private boolean mIsUnicastReplyEnabled;
private boolean mIsAggressiveQueryModeEnabled;
private boolean mIsQueryWithKnownAnswerEnabled;
+ private boolean mAvoidAdvertisingEmptyTxtRecords;
+ private boolean mIsCachedServicesRemovalEnabled;
+ private long mCachedServicesRetentionTime;
private FlagOverrideProvider mOverrideProvider;
/**
@@ -195,6 +280,9 @@
mIsUnicastReplyEnabled = true; // Default enabled.
mIsAggressiveQueryModeEnabled = false;
mIsQueryWithKnownAnswerEnabled = false;
+ mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
+ mIsCachedServicesRemovalEnabled = false;
+ mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
mOverrideProvider = null;
}
@@ -291,6 +379,36 @@
}
/**
+ * Set whether to avoid advertising empty TXT records.
+ *
+ * @see #NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS
+ */
+ public Builder setAvoidAdvertisingEmptyTxtRecords(boolean avoidAdvertisingEmptyTxtRecords) {
+ mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
+ return this;
+ }
+
+ /**
+ * Set whether the cached services removal is enabled.
+ *
+ * @see #NSD_CACHED_SERVICES_REMOVAL
+ */
+ public Builder setIsCachedServicesRemovalEnabled(boolean isCachedServicesRemovalEnabled) {
+ mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
+ return this;
+ }
+
+ /**
+ * Set cached services retention time.
+ *
+ * @see #NSD_CACHED_SERVICES_RETENTION_TIME
+ */
+ public Builder setCachedServicesRetentionTime(long cachedServicesRetentionTime) {
+ mCachedServicesRetentionTime = cachedServicesRetentionTime;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
@@ -302,6 +420,9 @@
mIsUnicastReplyEnabled,
mIsAggressiveQueryModeEnabled,
mIsQueryWithKnownAnswerEnabled,
+ mAvoidAdvertisingEmptyTxtRecords,
+ mIsCachedServicesRemovalEnabled,
+ mCachedServicesRetentionTime,
mOverrideProvider);
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
index 4399f2d..3eae3c7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
@@ -18,8 +18,6 @@
import android.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -29,7 +27,6 @@
import java.util.Objects;
/** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
-@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsInetAddressRecord extends MdnsRecord {
@Nullable private Inet6Address inet6Address;
@Nullable private Inet4Address inet4Address;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 0b2003f..58defa9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -416,13 +416,6 @@
// recvbuf and src are reused after this returns; ensure references to src are not kept.
final InetSocketAddress srcCopy = new InetSocketAddress(src.getAddress(), src.getPort());
- if (DBG) {
- mSharedLog.v("Parsed packet with " + packet.questions.size() + " questions, "
- + packet.answers.size() + " answers, "
- + packet.authorityRecords.size() + " authority, "
- + packet.additionalRecords.size() + " additional from " + srcCopy);
- }
-
Map<Integer, Integer> conflictingServices =
mRecordRepository.getConflictingServices(packet);
@@ -440,7 +433,14 @@
// answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the
// conflicting service is still probing and won't reply either.
final MdnsReplyInfo answers = mRecordRepository.getReply(packet, srcCopy);
-
+ // Dump the query packet.
+ if (DBG || answers != null) {
+ mSharedLog.v("Parsed packet with transactionId(" + packet.transactionId + "): "
+ + packet.questions.size() + " questions, "
+ + packet.answers.size() + " answers, "
+ + packet.authorityRecords.size() + " authority, "
+ + packet.additionalRecords.size() + " additional from " + srcCopy);
+ }
if (answers == null) return;
mReplySender.queueReply(answers);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index c575d40..36fad31 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -16,7 +16,7 @@
package com.android.server.connectivity.mdns;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
index 1239180..a5b8803 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
@@ -20,7 +20,7 @@
import android.text.TextUtils;
import com.android.net.module.util.CollectionUtils;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
import java.io.IOException;
import java.util.ArrayList;
@@ -153,7 +153,7 @@
@Override
public int hashCode() {
return Objects.hash(super.hashCode(),
- Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(mNextDomain)),
+ Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(mNextDomain)),
Arrays.hashCode(mTypes));
}
@@ -167,7 +167,7 @@
}
return super.equals(other)
- && MdnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
+ && DnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
((MdnsNsecRecord) other).mNextDomain)
&& Arrays.equals(mTypes, ((MdnsNsecRecord) other).mTypes);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
index e84cead..cfd8e9a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
@@ -27,6 +27,7 @@
import android.os.Looper;
import android.os.Message;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.SharedLog;
import java.io.IOException;
@@ -167,9 +168,7 @@
* @return true if probing was in progress, false if this was a no-op
*/
public boolean stop(int id) {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException("stop can only be called from the looper thread");
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
// Since this is run on the looper thread, messages cannot be currently processing and are
// all in the handler queue; unless this method is called from a message, but the current
// message cannot be cancelled.
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
index 6879a64..cf788be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -16,8 +16,8 @@
package com.android.server.connectivity.mdns;
+import com.android.net.module.util.DnsUtils;
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.net.DatagramPacket;
@@ -180,7 +180,7 @@
int existingOffset = entry.getKey();
String[] existingLabels = entry.getValue();
- if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
+ if (DnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
writePointer(existingOffset);
return;
} else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
index e5c90a4..e8f5e71 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -18,15 +18,12 @@
import android.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
import java.io.IOException;
import java.util.Arrays;
/** An mDNS "PTR" record, which holds a name (the "pointer"). */
-@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsPointerRecord extends MdnsRecord {
private String[] pointer;
@@ -68,7 +65,7 @@
}
public boolean hasSubtype() {
- return (name != null) && (name.length > 2) && MdnsUtils.equalsIgnoreDnsCase(name[1],
+ return (name != null) && (name.length > 2) && DnsUtils.equalsIgnoreDnsCase(name[1],
MdnsConstants.SUBTYPE_LABEL);
}
@@ -83,7 +80,7 @@
@Override
public int hashCode() {
- return (super.hashCode() * 31) + Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(pointer));
+ return (super.hashCode() * 31) + Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(pointer));
}
@Override
@@ -95,7 +92,7 @@
return false;
}
- return super.equals(other) && MdnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
+ return super.equals(other) && DnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
((MdnsPointerRecord) other).pointer);
}
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
index e88947a..4a44fff 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -23,8 +23,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.util.ArrayList;
import java.util.Collections;
@@ -114,7 +114,7 @@
private static boolean containsName(@NonNull List<MdnsRecord> records,
@NonNull String[] name) {
return CollectionUtils.any(records,
- r -> MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
+ r -> DnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
index 3fcf0d4..356b738 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -63,17 +63,22 @@
* rescheduling is not necessary.
*/
@Nullable
- public ScheduledQueryTaskArgs maybeRescheduleCurrentRun(long now,
- long minRemainingTtl, long lastSentTime, long sessionId) {
+ public ScheduledQueryTaskArgs maybeRescheduleCurrentRun(
+ long now,
+ long minRemainingTtl,
+ long lastSentTime,
+ long sessionId,
+ int numOfQueriesBeforeBackoff) {
if (mLastScheduledQueryTaskArgs == null) {
return null;
}
- if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff()) {
+ if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff(numOfQueriesBeforeBackoff)) {
return null;
}
final long timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
- mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime);
+ mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime,
+ numOfQueriesBeforeBackoff, false /* forceEnableBackoff */);
if (timeToRun <= mLastScheduledQueryTaskArgs.timeToRun) {
return null;
@@ -95,14 +100,18 @@
long minRemainingTtl,
long now,
long lastSentTime,
- long sessionId) {
- final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun();
- final long timeToRun;
- if (mLastScheduledQueryTaskArgs == null) {
- timeToRun = now + nextRunConfig.delayUntilNextTaskWithoutBackoffMs;
+ long sessionId,
+ int queryMode,
+ int numOfQueriesBeforeBackoff,
+ boolean forceEnableBackoff) {
+ final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
+ long timeToRun;
+ if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
+ timeToRun = now + nextRunConfig.getDelayBeforeTaskWithoutBackoff();
} else {
timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
- nextRunConfig, now, minRemainingTtl, lastSentTime);
+ nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff,
+ forceEnableBackoff);
}
mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(nextRunConfig, timeToRun,
minRemainingTtl + now,
@@ -121,10 +130,12 @@
return mLastScheduledQueryTaskArgs;
}
- private static long calculateTimeToRun(@NonNull ScheduledQueryTaskArgs taskArgs,
- QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime) {
- final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
- if (!queryTaskConfig.shouldUseQueryBackoff()) {
+ private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
+ QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
+ int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
+ final long baseDelayInMs = queryTaskConfig.getDelayBeforeTaskWithoutBackoff();
+ if (!(forceEnableBackoff
+ || queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff))) {
return lastSentTime + baseDelayInMs;
}
if (minRemainingTtl <= 0) {
@@ -133,7 +144,7 @@
return lastSentTime + baseDelayInMs;
}
// If the next TTL expiration time hasn't changed, then use previous calculated timeToRun.
- if (lastSentTime < now
+ if (lastSentTime < now && taskArgs != null
&& taskArgs.minTtlExpirationTimeWhenScheduled == now + minRemainingTtl) {
// Use the original scheduling time if the TTL has not changed, to avoid continuously
// rescheduling to 80% of the remaining TTL as time passes
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
index b865319..d464ca7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -25,7 +25,7 @@
import androidx.annotation.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
import java.io.IOException;
import java.util.Arrays;
@@ -139,7 +139,7 @@
}
for (int i = 0; i < list1.length; ++i) {
- if (!MdnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
+ if (!DnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
return false;
}
}
@@ -284,13 +284,13 @@
MdnsRecord otherRecord = (MdnsRecord) other;
- return MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
+ return DnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
== otherRecord.type);
}
@Override
public int hashCode() {
- return Objects.hash(Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(name)), type);
+ return Objects.hash(Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(name)), type);
}
/**
@@ -311,7 +311,7 @@
public Key(int recordType, String[] recordName) {
this.recordType = recordType;
- this.recordName = MdnsUtils.toDnsLabelsLowerCase(recordName);
+ this.recordName = DnsUtils.toDnsLabelsUpperCase(recordName);
}
@Override
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 36f3982..c3cb776 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -38,6 +38,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.HexDump;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -226,11 +227,13 @@
/**
* Create a ServiceRegistration with only update the subType.
*/
- ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
+ ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes,
+ @NonNull MdnsFeatureFlags featureFlags) {
NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
newServiceInfo.setSubtypes(newSubtypes);
return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
- repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
+ repliedServiceCount, sentPacketCount, exiting, isProbing, ttl,
+ featureFlags);
}
/**
@@ -238,7 +241,7 @@
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
- @Nullable Duration ttl) {
+ @Nullable Duration ttl, @NonNull MdnsFeatureFlags featureFlags) {
this.serviceInfo = serviceInfo;
final long nonNameRecordsTtlMillis;
@@ -309,7 +312,8 @@
// Service name is verified unique after probing
true /* cacheFlush */,
nonNameRecordsTtlMillis,
- attrsToTextEntries(serviceInfo.getAttributes())),
+ attrsToTextEntries(
+ serviceInfo.getAttributes(), featureFlags)),
false /* sharedName */);
allRecords.addAll(ptrRecords);
@@ -392,9 +396,10 @@
* @param serviceInfo Service to advertise
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
+ int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl,
+ @NonNull MdnsFeatureFlags featureFlags) {
this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
- false /* exiting */, true /* isProbing */, ttl);
+ false /* exiting */, true /* isProbing */, ttl, featureFlags);
}
void setProbing(boolean probing) {
@@ -445,7 +450,7 @@
"Service ID must already exist for an update request: " + serviceId);
}
final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
- subtypes);
+ subtypes, mMdnsFeatureFlags);
mServices.put(serviceId, updatedRegistration);
}
@@ -476,7 +481,8 @@
final ServiceRegistration registration = new ServiceRegistration(
mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
- NO_PACKET /* sentPacketCount */, ttl);
+ NO_PACKET /* sentPacketCount */, ttl,
+ mMdnsFeatureFlags);
mServices.put(serviceId, registration);
// Remove existing exiting service
@@ -494,8 +500,8 @@
}
for (int i = 0; i < mServices.size(); i++) {
final NsdServiceInfo info = mServices.valueAt(i).serviceInfo;
- if (MdnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
- && MdnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
+ if (DnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
+ && DnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
return mServices.keyAt(i);
}
}
@@ -547,8 +553,17 @@
return new MdnsProber.ProbingInfo(serviceId, probingRecords);
}
- private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(Map<String, byte[]> attrs) {
- final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(attrs.size());
+ private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(
+ @NonNull Map<String, byte[]> attrs, @NonNull MdnsFeatureFlags featureFlags) {
+ final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(
+ attrs.size() == 0 ? 1 : attrs.size());
+ if (featureFlags.avoidAdvertisingEmptyTxtRecords() && attrs.size() == 0) {
+ // As per RFC6763 6.1, empty TXT records are not allowed, but records containing a
+ // single empty String must be treated as equivalent.
+ out.add(new MdnsServiceInfo.TextEntry("", MdnsServiceInfo.TextEntry.VALUE_NONE));
+ return out;
+ }
+
for (Map.Entry<String, byte[]> attr : attrs.entrySet()) {
out.add(new MdnsServiceInfo.TextEntry(attr.getKey(), attr.getValue()));
}
@@ -821,7 +836,7 @@
must match the question qtype unless the qtype is "ANY" (255) or the rrtype is
"CNAME" (5), and the record rrclass must match the question qclass unless the
qclass is "ANY" (255) */
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
+ if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
continue;
}
hasFullyOwnedNameMatch |= !info.isSharedName;
@@ -1232,7 +1247,7 @@
return RecordConflictType.NO_CONFLICT;
}
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
+ if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
return RecordConflictType.NO_CONFLICT;
}
@@ -1270,7 +1285,7 @@
}
// Different names. There won't be a conflict.
- if (!MdnsUtils.equalsIgnoreDnsCase(
+ if (!DnsUtils.equalsIgnoreDnsCase(
record.getName()[0], registration.serviceInfo.getHostname())) {
return RecordConflictType.NO_CONFLICT;
}
@@ -1351,7 +1366,7 @@
int id = mServices.keyAt(i);
ServiceRegistration service = mServices.valueAt(i);
if (service.exiting) continue;
- if (MdnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
+ if (DnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
consumer.accept(id, service);
}
}
@@ -1402,7 +1417,8 @@
if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
- existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
+ existing.repliedServiceCount, existing.sentPacketCount, existing.ttl,
+ mMdnsFeatureFlags);
mServices.put(serviceId, newService);
return makeProbingInfo(serviceId, newService);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index db3845a..4708cb6 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -16,9 +16,9 @@
package com.android.server.connectivity.mdns;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
import android.annotation.NonNull;
import android.annotation.RequiresApi;
@@ -245,7 +245,7 @@
return;
}
- if (mEnableDebugLog) mSharedLog.v("Sending " + replyInfo);
+ mSharedLog.log("Sending " + replyInfo);
final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
final MdnsPacket packet = new MdnsPacket(flags,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index 05ad1be..2957da5 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -19,11 +19,14 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.Network;
+import android.os.SystemClock;
+import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
import java.io.IOException;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -111,7 +114,7 @@
* pointer record is already present in the response with the same TTL.
*/
public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
+ if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
throw new IllegalArgumentException(
"Pointer records for different service names cannot be added");
}
@@ -305,13 +308,13 @@
boolean dropAddressRecords = false;
for (MdnsInetAddressRecord inetAddressRecord : getInet4AddressRecords()) {
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+ if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(
this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
dropAddressRecords = true;
}
}
for (MdnsInetAddressRecord inetAddressRecord : getInet6AddressRecords()) {
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+ if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(
this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
dropAddressRecords = true;
}
@@ -426,4 +429,18 @@
return count;
}
+
+ @Override
+ public String toString() {
+ return "Name: " + TextUtils.join(".", serviceName)
+ + ", pointerRecords: " + pointerRecords
+ + ", serviceRecord: " + serviceRecord
+ + ", textRecord: " + textRecord
+ + ", inet4AddressRecords: " + inet4AddressRecords
+ + ", inet6AddressRecords: " + inet6AddressRecords
+ + ", interfaceIndex: " + interfaceIndex
+ + ", network: " + network
+ + ", lastUpdateTime: " + Instant.now().minusMillis(
+ SystemClock.elapsedRealtime() - lastUpdateTime);
+ }
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
index b812bb4..52e76ad 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -22,6 +22,7 @@
import android.util.ArrayMap;
import android.util.Pair;
+import com.android.net.module.util.DnsUtils;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.EOFException;
@@ -49,7 +50,7 @@
List<MdnsResponse> responses, String[] pointer) {
if (responses != null) {
for (MdnsResponse response : responses) {
- if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
+ if (DnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
return response;
}
}
@@ -65,7 +66,7 @@
if (serviceRecord == null) {
continue;
}
- if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
+ if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
hostName)) {
return response;
}
@@ -318,7 +319,7 @@
if (serviceRecord == null) {
continue;
}
- if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
+ if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
if (result == null) {
result = new ArrayList<>(/* initialCapacity= */ responses.size());
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index e9a41d1..4ae8701 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -16,10 +16,10 @@
package com.android.server.connectivity.mdns;
+import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase;
+import static com.android.net.module.util.DnsUtils.toDnsUpperCase;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import static com.android.server.connectivity.mdns.MdnsResponse.EXPIRATION_NEVER;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase;
import static java.lang.Math.min;
@@ -32,6 +32,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.connectivity.mdns.util.MdnsUtils;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -48,17 +49,17 @@
* to their default value (0, false or null).
*/
public class MdnsServiceCache {
- static class CacheKey {
- @NonNull final String mLowercaseServiceType;
+ public static class CacheKey {
+ @NonNull final String mUpperCaseServiceType;
@NonNull final SocketKey mSocketKey;
CacheKey(@NonNull String serviceType, @NonNull SocketKey socketKey) {
- mLowercaseServiceType = toDnsLowerCase(serviceType);
+ mUpperCaseServiceType = toDnsUpperCase(serviceType);
mSocketKey = socketKey;
}
@Override public int hashCode() {
- return Objects.hash(mLowercaseServiceType, mSocketKey);
+ return Objects.hash(mUpperCaseServiceType, mSocketKey);
}
@Override public boolean equals(Object other) {
@@ -68,9 +69,14 @@
if (!(other instanceof CacheKey)) {
return false;
}
- return Objects.equals(mLowercaseServiceType, ((CacheKey) other).mLowercaseServiceType)
+ return Objects.equals(mUpperCaseServiceType, ((CacheKey) other).mUpperCaseServiceType)
&& Objects.equals(mSocketKey, ((CacheKey) other).mSocketKey);
}
+
+ @Override
+ public String toString() {
+ return "CacheKey{ ServiceType=" + mUpperCaseServiceType + ", " + mSocketKey + " }";
+ }
}
/**
* A map of cached services. Key is composed of service type and socket. Value is the list of
@@ -233,6 +239,21 @@
}
/**
+ * Remove services which matches the given type and socket.
+ *
+ * @param cacheKey the target CacheKey.
+ */
+ public void removeServices(@NonNull CacheKey cacheKey) {
+ ensureRunningOnHandlerThread(mHandler);
+ // Remove all services
+ if (mCachedServices.remove(cacheKey) == null) {
+ return;
+ }
+ // Update the next expiration check time if services are removed.
+ mNextExpirationTime = getNextExpirationTime(mClock.elapsedRealtime());
+ }
+
+ /**
* Register a callback to listen to service expiration.
*
* <p> Registering the same callback instance twice is a no-op, since MdnsServiceTypeClient
@@ -338,6 +359,22 @@
mNextExpirationTime = getNextExpirationTime(now);
}
+ /**
+ * Dump ServiceCache state.
+ */
+ public void dump(PrintWriter pw, String indent) {
+ ensureRunningOnHandlerThread(mHandler);
+ // IndentingPrintWriter cannot be used on the mDNS stack build. So, manually add an indent.
+ for (int i = 0; i < mCachedServices.size(); i++) {
+ final CacheKey key = mCachedServices.keyAt(i);
+ pw.println(indent + key);
+ for (MdnsResponse response : mCachedServices.valueAt(i)) {
+ pw.println(indent + " Response{ " + response + " }");
+ }
+ pw.println();
+ }
+ }
+
/*** Callbacks for listening service expiration */
public interface ServiceExpiredCallback {
/*** Notify the service is expired */
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index 1ec9e39..a16fcf7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -16,8 +16,6 @@
package com.android.server.connectivity.mdns;
-import static com.android.server.connectivity.mdns.MdnsSocket.INTERFACE_INDEX_UNSPECIFIED;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.Network;
@@ -33,7 +31,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
@@ -355,6 +352,13 @@
/** Represents a DNS TXT key-value pair defined by RFC 6763. */
public static final class TextEntry implements Parcelable {
+ /**
+ * The value to use for attributes with no value.
+ *
+ * <p>As per RFC6763 P.16, attributes may have no value, which is different from having an
+ * empty value (which would be an empty byte array).
+ */
+ public static final byte[] VALUE_NONE = null;
public static final Parcelable.Creator<TextEntry> CREATOR =
new Parcelable.Creator<TextEntry>() {
@Override
@@ -389,7 +393,7 @@
// 2. If there is no '=' in a DNS-SD TXT record string, then it is a
// boolean attribute, simply identified as being present, with no value.
if (delimitPos < 0) {
- return new TextEntry(new String(textBytes, US_ASCII), (byte[]) null);
+ return new TextEntry(new String(textBytes, US_ASCII), VALUE_NONE);
} else if (delimitPos == 0) {
return null;
}
@@ -400,13 +404,13 @@
/** Creates a new {@link TextEntry} with given key and value of a UTF-8 string. */
public TextEntry(String key, String value) {
- this(key, value == null ? null : value.getBytes(UTF_8));
+ this(key, value == null ? VALUE_NONE : value.getBytes(UTF_8));
}
/** Creates a new {@link TextEntry} with given key and value of a byte array. */
public TextEntry(String key, byte[] value) {
this.key = key;
- this.value = value == null ? null : value.clone();
+ this.value = value == VALUE_NONE ? VALUE_NONE : value.clone();
}
private TextEntry(Parcel in) {
@@ -419,22 +423,26 @@
}
public byte[] getValue() {
- return value == null ? null : value.clone();
+ return value == VALUE_NONE ? VALUE_NONE : value.clone();
}
/** Converts this {@link TextEntry} instance to '=' separated byte array. */
public byte[] toBytes() {
final byte[] keyBytes = key.getBytes(US_ASCII);
- if (value == null) {
+ if (value == VALUE_NONE) {
return keyBytes;
}
return ByteUtils.concat(keyBytes, new byte[]{'='}, value);
}
+ public boolean isEmpty() {
+ return TextUtils.isEmpty(key) && (value == VALUE_NONE || value.length == 0);
+ }
+
/** Converts this {@link TextEntry} instance to '=' separated string. */
@Override
public String toString() {
- if (value == null) {
+ if (value == VALUE_NONE) {
return key;
}
return key + "=" + new String(value, UTF_8);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
index 0d6a9ec..907e2ff 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -18,9 +18,7 @@
import android.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
import java.io.IOException;
import java.util.Arrays;
@@ -28,7 +26,6 @@
import java.util.Objects;
/** An mDNS "SRV" record, which contains service information. */
-@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsServiceRecord extends MdnsRecord {
public static final int PROTO_NONE = 0;
public static final int PROTO_TCP = 1;
@@ -151,7 +148,7 @@
public int hashCode() {
return (super.hashCode() * 31)
+ Objects.hash(servicePriority, serviceWeight,
- Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(serviceHost)),
+ Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(serviceHost)),
servicePort);
}
@@ -168,7 +165,7 @@
return super.equals(other)
&& (servicePriority == otherRecord.servicePriority)
&& (serviceWeight == otherRecord.serviceWeight)
- && MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
+ && DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
&& (servicePort == otherRecord.servicePort);
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 643430a..a43486e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -16,10 +16,12 @@
package com.android.server.connectivity.mdns;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
+import static com.android.server.connectivity.mdns.util.MdnsUtils.buildMdnsServiceInfoFromResponse;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -33,16 +35,14 @@
import androidx.annotation.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.DatagramPacket;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
import java.net.InetSocketAddress;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -59,6 +59,7 @@
public class MdnsServiceTypeClient {
private static final String TAG = MdnsServiceTypeClient.class.getSimpleName();
+ private static final boolean DBG = MdnsDiscoveryManager.DBG;
@VisibleForTesting
static final int EVENT_START_QUERYTASK = 1;
static final int EVENT_QUERY_RESULT = 2;
@@ -120,11 +121,11 @@
* @return true if the service name was not discovered before.
*/
boolean setServiceDiscovered(@NonNull String serviceName) {
- return discoveredServiceNames.add(MdnsUtils.toDnsLowerCase(serviceName));
+ return discoveredServiceNames.add(DnsUtils.toDnsUpperCase(serviceName));
}
void unsetServiceDiscovered(@NonNull String serviceName) {
- discoveredServiceNames.remove(MdnsUtils.toDnsLowerCase(serviceName));
+ discoveredServiceNames.remove(DnsUtils.toDnsUpperCase(serviceName));
}
}
@@ -147,7 +148,8 @@
final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
final QueryTask queryTask = new QueryTask(taskArgs, servicesToResolve,
getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
- getExistingServices());
+ getExistingServices(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
+ socketKey);
executor.submit(queryTask);
break;
}
@@ -178,12 +180,19 @@
minRemainingTtl,
now,
lastSentTime,
- sentResult.taskArgs.sessionId
+ sentResult.taskArgs.sessionId,
+ searchOptions.getQueryMode(),
+ searchOptions.numOfQueriesBeforeBackoff(),
+ false /* forceEnableBackoff */
);
+ final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
+ sharedLog.log(String.format("Query sent with transactionId: %d. "
+ + "Next run: sessionId: %d, in %d ms",
+ sentResult.transactionId, args.sessionId, timeToNextTaskMs));
dependencies.sendMessageDelayed(
handler,
handler.obtainMessage(EVENT_START_QUERYTASK, args),
- calculateTimeToNextTask(args, now, sharedLog));
+ timeToNextTaskMs);
break;
}
default:
@@ -303,57 +312,6 @@
serviceCache.unregisterServiceExpiredCallback(cacheKey);
}
- private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(@NonNull MdnsResponse response,
- @NonNull String[] serviceTypeLabels, long elapsedRealtimeMillis) {
- String[] hostName = null;
- int port = 0;
- if (response.hasServiceRecord()) {
- hostName = response.getServiceRecord().getServiceHost();
- port = response.getServiceRecord().getServicePort();
- }
-
- final List<String> ipv4Addresses = new ArrayList<>();
- final List<String> ipv6Addresses = new ArrayList<>();
- if (response.hasInet4AddressRecord()) {
- for (MdnsInetAddressRecord inetAddressRecord : response.getInet4AddressRecords()) {
- final Inet4Address inet4Address = inetAddressRecord.getInet4Address();
- ipv4Addresses.add((inet4Address == null) ? null : inet4Address.getHostAddress());
- }
- }
- if (response.hasInet6AddressRecord()) {
- for (MdnsInetAddressRecord inetAddressRecord : response.getInet6AddressRecords()) {
- final Inet6Address inet6Address = inetAddressRecord.getInet6Address();
- ipv6Addresses.add((inet6Address == null) ? null : inet6Address.getHostAddress());
- }
- }
- String serviceInstanceName = response.getServiceInstanceName();
- if (serviceInstanceName == null) {
- throw new IllegalStateException(
- "mDNS response must have non-null service instance name");
- }
- List<String> textStrings = null;
- List<MdnsServiceInfo.TextEntry> textEntries = null;
- if (response.hasTextRecord()) {
- textStrings = response.getTextRecord().getStrings();
- textEntries = response.getTextRecord().getEntries();
- }
- Instant now = Instant.now();
- // TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
- return new MdnsServiceInfo(
- serviceInstanceName,
- serviceTypeLabels,
- response.getSubtypes(),
- hostName,
- port,
- ipv4Addresses,
- ipv6Addresses,
- textStrings,
- textEntries,
- response.getInterfaceIndex(),
- response.getNetwork(),
- now.plusMillis(response.getMinRemainingTtl(elapsedRealtimeMillis)));
- }
-
private List<MdnsResponse> getExistingServices() {
return featureFlags.isQueryWithKnownAnswerEnabled()
? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
@@ -392,14 +350,13 @@
}
// Remove the next scheduled periodical task.
removeScheduledTask();
- mdnsQueryScheduler.cancelScheduledRun();
- // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
- // interested anymore.
- final QueryTaskConfig taskConfig = new QueryTaskConfig(
- searchOptions.getQueryMode(),
- searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
- searchOptions.numOfQueriesBeforeBackoff(),
- socketKey);
+ final boolean forceEnableBackoff =
+ (searchOptions.getQueryMode() == AGGRESSIVE_QUERY_MODE && hadReply);
+ // Keep the latest scheduled run for rescheduling if there is a service in the cache.
+ if (!(forceEnableBackoff)) {
+ mdnsQueryScheduler.cancelScheduledRun();
+ }
+ final QueryTaskConfig taskConfig = new QueryTaskConfig(searchOptions.getQueryMode());
final long now = clock.elapsedRealtime();
if (lastSentTime == 0) {
lastSentTime = now;
@@ -412,19 +369,26 @@
minRemainingTtl,
now,
lastSentTime,
- currentSessionId
+ currentSessionId,
+ searchOptions.getQueryMode(),
+ searchOptions.numOfQueriesBeforeBackoff(),
+ forceEnableBackoff
);
+ final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
+ sharedLog.log(String.format("Schedule a query. Next run: sessionId: %d, in %d ms",
+ args.sessionId, timeToNextTaskMs));
dependencies.sendMessageDelayed(
handler,
handler.obtainMessage(EVENT_START_QUERYTASK, args),
- calculateTimeToNextTask(args, now, sharedLog));
+ timeToNextTaskMs);
} else {
final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
final QueryTask queryTask = new QueryTask(
mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
minRemainingTtl, currentSessionId), servicesToResolve,
getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
- getExistingServices());
+ getExistingServices(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
+ socketKey);
executor.submit(queryTask);
}
@@ -447,6 +411,14 @@
return executor;
}
+ /**
+ * Get the cache key for this service type client.
+ */
+ @NonNull
+ public MdnsServiceCache.CacheKey getCacheKey() {
+ return cacheKey;
+ }
+
private void removeScheduledTask() {
dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
sharedLog.log("Remove EVENT_START_QUERYTASK"
@@ -458,7 +430,7 @@
@NonNull MdnsSearchOptions options) {
final boolean matchesInstanceName = options.getResolveInstanceName() == null
// DNS is case-insensitive, so ignore case in the comparison
- || MdnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
+ || DnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
response.getServiceInstanceName());
// If discovery is requiring some subtypes, the response must have one that matches a
@@ -468,7 +440,7 @@
final boolean matchesSubtype = options.getSubtypes().size() == 0
|| CollectionUtils.any(options.getSubtypes(), requiredSub ->
CollectionUtils.any(responseSubtypes, actualSub ->
- MdnsUtils.equalsIgnoreDnsCase(
+ DnsUtils.equalsIgnoreDnsCase(
MdnsConstants.SUBTYPE_PREFIX + requiredSub, actualSub)));
return matchesInstanceName && matchesSubtype;
@@ -528,6 +500,10 @@
// If the response is not modified and already in the cache. The cache will
// need to be updated to refresh the last receipt time.
serviceCache.addOrUpdateService(cacheKey, response);
+ if (DBG) {
+ sharedLog.v("Update the last receipt time for service:"
+ + serviceInstanceName);
+ }
}
}
if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
@@ -535,13 +511,17 @@
final long minRemainingTtl = getMinRemainingTtl(now);
MdnsQueryScheduler.ScheduledQueryTaskArgs args =
mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl,
- lastSentTime, currentSessionId + 1);
+ lastSentTime, currentSessionId + 1,
+ searchOptions.numOfQueriesBeforeBackoff());
if (args != null) {
removeScheduledTask();
+ final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
+ sharedLog.log(String.format("Reschedule a query. Next run: sessionId: %d, in %d ms",
+ args.sessionId, timeToNextTaskMs));
dependencies.sendMessageDelayed(
handler,
handler.obtainMessage(EVENT_START_QUERYTASK, args),
- calculateTimeToNextTask(args, now, sharedLog));
+ timeToNextTaskMs);
}
}
}
@@ -658,7 +638,7 @@
continue;
}
if (CollectionUtils.any(resolveResponses,
- r -> MdnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
+ r -> DnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
continue;
}
MdnsResponse knownResponse =
@@ -723,15 +703,21 @@
private final List<String> subtypes = new ArrayList<>();
private final boolean sendDiscoveryQueries;
private final List<MdnsResponse> existingServices = new ArrayList<>();
+ private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
+ private final SocketKey socketKey;
QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
@NonNull Collection<MdnsResponse> servicesToResolve,
@NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
- @NonNull Collection<MdnsResponse> existingServices) {
+ @NonNull Collection<MdnsResponse> existingServices,
+ boolean onlyUseIpv6OnIpv6OnlyNetworks,
+ @NonNull SocketKey socketKey) {
this.taskArgs = taskArgs;
this.servicesToResolve.addAll(servicesToResolve);
this.subtypes.addAll(subtypes);
this.sendDiscoveryQueries = sendDiscoveryQueries;
this.existingServices.addAll(existingServices);
+ this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
+ this.socketKey = socketKey;
}
@Override
@@ -745,8 +731,8 @@
subtypes,
taskArgs.config.expectUnicastResponse,
taskArgs.config.transactionId,
- taskArgs.config.socketKey,
- taskArgs.config.onlyUseIpv6OnIpv6OnlyNetworks,
+ socketKey,
+ onlyUseIpv6OnIpv6OnlyNetworks,
sendDiscoveryQueries,
servicesToResolve,
clock,
@@ -786,11 +772,8 @@
}
private static long calculateTimeToNextTask(MdnsQueryScheduler.ScheduledQueryTaskArgs args,
- long now, SharedLog sharedLog) {
- long timeToNextTasksWithBackoffInMs = Math.max(args.timeToRun - now, 0);
- sharedLog.log(String.format("Next run: sessionId: %d, in %d ms",
- args.sessionId, timeToNextTasksWithBackoffInMs));
- return timeToNextTasksWithBackoffInMs;
+ long now) {
+ return Math.max(args.timeToRun - now, 0);
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
index 5c9ec09..b640c32 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
@@ -19,7 +19,8 @@
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
+
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import static com.android.server.connectivity.mdns.util.MdnsUtils.isNetworkMatched;
import android.annotation.NonNull;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
index 92cf324..2b3ebf9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -18,8 +18,6 @@
import android.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import java.io.IOException;
@@ -29,7 +27,6 @@
import java.util.Objects;
/** An mDNS "TXT" record, which contains a list of {@link TextEntry}. */
-@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsTextRecord extends MdnsRecord {
private List<TextEntry> entries;
@@ -89,6 +86,13 @@
}
}
+ private boolean isEmpty() {
+ return entries == null || entries.size() == 0
+ // RFC6763 6.1 indicates that a TXT record with a single zero byte is equivalent to
+ // an empty record.
+ || (entries.size() == 1 && entries.get(0).isEmpty());
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -105,7 +109,7 @@
@Override
public int hashCode() {
- return (super.hashCode() * 31) + Objects.hash(entries);
+ return (super.hashCode() * 31) + (isEmpty() ? 0 : Objects.hash(entries));
}
@Override
@@ -116,7 +120,19 @@
if (!(other instanceof MdnsTextRecord)) {
return false;
}
-
- return super.equals(other) && Objects.equals(entries, ((MdnsTextRecord) other).entries);
+ if (!super.equals(other)) {
+ return false;
+ }
+ // As per RFC6763 6.1: DNS-SD clients MUST treat the following as equivalent:
+ // - A TXT record containing a single zero byte.
+ // - An empty (zero-length) TXT record. (This is not strictly legal, but should one be
+ // received, it should be interpreted as the same as a single empty string.)
+ // - No TXT record
+ // Ensure that empty TXT records are considered equal, so that they are not considered
+ // conflicting for example.
+ if (isEmpty() && ((MdnsTextRecord) other).isEmpty()) {
+ return true;
+ }
+ return Objects.equals(entries, ((MdnsTextRecord) other).entries);
}
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
index 70451f3..4d7e4bc 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
@@ -16,7 +16,7 @@
package com.android.server.connectivity.mdns;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import android.annotation.NonNull;
import android.os.Handler;
diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index 0894166..dd4073f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -19,9 +19,6 @@
import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
import com.android.internal.annotations.VisibleForTesting;
/**
@@ -51,146 +48,138 @@
static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
private final boolean alwaysAskForUnicastResponse =
MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
- private final int queryMode;
- final boolean onlyUseIpv6OnIpv6OnlyNetworks;
- private final int numOfQueriesBeforeBackoff;
@VisibleForTesting
final int transactionId;
@VisibleForTesting
final boolean expectUnicastResponse;
- private final int queriesPerBurst;
- private final int timeBetweenBurstsInMs;
- private final int burstCounter;
- final long delayUntilNextTaskWithoutBackoffMs;
- private final boolean isFirstBurst;
- private final long queryCount;
- @NonNull
- final SocketKey socketKey;
+ private final int queryIndex;
+ private final int queryMode;
- QueryTaskConfig(@NonNull QueryTaskConfig other, long queryCount, int transactionId,
- boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
- int queriesPerBurst, int timeBetweenBurstsInMs,
- long delayUntilNextTaskWithoutBackoffMs) {
- this.queryMode = other.queryMode;
- this.onlyUseIpv6OnIpv6OnlyNetworks = other.onlyUseIpv6OnIpv6OnlyNetworks;
- this.numOfQueriesBeforeBackoff = other.numOfQueriesBeforeBackoff;
- this.transactionId = transactionId;
- this.expectUnicastResponse = expectUnicastResponse;
- this.queriesPerBurst = queriesPerBurst;
- this.timeBetweenBurstsInMs = timeBetweenBurstsInMs;
- this.burstCounter = burstCounter;
- this.delayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
- this.isFirstBurst = isFirstBurst;
- this.queryCount = queryCount;
- this.socketKey = other.socketKey;
- }
-
- QueryTaskConfig(int queryMode,
- boolean onlyUseIpv6OnIpv6OnlyNetworks,
- int numOfQueriesBeforeBackoff,
- @Nullable SocketKey socketKey) {
+ QueryTaskConfig(int queryMode, int queryIndex, int transactionId,
+ boolean expectUnicastResponse) {
this.queryMode = queryMode;
- this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
- this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
- this.queriesPerBurst = QUERIES_PER_BURST;
- this.burstCounter = 0;
- this.transactionId = 1;
- this.expectUnicastResponse = true;
- this.isFirstBurst = true;
- // Config the scan frequency based on the scan mode.
- if (this.queryMode == AGGRESSIVE_QUERY_MODE) {
- this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
- this.delayUntilNextTaskWithoutBackoffMs =
- TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
- } else if (this.queryMode == PASSIVE_QUERY_MODE) {
- // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
- // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
- // queries.
- this.timeBetweenBurstsInMs = MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
- this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
- } else {
- // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
- // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
- // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
- // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
- this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
- this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
- }
- this.socketKey = socketKey;
- this.queryCount = 0;
+ this.transactionId = transactionId;
+ this.queryIndex = queryIndex;
+ this.expectUnicastResponse = expectUnicastResponse;
}
- long getDelayUntilNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
- boolean isLastQueryInBurst) {
- if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
- return 0;
+ QueryTaskConfig(int queryMode) {
+ this(queryMode, 0, 1, true);
+ }
+
+ private static int getBurstIndex(int queryIndex, int queryMode) {
+ if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
+ // In passive mode, after the first burst of QUERIES_PER_BURST queries, subsequent
+ // bursts have QUERIES_PER_BURST_PASSIVE_MODE queries.
+ final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
+ return 1 + (queryIndexAfterFirstBurst / QUERIES_PER_BURST_PASSIVE_MODE);
+ } else {
+ return queryIndex / QUERIES_PER_BURST;
}
- if (isLastQueryInBurst) {
- return timeBetweenBurstsInMs;
+ }
+
+ private static int getQueryIndexInBurst(int queryIndex, int queryMode) {
+ if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
+ final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
+ return queryIndexAfterFirstBurst % QUERIES_PER_BURST_PASSIVE_MODE;
+ } else {
+ return queryIndex % QUERIES_PER_BURST;
+ }
+ }
+
+ private static boolean isFirstBurst(int queryIndex, int queryMode) {
+ return getBurstIndex(queryIndex, queryMode) == 0;
+ }
+
+ private static boolean isFirstQueryInBurst(int queryIndex, int queryMode) {
+ return getQueryIndexInBurst(queryIndex, queryMode) == 0;
+ }
+
+ // TODO: move delay calculations to MdnsQueryScheduler
+ long getDelayBeforeTaskWithoutBackoff() {
+ return getDelayBeforeTaskWithoutBackoff(queryIndex, queryMode);
+ }
+
+ private static long getDelayBeforeTaskWithoutBackoff(int queryIndex, int queryMode) {
+ final int burstIndex = getBurstIndex(queryIndex, queryMode);
+ final int queryIndexInBurst = getQueryIndexInBurst(queryIndex, queryMode);
+ if (queryIndexInBurst == 0) {
+ return getTimeToBurstMs(burstIndex, queryMode);
+ } else if (queryIndexInBurst == 1 && queryMode == AGGRESSIVE_QUERY_MODE) {
+ // In aggressive mode, the first 2 queries are sent without delay.
+ return 0;
}
return queryMode == AGGRESSIVE_QUERY_MODE
? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS
: TIME_BETWEEN_QUERIES_IN_BURST_MS;
}
- boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst) {
- if (!isLastQueryInBurst) {
- return false;
- }
+ private boolean getExpectUnicastResponse(int queryIndex, int queryMode) {
if (queryMode == AGGRESSIVE_QUERY_MODE) {
- return true;
+ if (isFirstQueryInBurst(queryIndex, queryMode)) {
+ return true;
+ }
}
return alwaysAskForUnicastResponse;
}
- int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst) {
- if (!isLastQueryInBurst) {
- return timeBetweenBurstsInMs;
+ /**
+ * Shifts a value left by the specified number of bits, coercing to at most maxValue.
+ *
+ * <p>This allows calculating min(value*2^shift, maxValue) without overflow.
+ */
+ private static int boundedLeftShift(int value, int shift, int maxValue) {
+ // There must be at least one leading zero for positive values, so the maximum left shift
+ // without overflow is the number of leading zeros minus one.
+ final int maxShift = Integer.numberOfLeadingZeros(value) - 1;
+ if (shift > maxShift) {
+ // The shift would overflow positive integers, so is greater than maxValue.
+ return maxValue;
}
- final int maxTimeBetweenBursts = queryMode == AGGRESSIVE_QUERY_MODE
- ? MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS : MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
- return Math.min(timeBetweenBurstsInMs * 2, maxTimeBetweenBursts);
+ return Math.min(value << shift, maxValue);
+ }
+
+ private static int getTimeToBurstMs(int burstIndex, int queryMode) {
+ if (burstIndex == 0) {
+ // No delay before the first burst
+ return 0;
+ }
+ switch (queryMode) {
+ case PASSIVE_QUERY_MODE:
+ return MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
+ case AGGRESSIVE_QUERY_MODE:
+ return boundedLeftShift(INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS,
+ burstIndex - 1,
+ MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+ default: // ACTIVE_QUERY_MODE
+ return boundedLeftShift(INITIAL_TIME_BETWEEN_BURSTS_MS,
+ burstIndex - 1,
+ MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS);
+ }
}
/**
* Get new QueryTaskConfig for next run.
*/
- public QueryTaskConfig getConfigForNextRun() {
- long newQueryCount = queryCount + 1;
+ public QueryTaskConfig getConfigForNextRun(int queryMode) {
+ final int newQueryIndex = queryIndex + 1;
int newTransactionId = transactionId + 1;
if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
newTransactionId = 1;
}
- int newQueriesPerBurst = queriesPerBurst;
- int newBurstCounter = burstCounter + 1;
- final boolean isFirstQueryInBurst = newBurstCounter == 1;
- final boolean isLastQueryInBurst = newBurstCounter == queriesPerBurst;
- boolean newIsFirstBurst = isFirstBurst && !isLastQueryInBurst;
- if (isLastQueryInBurst) {
- newBurstCounter = 0;
- // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and
- // then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
- // queries.
- if (isFirstBurst && queryMode == PASSIVE_QUERY_MODE) {
- newQueriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
- }
- }
-
- return new QueryTaskConfig(this, newQueryCount, newTransactionId,
- getNextExpectUnicastResponse(isLastQueryInBurst), newIsFirstBurst, newBurstCounter,
- newQueriesPerBurst, getNextTimeBetweenBurstsMs(isLastQueryInBurst),
- getDelayUntilNextTaskWithoutBackoff(isFirstQueryInBurst, isLastQueryInBurst));
+ return new QueryTaskConfig(queryMode, newQueryIndex, newTransactionId,
+ getExpectUnicastResponse(newQueryIndex, queryMode));
}
/**
* Determine if the query backoff should be used.
*/
- public boolean shouldUseQueryBackoff() {
+ public boolean shouldUseQueryBackoff(int numOfQueriesBeforeBackoff) {
// Don't enable backoff mode during the burst or in the first burst
- if (burstCounter != 0 || isFirstBurst) {
+ if (!isFirstQueryInBurst(queryIndex, queryMode) || isFirstBurst(queryIndex, queryMode)) {
return false;
}
- return queryCount > numOfQueriesBeforeBackoff;
+ return queryIndex > numOfQueriesBeforeBackoff;
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 226867f..41b15dd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -16,24 +16,30 @@
package com.android.server.connectivity.mdns.util;
+import static com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase;
+import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase;
import static com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.Network;
import android.os.Build;
-import android.os.Handler;
import android.os.SystemClock;
import android.util.ArraySet;
import android.util.Pair;
import com.android.server.connectivity.mdns.MdnsConstants;
+import com.android.server.connectivity.mdns.MdnsInetAddressRecord;
import com.android.server.connectivity.mdns.MdnsPacket;
import com.android.server.connectivity.mdns.MdnsPacketWriter;
import com.android.server.connectivity.mdns.MdnsRecord;
+import com.android.server.connectivity.mdns.MdnsResponse;
+import com.android.server.connectivity.mdns.MdnsServiceInfo;
import java.io.IOException;
import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
@@ -41,6 +47,7 @@
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -56,18 +63,17 @@
private MdnsUtils() { }
/**
- * Convert the string to DNS case-insensitive lowercase
+ * Compare labels a equals b or a is suffix of b.
*
- * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to
- * their unaccented counterparts. So the "DNS lowercase" should be if character is A-Z then they
- * transform into a-z. Otherwise, they are kept as-is.
+ * @param a the type or subtype.
+ * @param b the base type
*/
- public static String toDnsLowerCase(@NonNull String string) {
- final char[] outChars = new char[string.length()];
- for (int i = 0; i < string.length(); i++) {
- outChars[i] = toDnsLowerCase(string.charAt(i));
- }
- return new String(outChars);
+ public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
+ @NonNull String[] b) {
+ return equalsDnsLabelIgnoreDnsCase(a, b)
+ || ((b.length == (a.length + 2))
+ && equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
+ && MdnsRecord.labelsAreSuffix(a, b));
}
/**
@@ -81,85 +87,6 @@
}
}
- /**
- * Convert the array of labels to DNS case-insensitive lowercase.
- */
- public static String[] toDnsLabelsLowerCase(@NonNull String[] labels) {
- final String[] outStrings = new String[labels.length];
- for (int i = 0; i < labels.length; ++i) {
- outStrings[i] = toDnsLowerCase(labels[i]);
- }
- return outStrings;
- }
-
- /**
- * Compare two strings by DNS case-insensitive lowercase.
- */
- public static boolean equalsIgnoreDnsCase(@Nullable String a, @Nullable String b) {
- if (a == null || b == null) {
- return a == null && b == null;
- }
- if (a.length() != b.length()) return false;
- for (int i = 0; i < a.length(); i++) {
- if (toDnsLowerCase(a.charAt(i)) != toDnsLowerCase(b.charAt(i))) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Compare two set of DNS labels by DNS case-insensitive lowercase.
- */
- public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
- if (a == b) {
- return true;
- }
- int length = a.length;
- if (b.length != length) {
- return false;
- }
- for (int i = 0; i < length; i++) {
- if (!equalsIgnoreDnsCase(a[i], b[i])) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Compare labels a equals b or a is suffix of b.
- *
- * @param a the type or subtype.
- * @param b the base type
- */
- public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
- @NonNull String[] b) {
- return MdnsUtils.equalsDnsLabelIgnoreDnsCase(a, b)
- || ((b.length == (a.length + 2))
- && MdnsUtils.equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
- && MdnsRecord.labelsAreSuffix(a, b));
- }
-
- private static char toDnsLowerCase(char a) {
- return a >= 'A' && a <= 'Z' ? (char) (a + ('a' - 'A')) : a;
- }
-
- /*** Ensure that current running thread is same as given handler thread */
- public static void ensureRunningOnHandlerThread(@NonNull Handler handler) {
- if (!isRunningOnHandlerThread(handler)) {
- throw new IllegalStateException(
- "Not running on Handler thread: " + Thread.currentThread().getName());
- }
- }
-
- /*** Check that current running thread is same as given handler thread */
- public static boolean isRunningOnHandlerThread(@NonNull Handler handler) {
- if (handler.getLooper().getThread() == Thread.currentThread()) {
- return true;
- }
- return false;
- }
/*** Check whether the target network matches the current network */
public static boolean isNetworkMatched(@Nullable Network targetNetwork,
@@ -381,4 +308,62 @@
}
return true;
}
+
+ /**
+ * Build MdnsServiceInfo object from given MdnsResponse, service type labels and current time.
+ *
+ * @param response target service response
+ * @param serviceTypeLabels service type labels
+ * @param elapsedRealtimeMillis current time.
+ */
+ public static MdnsServiceInfo buildMdnsServiceInfoFromResponse(@NonNull MdnsResponse response,
+ @NonNull String[] serviceTypeLabels, long elapsedRealtimeMillis) {
+ String[] hostName = null;
+ int port = 0;
+ if (response.hasServiceRecord()) {
+ hostName = response.getServiceRecord().getServiceHost();
+ port = response.getServiceRecord().getServicePort();
+ }
+
+ final List<String> ipv4Addresses = new ArrayList<>();
+ final List<String> ipv6Addresses = new ArrayList<>();
+ if (response.hasInet4AddressRecord()) {
+ for (MdnsInetAddressRecord inetAddressRecord : response.getInet4AddressRecords()) {
+ final Inet4Address inet4Address = inetAddressRecord.getInet4Address();
+ ipv4Addresses.add((inet4Address == null) ? null : inet4Address.getHostAddress());
+ }
+ }
+ if (response.hasInet6AddressRecord()) {
+ for (MdnsInetAddressRecord inetAddressRecord : response.getInet6AddressRecords()) {
+ final Inet6Address inet6Address = inetAddressRecord.getInet6Address();
+ ipv6Addresses.add((inet6Address == null) ? null : inet6Address.getHostAddress());
+ }
+ }
+ String serviceInstanceName = response.getServiceInstanceName();
+ if (serviceInstanceName == null) {
+ throw new IllegalStateException(
+ "mDNS response must have non-null service instance name");
+ }
+ List<String> textStrings = null;
+ List<MdnsServiceInfo.TextEntry> textEntries = null;
+ if (response.hasTextRecord()) {
+ textStrings = response.getTextRecord().getStrings();
+ textEntries = response.getTextRecord().getEntries();
+ }
+ Instant now = Instant.now();
+ // TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
+ return new MdnsServiceInfo(
+ serviceInstanceName,
+ serviceTypeLabels,
+ response.getSubtypes(),
+ hostName,
+ port,
+ ipv4Addresses,
+ ipv6Addresses,
+ textStrings,
+ textEntries,
+ response.getInterfaceIndex(),
+ response.getNetwork(),
+ now.plusMillis(response.getMinRemainingTtl(elapsedRealtimeMillis)));
+ }
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index cadc04d..1ac99e4 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -202,20 +202,6 @@
return;
}
- private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc,
- NetworkCapabilities addedNc) {
- final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(nc);
- for (int transport : addedNc.getTransportTypes()) builder.addTransportType(transport);
- for (int capability : addedNc.getCapabilities()) builder.addCapability(capability);
- return builder.build();
- }
-
- private static NetworkCapabilities createDefaultNetworkCapabilities() {
- return NetworkCapabilities.Builder
- .withoutDefaultCapabilities()
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET).build();
- }
-
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
protected boolean removeInterface(String interfaceName) {
NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName);
@@ -556,14 +542,6 @@
maybeRestart();
}
- private void ensureRunningOnEthernetHandlerThread() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on the Ethernet thread: "
- + Thread.currentThread().getName());
- }
- }
-
private void handleOnLinkPropertiesChange(LinkProperties linkProperties) {
mLinkProperties = linkProperties;
if (mNetworkAgent != null) {
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 71f289e..5228aab 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -49,6 +49,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -138,8 +139,8 @@
private int mTetheringInterfaceMode = INTERFACE_MODE_CLIENT;
// Tracks whether clients were notified that the tethered interface is available
private boolean mTetheredInterfaceWasAvailable = false;
-
- private int mEthernetState = ETHERNET_STATE_ENABLED;
+ // Tracks the current state of ethernet as configured by EthernetManager#setEthernetEnabled.
+ private boolean mIsEthernetEnabled = true;
private class TetheredInterfaceRequestList extends
RemoteCallbackList<ITetheredInterfaceCallback> {
@@ -214,9 +215,6 @@
// Note: processNetlinkMessage is called on the handler thread.
@Override
protected void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) {
- // ignore all updates when ethernet is disabled.
- if (mEthernetState == ETHERNET_STATE_DISABLED) return;
-
if (nlMsg instanceof RtNetlinkLinkMessage) {
processRtNetlinkLinkMessage((RtNetlinkLinkMessage) nlMsg);
} else {
@@ -302,11 +300,7 @@
}
private void ensureRunningOnEthernetServiceThread() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on EthernetService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
}
/**
@@ -450,7 +444,7 @@
unicastInterfaceStateChange(listener, mTetheringInterface);
}
- unicastEthernetStateChange(listener, mEthernetState);
+ unicastEthernetStateChange(listener, mIsEthernetEnabled);
});
}
@@ -599,10 +593,13 @@
// Read the flags before attempting to bring up the interface. If the interface is
// already running an UP event is created after adding the interface.
config = NetdUtils.getInterfaceConfigParcel(mNetd, iface);
- if (NetdUtils.hasFlag(config, INetd.IF_STATE_DOWN)) {
+ // Only bring the interface up when ethernet is enabled.
+ if (mIsEthernetEnabled) {
// As a side-effect, NetdUtils#setInterfaceUp() also clears the interface's IPv4
// address and readds it which *could* lead to unexpected behavior in the future.
NetdUtils.setInterfaceUp(mNetd, iface);
+ } else {
+ NetdUtils.setInterfaceDown(mNetd, iface);
}
} catch (IllegalStateException e) {
// Either the system is crashing or the interface has disappeared. Just ignore the
@@ -649,6 +646,10 @@
}
private void setInterfaceAdministrativeState(String iface, boolean up, EthernetCallback cb) {
+ if (!mIsEthernetEnabled) {
+ cb.onError("Cannot enable/disable interface when ethernet is disabled");
+ return;
+ }
if (getInterfaceState(iface) == EthernetManager.STATE_ABSENT) {
cb.onError("Failed to enable/disable absent interface: " + iface);
return;
@@ -963,43 +964,51 @@
@VisibleForTesting(visibility = PACKAGE)
protected void setEthernetEnabled(boolean enabled) {
mHandler.post(() -> {
- int newState = enabled ? ETHERNET_STATE_ENABLED : ETHERNET_STATE_DISABLED;
- if (mEthernetState == newState) return;
+ if (mIsEthernetEnabled == enabled) return;
- mEthernetState = newState;
+ mIsEthernetEnabled = enabled;
- if (enabled) {
- trackAvailableInterfaces();
- } else {
- // TODO: maybe also disable server mode interface as well.
- untrackFactoryInterfaces();
+ // Interface in server mode should also be included.
+ ArrayList<String> interfaces =
+ new ArrayList<>(
+ List.of(mFactory.getAvailableInterfaces(/* includeRestricted */ true)));
+
+ if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER) {
+ interfaces.add(mTetheringInterface);
}
- broadcastEthernetStateChange(mEthernetState);
+
+ for (String iface : interfaces) {
+ if (enabled) {
+ NetdUtils.setInterfaceUp(mNetd, iface);
+ } else {
+ NetdUtils.setInterfaceDown(mNetd, iface);
+ }
+ }
+ broadcastEthernetStateChange(mIsEthernetEnabled);
});
}
- private void untrackFactoryInterfaces() {
- for (String iface : mFactory.getAvailableInterfaces(true /* includeRestricted */)) {
- stopTrackingInterface(iface);
- }
+ private int isEthernetEnabledAsInt(boolean state) {
+ return state ? ETHERNET_STATE_ENABLED : ETHERNET_STATE_DISABLED;
}
private void unicastEthernetStateChange(@NonNull IEthernetServiceListener listener,
- int state) {
+ boolean enabled) {
ensureRunningOnEthernetServiceThread();
try {
- listener.onEthernetStateChanged(state);
+ listener.onEthernetStateChanged(isEthernetEnabledAsInt(enabled));
} catch (RemoteException e) {
// Do nothing here.
}
}
- private void broadcastEthernetStateChange(int state) {
+ private void broadcastEthernetStateChange(boolean enabled) {
ensureRunningOnEthernetServiceThread();
final int n = mListeners.beginBroadcast();
for (int i = 0; i < n; i++) {
try {
- mListeners.getBroadcastItem(i).onEthernetStateChanged(state);
+ mListeners.getBroadcastItem(i)
+ .onEthernetStateChanged(isEthernetEnabledAsInt(enabled));
} catch (RemoteException e) {
// Do nothing here.
}
@@ -1011,7 +1020,7 @@
postAndWaitForRunnable(() -> {
pw.println(getClass().getSimpleName());
pw.println("Ethernet State: "
- + (mEthernetState == ETHERNET_STATE_ENABLED ? "enabled" : "disabled"));
+ + (mIsEthernetEnabled ? "enabled" : "disabled"));
pw.println("Ethernet interface name filter: " + mIfaceMatch);
pw.println("Interface used for tethering: " + mTetheringInterface);
pw.println("Tethering interface mode: " + mTetheringInterfaceMode);
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 114cf2e..fb712a1 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -51,12 +51,7 @@
import static android.net.NetworkTemplate.MATCH_WIFI;
import static android.net.TrafficStats.KB_IN_BYTES;
import static android.net.TrafficStats.MB_IN_BYTES;
-import static android.net.TrafficStats.TYPE_RX_BYTES;
-import static android.net.TrafficStats.TYPE_RX_PACKETS;
-import static android.net.TrafficStats.TYPE_TX_BYTES;
-import static android.net.TrafficStats.TYPE_TX_PACKETS;
import static android.net.TrafficStats.UID_TETHERING;
-import static android.net.TrafficStats.UNSUPPORTED;
import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
@@ -74,12 +69,12 @@
import static com.android.net.module.util.DeviceConfigUtils.getDeviceConfigPropertyInt;
import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT;
-import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REMOVE_UIDS;
@@ -131,6 +126,8 @@
import android.net.Uri;
import android.net.netstats.IUsageCallback;
import android.net.netstats.NetworkStatsDataMigrationUtils;
+import android.net.netstats.StatsResult;
+import android.net.netstats.TrafficStatsRateLimitCacheConfig;
import android.net.netstats.provider.INetworkStatsProvider;
import android.net.netstats.provider.INetworkStatsProviderCallback;
import android.net.netstats.provider.NetworkStatsProvider;
@@ -242,13 +239,11 @@
// A message for broadcasting ACTION_NETWORK_STATS_UPDATED in handler thread to prevent
// deadlock.
private static final int MSG_BROADCAST_NETWORK_STATS_UPDATED = 4;
-
/** Flags to control detail level of poll event. */
private static final int FLAG_PERSIST_NETWORK = 0x1;
private static final int FLAG_PERSIST_UID = 0x2;
private static final int FLAG_PERSIST_ALL = FLAG_PERSIST_NETWORK | FLAG_PERSIST_UID;
private static final int FLAG_PERSIST_FORCE = 0x100;
-
/**
* When global alert quota is high, wait for this delay before processing each polling,
* and do not schedule further polls once there is already one queued.
@@ -310,9 +305,16 @@
static final String TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME =
"trafficstats_cache_expiry_duration_ms";
- static final String TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME = "trafficstats_cache_max_entries";
+ static final String TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME =
+ "trafficstats_cache_max_entries";
static final int DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS = 1000;
- static final int DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES = 400;
+ static final int DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES = 400;
+ /**
+ * The delay time between to network stats update intents.
+ * Added to fix intent spams (b/343844995)
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ static final int BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS = 1000;
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
@@ -385,6 +387,7 @@
long getXtPersistBytes(long def);
long getUidPersistBytes(long def);
long getUidTagPersistBytes(long def);
+ long getBroadcastNetworkStatsUpdateDelayMs();
}
private final Object mStatsLock = new Object();
@@ -469,14 +472,40 @@
private long mLastStatsSessionPoll;
+ /**
+ * The timestamp of the most recent network stats broadcast.
+ *
+ * Note that this time could be in the past for completed broadcasts,
+ * or in the future for scheduled broadcasts.
+ *
+ * It is initialized to {@code Long.MIN_VALUE} to ensure that the first broadcast request
+ * is fulfilled immediately, regardless of the delay time.
+ *
+ * This value is used to enforce rate limiting on intents, preventing intent spam.
+ */
+ @GuardedBy("mStatsLock")
+ private long mLatestNetworkStatsUpdatedBroadcastScheduledTime = Long.MIN_VALUE;
+
+ @Nullable
private final TrafficStatsRateLimitCache mTrafficStatsTotalCache;
+ @Nullable
private final TrafficStatsRateLimitCache mTrafficStatsIfaceCache;
+ @Nullable
private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
- static final String TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG =
+ // A feature flag to control whether the client-side rate limit cache should be enabled.
+ static final String TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG =
+ "trafficstats_client_rate_limit_cache_enabled_flag";
+ static final String TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG =
"trafficstats_rate_limit_cache_enabled_flag";
- private final boolean mAlwaysUseTrafficStatsRateLimitCache;
+ static final String BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG =
+ "broadcast_network_stats_updated_rate_limit_enabled_flag";
+ private final boolean mIsTrafficStatsServiceRateLimitCacheEnabled;
private final int mTrafficStatsRateLimitCacheExpiryDuration;
- private final int mTrafficStatsRateLimitCacheMaxEntries;
+ private final int mTrafficStatsServiceRateLimitCacheMaxEntries;
+ private final TrafficStatsRateLimitCacheConfig mTrafficStatsRateLimitCacheClientSideConfig;
+ private final boolean mBroadcastNetworkStatsUpdatedRateLimitEnabled;
+
+
private final Object mOpenSessionCallsLock = new Object();
@@ -667,18 +696,34 @@
mEventLogger = null;
}
- mAlwaysUseTrafficStatsRateLimitCache =
- mDeps.alwaysUseTrafficStatsRateLimitCache(mContext);
+ mTrafficStatsRateLimitCacheClientSideConfig =
+ mDeps.getTrafficStatsRateLimitCacheClientSideConfig(mContext);
+ // If the client side cache feature is enabled, disable the service side
+ // cache unconditionally.
+ mIsTrafficStatsServiceRateLimitCacheEnabled =
+ mDeps.isTrafficStatsServiceRateLimitCacheEnabled(mContext,
+ mTrafficStatsRateLimitCacheClientSideConfig.isCacheEnabled);
+ mBroadcastNetworkStatsUpdatedRateLimitEnabled =
+ mDeps.enabledBroadcastNetworkStatsUpdatedRateLimiting(mContext);
mTrafficStatsRateLimitCacheExpiryDuration =
mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
- mTrafficStatsRateLimitCacheMaxEntries =
- mDeps.getTrafficStatsRateLimitCacheMaxEntries();
- mTrafficStatsTotalCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
- mTrafficStatsIfaceCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
- mTrafficStatsUidCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
+ mTrafficStatsServiceRateLimitCacheMaxEntries =
+ mDeps.getTrafficStatsServiceRateLimitCacheMaxEntries();
+ if (mIsTrafficStatsServiceRateLimitCacheEnabled) {
+ mTrafficStatsTotalCache = new TrafficStatsRateLimitCache(mClock,
+ mTrafficStatsRateLimitCacheExpiryDuration,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
+ mTrafficStatsIfaceCache = new TrafficStatsRateLimitCache(mClock,
+ mTrafficStatsRateLimitCacheExpiryDuration,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
+ mTrafficStatsUidCache = new TrafficStatsRateLimitCache(mClock,
+ mTrafficStatsRateLimitCacheExpiryDuration,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
+ } else {
+ mTrafficStatsTotalCache = null;
+ mTrafficStatsIfaceCache = null;
+ mTrafficStatsUidCache = null;
+ }
// TODO: Remove bpfNetMaps creation and always start SkDestroyListener
// Following code is for the experiment to verify the SkDestroyListener refactoring. Based
@@ -927,14 +972,54 @@
}
/**
- * Get whether TrafficStats rate-limit cache is always applied.
+ * Get whether broadcast network stats update rate limiting is enabled.
*
* This method should only be called once in the constructor,
* to ensure that the code does not need to deal with flag values changing at runtime.
*/
- public boolean alwaysUseTrafficStatsRateLimitCache(@NonNull Context ctx) {
- return SdkLevel.isAtLeastV() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
- ctx, TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG);
+ public boolean enabledBroadcastNetworkStatsUpdatedRateLimiting(Context ctx) {
+ return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+ ctx, BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG);
+ }
+
+ /**
+ * Get client side traffic stats rate-limit cache config.
+ *
+ * This method should only be called once in the constructor,
+ * to ensure that the code does not need to deal with flag values changing at runtime.
+ */
+ @NonNull
+ public TrafficStatsRateLimitCacheConfig getTrafficStatsRateLimitCacheClientSideConfig(
+ @NonNull Context ctx) {
+ final TrafficStatsRateLimitCacheConfig config =
+ new TrafficStatsRateLimitCacheConfig.Builder()
+ .setIsCacheEnabled(DeviceConfigUtils.isTetheringFeatureEnabled(
+ ctx, TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG))
+ .setExpiryDurationMs(getDeviceConfigPropertyInt(
+ NAMESPACE_TETHERING, TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
+ DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS))
+ .setMaxEntries(getDeviceConfigPropertyInt(
+ NAMESPACE_TETHERING,
+ TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME,
+ DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES))
+ .build();
+ return config;
+ }
+
+ /**
+ * Determines whether the service-side rate-limiting cache is enabled.
+ *
+ * The cache is enabled for devices running Android V+ or apps targeting SDK V+
+ * if the `TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG` feature flag
+ * is enabled and client-side caching is disabled.
+ *
+ * This method should only be called once in the constructor,
+ * to ensure that the code does not need to deal with flag values changing at runtime.
+ */
+ public boolean isTrafficStatsServiceRateLimitCacheEnabled(@NonNull Context ctx,
+ boolean clientCacheEnabled) {
+ return !clientCacheEnabled && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+ ctx, TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG);
}
/**
@@ -950,15 +1035,15 @@
}
/**
- * Get TrafficStats rate-limit cache max entries.
+ * Get TrafficStats service side rate-limit cache max entries.
*
* This method should only be called once in the constructor,
* to ensure that the code does not need to deal with flag values changing at runtime.
*/
- public int getTrafficStatsRateLimitCacheMaxEntries() {
+ public int getTrafficStatsServiceRateLimitCacheMaxEntries() {
return getDeviceConfigPropertyInt(
- NAMESPACE_TETHERING, TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME,
- DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES);
+ NAMESPACE_TETHERING, TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME,
+ DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES);
}
/**
@@ -2096,22 +2181,48 @@
}
}
+ /**
+ * Determines whether to use the client-side cache for traffic stats rate limiting.
+ *
+ * This is based on the cache enabled feature flag. If enabled, the client-side cache
+ * is used for V+ devices or callers with V+ target sdk.
+ *
+ * @param callingUid The UID of the app making the request.
+ * @return True if the client-side cache should be used, false otherwise.
+ */
+ private boolean useClientSideCache(int callingUid) {
+ return mTrafficStatsRateLimitCacheClientSideConfig.isCacheEnabled && (SdkLevel.isAtLeastV()
+ || mDeps.isChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, callingUid));
+ }
+
+ /**
+ * Determines whether to use the service-side cache for traffic stats rate limiting.
+ *
+ * This is based on the cache enabled feature flag. If enabled, the service-side cache
+ * is used for V+ devices or callers with V+ target sdk.
+ *
+ * @param callingUid The UID of the app making the request.
+ * @return True if the service-side cache should be used, false otherwise.
+ */
+ private boolean useServiceSideCache(int callingUid) {
+ return mIsTrafficStatsServiceRateLimitCacheEnabled && (SdkLevel.isAtLeastV()
+ || mDeps.isChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, callingUid));
+ }
+
+ @Nullable
@Override
- public long getUidStats(int uid, int type) {
+ public StatsResult getUidStats(int uid) {
final int callingUid = Binder.getCallingUid();
if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
- return UNSUPPORTED;
+ return null;
}
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
-
- if (mAlwaysUseTrafficStatsRateLimitCache
- || mDeps.isChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, callingUid)) {
- final NetworkStats.Entry entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
+ final NetworkStats.Entry entry;
+ if (useServiceSideCache(callingUid)) {
+ entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
() -> mDeps.nativeGetUidStat(uid));
- return getEntryValueForType(entry, type);
- }
+ } else entry = mDeps.nativeGetUidStat(uid);
- return getEntryValueForType(mDeps.nativeGetUidStat(uid), type);
+ return getStatsResultFromEntryOrNull(entry);
}
@Nullable
@@ -2128,50 +2239,24 @@
return entry;
}
+ @Nullable
@Override
- public long getIfaceStats(@NonNull String iface, int type) {
+ public StatsResult getIfaceStats(@NonNull String iface) {
Objects.requireNonNull(iface);
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
- if (mAlwaysUseTrafficStatsRateLimitCache
- || mDeps.isChangeEnabled(
- ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
- final NetworkStats.Entry entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
+ final NetworkStats.Entry entry;
+ if (useServiceSideCache(Binder.getCallingUid())) {
+ entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
() -> getIfaceStatsInternal(iface));
- return getEntryValueForType(entry, type);
- }
+ } else entry = getIfaceStatsInternal(iface);
- return getEntryValueForType(getIfaceStatsInternal(iface), type);
+ return getStatsResultFromEntryOrNull(entry);
}
- private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
- if (entry == null) return UNSUPPORTED;
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
- switch (type) {
- case TYPE_RX_BYTES:
- return entry.rxBytes;
- case TYPE_RX_PACKETS:
- return entry.rxPackets;
- case TYPE_TX_BYTES:
- return entry.txBytes;
- case TYPE_TX_PACKETS:
- return entry.txPackets;
- default:
- throw new IllegalStateException("Bug: Invalid type: "
- + type + " should not reach here.");
- }
- }
-
- private boolean isEntryValueTypeValid(int type) {
- switch (type) {
- case TYPE_RX_BYTES:
- case TYPE_RX_PACKETS:
- case TYPE_TX_BYTES:
- case TYPE_TX_PACKETS:
- return true;
- default :
- return false;
- }
+ @Nullable
+ private StatsResult getStatsResultFromEntryOrNull(@Nullable NetworkStats.Entry entry) {
+ if (entry == null) return null;
+ return new StatsResult(entry.rxBytes, entry.rxPackets, entry.txBytes, entry.txPackets);
}
@Nullable
@@ -2184,26 +2269,39 @@
return entry;
}
+ @Nullable
@Override
- public long getTotalStats(int type) {
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
- if (mAlwaysUseTrafficStatsRateLimitCache
- || mDeps.isChangeEnabled(
- ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
- final NetworkStats.Entry entry = mTrafficStatsTotalCache.getOrCompute(
+ public StatsResult getTotalStats() {
+ final NetworkStats.Entry entry;
+ if (useServiceSideCache(Binder.getCallingUid())) {
+ entry = mTrafficStatsTotalCache.getOrCompute(
IFACE_ALL, UID_ALL, () -> getTotalStatsInternal());
- return getEntryValueForType(entry, type);
- }
+ } else entry = getTotalStatsInternal();
- return getEntryValueForType(getTotalStatsInternal(), type);
+ return getStatsResultFromEntryOrNull(entry);
}
@Override
public void clearTrafficStatsRateLimitCaches() {
PermissionUtils.enforceNetworkStackPermissionOr(mContext, NETWORK_SETTINGS);
- mTrafficStatsUidCache.clear();
- mTrafficStatsIfaceCache.clear();
- mTrafficStatsTotalCache.clear();
+ if (mIsTrafficStatsServiceRateLimitCacheEnabled) {
+ mTrafficStatsUidCache.clear();
+ mTrafficStatsIfaceCache.clear();
+ mTrafficStatsTotalCache.clear();
+ }
+ }
+
+ @Override
+ public TrafficStatsRateLimitCacheConfig getRateLimitCacheConfig() {
+ // Build a per uid config for the client based on the checking result.
+ final TrafficStatsRateLimitCacheConfig config =
+ new TrafficStatsRateLimitCacheConfig.Builder()
+ .setIsCacheEnabled(useClientSideCache(Binder.getCallingUid()))
+ .setExpiryDurationMs(
+ mTrafficStatsRateLimitCacheClientSideConfig.expiryDurationMs)
+ .setMaxEntries(mTrafficStatsRateLimitCacheClientSideConfig.maxEntries)
+ .build();
+ return config;
}
private NetworkStats.Entry getProviderIfaceStats(@Nullable String iface) {
@@ -2645,8 +2743,22 @@
performSampleLocked();
}
- // finally, dispatch updated event to any listeners
- mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+ // Dispatch updated event to listeners, preventing intent spamming
+ // (b/343844995) possibly from abnormal modem RAT changes or misbehaving
+ // app calls (see NetworkStatsEventLogger#POLL_REASON_* for possible reasons).
+ // If no broadcasts are scheduled, use the time of the last broadcast
+ // to schedule the next one ASAP.
+ if (!mBroadcastNetworkStatsUpdatedRateLimitEnabled) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+ } else if (mLatestNetworkStatsUpdatedBroadcastScheduledTime < SystemClock.uptimeMillis()) {
+ mLatestNetworkStatsUpdatedBroadcastScheduledTime = Math.max(
+ mLatestNetworkStatsUpdatedBroadcastScheduledTime
+ + mSettings.getBroadcastNetworkStatsUpdateDelayMs(),
+ SystemClock.uptimeMillis()
+ );
+ mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED),
+ mLatestNetworkStatsUpdatedBroadcastScheduledTime);
+ }
Trace.traceEnd(TRACE_TAG_NETWORK);
}
@@ -2959,12 +3071,17 @@
} catch (IOException e) {
pw.println("(failed to dump FastDataInput counters)");
}
- pw.print("trafficstats.cache.alwaysuse", mAlwaysUseTrafficStatsRateLimitCache);
+ pw.print("trafficstats.service.cache.isenabled",
+ mIsTrafficStatsServiceRateLimitCacheEnabled);
pw.println();
pw.print(TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
mTrafficStatsRateLimitCacheExpiryDuration);
pw.println();
- pw.print(TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME, mTrafficStatsRateLimitCacheMaxEntries);
+ pw.print(TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
+ pw.println();
+ pw.print("trafficstats.client.cache.config",
+ mTrafficStatsRateLimitCacheClientSideConfig);
pw.println();
pw.decreaseIndent();
@@ -3605,6 +3722,11 @@
public long getUidTagPersistBytes(long def) {
return def;
}
+
+ @Override
+ public long getBroadcastNetworkStatsUpdateDelayMs() {
+ return BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
+ }
}
// TODO: Read stats by using BpfNetMapsReader.
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index ca97d07..667aad1 100644
--- a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -19,9 +19,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.NetworkStats;
-import android.util.LruCache;
-import com.android.internal.annotations.GuardedBy;
+import com.android.net.module.util.LruCacheWithExpiry;
import java.time.Clock;
import java.util.Objects;
@@ -31,9 +30,8 @@
* A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
* with an adjustable expiry duration to manage data freshness.
*/
-class TrafficStatsRateLimitCache {
- private final Clock mClock;
- private final long mExpiryDurationMs;
+class TrafficStatsRateLimitCache extends
+ LruCacheWithExpiry<TrafficStatsRateLimitCache.TrafficStatsCacheKey, NetworkStats.Entry> {
/**
* Constructs a new {@link TrafficStatsRateLimitCache} with the specified expiry duration.
@@ -43,19 +41,17 @@
* @param maxSize Maximum number of entries.
*/
TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
- mClock = clock;
- mExpiryDurationMs = expiryDurationMs;
- mMap = new LruCache<>(maxSize);
+ super(clock, expiryDurationMs, maxSize, it -> !it.isEmpty());
}
- private static class TrafficStatsCacheKey {
+ public static class TrafficStatsCacheKey {
@Nullable
- public final String iface;
- public final int uid;
+ private final String mIface;
+ private final int mUid;
TrafficStatsCacheKey(@Nullable String iface, int uid) {
- this.iface = iface;
- this.uid = uid;
+ this.mIface = iface;
+ this.mUid = uid;
}
@Override
@@ -63,29 +59,15 @@
if (this == o) return true;
if (!(o instanceof TrafficStatsCacheKey)) return false;
TrafficStatsCacheKey that = (TrafficStatsCacheKey) o;
- return uid == that.uid && Objects.equals(iface, that.iface);
+ return mUid == that.mUid && Objects.equals(mIface, that.mIface);
}
@Override
public int hashCode() {
- return Objects.hash(iface, uid);
+ return Objects.hash(mIface, mUid);
}
}
- private static class TrafficStatsCacheValue {
- public final long timestamp;
- @NonNull
- public final NetworkStats.Entry entry;
-
- TrafficStatsCacheValue(long timestamp, NetworkStats.Entry entry) {
- this.timestamp = timestamp;
- this.entry = entry;
- }
- }
-
- @GuardedBy("mMap")
- private final LruCache<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap;
-
/**
* Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
*
@@ -95,16 +77,7 @@
*/
@Nullable
NetworkStats.Entry get(String iface, int uid) {
- final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
- synchronized (mMap) { // Synchronize for thread-safety
- final TrafficStatsCacheValue value = mMap.get(key);
- if (value != null && !isExpired(value.timestamp)) {
- return value.entry;
- } else {
- mMap.remove(key); // Remove expired entries
- return null;
- }
- }
+ return super.get(new TrafficStatsCacheKey(iface, uid));
}
/**
@@ -122,19 +95,7 @@
@Nullable
NetworkStats.Entry getOrCompute(String iface, int uid,
@NonNull Supplier<NetworkStats.Entry> supplier) {
- synchronized (mMap) {
- final NetworkStats.Entry cachedValue = get(iface, uid);
- if (cachedValue != null) {
- return cachedValue;
- }
-
- // Entry not found or expired, compute it
- final NetworkStats.Entry computedEntry = supplier.get();
- if (computedEntry != null && !computedEntry.isEmpty()) {
- put(iface, uid, computedEntry);
- }
- return computedEntry;
- }
+ return super.getOrCompute(new TrafficStatsCacheKey(iface, uid), supplier);
}
/**
@@ -145,23 +106,7 @@
* @param entry The {@link NetworkStats.Entry} to store in the cache.
*/
void put(String iface, int uid, @NonNull final NetworkStats.Entry entry) {
- Objects.requireNonNull(entry);
- final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
- synchronized (mMap) { // Synchronize for thread-safety
- mMap.put(key, new TrafficStatsCacheValue(mClock.millis(), entry));
- }
+ super.put(new TrafficStatsCacheKey(iface, uid), entry);
}
- /**
- * Clear the cache.
- */
- void clear() {
- synchronized (mMap) {
- mMap.evictAll();
- }
- }
-
- private boolean isExpired(long timestamp) {
- return mClock.millis() > timestamp + mExpiryDurationMs;
- }
}
diff --git a/service/Android.bp b/service/Android.bp
index 1a0e045..fd3d4a3 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -125,6 +125,7 @@
"libmodules-utils-build",
"libnetjniutils",
"libnet_utils_device_common_bpfjni",
+ "libnet_utils_device_common_timerfdjni",
"netd_aidl_interface-lateststable-ndk",
],
shared_libs: [
@@ -161,7 +162,7 @@
],
libs: [
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
// The framework-connectivity-t library is only available on T+ platforms
// so any calls to it must be protected with a check to ensure that it is
@@ -175,12 +176,12 @@
// TODO: figure out why just using "framework-tethering" uses the stubs, even though both
// service-connectivity and framework-tethering are in the same APEX.
"framework-tethering.impl",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"unsupportedappusage",
"ServiceConnectivityResources",
- "framework-statsd",
- "framework-permission",
- "framework-permission-s",
+ "framework-statsd.stubs.module_lib",
+ "framework-permission.stubs.module_lib",
+ "framework-permission-s.stubs.module_lib",
],
static_libs: [
// Do not add libs here if they are already included
@@ -206,6 +207,7 @@
},
visibility: [
"//packages/modules/Connectivity/service-t",
+ "//packages/modules/Connectivity/networksecurity:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/service:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
@@ -247,6 +249,7 @@
"service-connectivity-pre-jarjar",
"service-connectivity-tiramisu-pre-jarjar",
"service-nearby-pre-jarjar",
+ "service-networksecurity-pre-jarjar",
service_remoteauth_pre_jarjar_lib,
"service-thread-pre-jarjar",
],
@@ -262,10 +265,10 @@
"framework-connectivity.impl",
"framework-connectivity-t.impl",
"framework-tethering.impl",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"libprotobuf-java-nano",
- "framework-permission",
- "framework-permission-s",
+ "framework-permission.stubs.module_lib",
+ "framework-permission-s.stubs.module_lib",
],
jarjar_rules: ":connectivity-jarjar-rules",
apex_available: [
@@ -308,13 +311,14 @@
apex_available: ["com.android.tethering"],
}
-genrule {
+java_genrule {
name: "connectivity-jarjar-rules",
defaults: ["jarjar-rules-combine-defaults"],
srcs: [
":framework-connectivity-jarjar-rules",
":service-connectivity-jarjar-gen",
":service-nearby-jarjar-gen",
+ ":service-networksecurity-jarjar-gen",
":service-remoteauth-jarjar-gen",
":service-thread-jarjar-gen",
],
@@ -404,6 +408,24 @@
visibility: ["//visibility:private"],
}
+java_genrule {
+ name: "service-networksecurity-jarjar-gen",
+ tool_files: [
+ ":service-networksecurity-pre-jarjar{.jar}",
+ "jarjar-excludes.txt",
+ ],
+ tools: [
+ "jarjar-rules-generator",
+ ],
+ out: ["service_ct_jarjar_rules.txt"],
+ cmd: "$(location jarjar-rules-generator) " +
+ "$(location :service-networksecurity-pre-jarjar{.jar}) " +
+ "--prefix com.android.server.net.ct " +
+ "--excludes $(location jarjar-excludes.txt) " +
+ "--output $(out)",
+ visibility: ["//visibility:private"],
+}
+
genrule {
name: "statslog-connectivity-java-gen",
tools: ["stats-log-api-gen"],
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index 2621256..be9b2b5 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -33,6 +33,7 @@
"com.android.tethering",
],
certificate: ":com.android.connectivity.resources.certificate",
+ updatable: true,
}
android_app_certificate {
diff --git a/service/ServiceConnectivityResources/res/values-ar/strings.xml b/service/ServiceConnectivityResources/res/values-ar/strings.xml
index 92dd9a1..8cefec4 100644
--- a/service/ServiceConnectivityResources/res/values-ar/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ar/strings.xml
@@ -40,7 +40,7 @@
<item msgid="5624324321165953608">"Wi-Fi"</item>
<item msgid="5667906231066981731">"بلوتوث"</item>
<item msgid="346574747471703768">"إيثرنت"</item>
- <item msgid="5734728378097476003">"شبكة افتراضية خاصة (VPN)"</item>
+ <item msgid="5734728378097476003">"شبكة VPN"</item>
</string-array>
<string name="network_switch_type_name_unknown" msgid="5116448402191972082">"نوع شبكة غير معروف"</string>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values-eu/strings.xml b/service/ServiceConnectivityResources/res/values-eu/strings.xml
index 81d8ddb..5a0a9d4 100644
--- a/service/ServiceConnectivityResources/res/values-eu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-eu/strings.xml
@@ -18,7 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Sistemaren konexio-baliabideak"</string>
- <string name="wifi_available_sign_in" msgid="8041178343789805553">"Hasi saioa Wi-Fi sarean"</string>
+ <string name="wifi_available_sign_in" msgid="8041178343789805553">"Hasi saioa wifi-sarean"</string>
<string name="network_available_sign_in" msgid="2622520134876355561">"Hasi saioa sarean"</string>
<!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
<skip />
diff --git a/service/ServiceConnectivityResources/res/values-fa/strings.xml b/service/ServiceConnectivityResources/res/values-fa/strings.xml
index 02c60df..09f1255 100644
--- a/service/ServiceConnectivityResources/res/values-fa/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-fa/strings.xml
@@ -23,15 +23,15 @@
<!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
<skip />
<string name="mobile_network_available_no_internet" msgid="1000871587359324217">"اتصال اینترنت وجود ندارد"</string>
- <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ممکن است داده <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> تمام شده باشد. برای گزینهها ضربه بزنید."</string>
- <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ممکن است داده شما تمام شده باشد. برای گزینهها ضربه بزنید."</string>
+ <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ممکن است داده <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> تمام شده باشد. برای گزینهها تکضرب بزنید."</string>
+ <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ممکن است داده شما تمام شده باشد. برای گزینهها تکضرب بزنید."</string>
<string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> به اینترنت دسترسی ندارد"</string>
- <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"برای گزینهها ضربه بزنید"</string>
+ <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"برای گزینهها تکضرب بزنید"</string>
<string name="mobile_no_internet" msgid="4087718456753201450">"شبکه تلفن همراه به اینترنت دسترسی ندارد"</string>
<string name="other_networks_no_internet" msgid="5693932964749676542">"شبکه به اینترنت دسترسی ندارد"</string>
<string name="private_dns_broken_detailed" msgid="2677123850463207823">"سرور DNS خصوصی قابل دسترسی نیست"</string>
<string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> اتصال محدودی دارد"</string>
- <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"بههرصورت، برای اتصال ضربه بزنید"</string>
+ <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"بههرصورت، برای اتصال تکضرب بزنید"</string>
<string name="network_switch_metered" msgid="5016937523571166319">"به <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> تغییر کرد"</string>
<string name="network_switch_metered_detail" msgid="1257300152739542096">"وقتی <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> به اینترنت دسترسی نداشته باشد، دستگاه از <xliff:g id="NEW_NETWORK">%1$s</xliff:g> استفاده میکند. ممکن است هزینههایی اعمال شود."</string>
<string name="network_switch_metered_toast" msgid="70691146054130335">"از <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> به <xliff:g id="NEW_NETWORK">%2$s</xliff:g> تغییر کرد"</string>
diff --git a/service/ServiceConnectivityResources/res/values-sw/strings.xml b/service/ServiceConnectivityResources/res/values-sw/strings.xml
index 29ec013..9ff9ada 100644
--- a/service/ServiceConnectivityResources/res/values-sw/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sw/strings.xml
@@ -25,7 +25,7 @@
<string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Hakuna intaneti"</string>
<string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Huenda data ya <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> imeisha. Gusa ili upate chaguo."</string>
<string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Huenda data yako imeisha. Gusa ili upate chaguo."</string>
- <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> haina uwezo wa kufikia intaneti"</string>
+ <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> haina intaneti"</string>
<string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Gusa ili upate chaguo"</string>
<string name="mobile_no_internet" msgid="4087718456753201450">"Mtandao wa simu hauna uwezo wa kufikia intaneti"</string>
<string name="other_networks_no_internet" msgid="5693932964749676542">"Mtandao hauna uwezo wa kufikia intaneti"</string>
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index bb70d4f..8e01260 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -26,6 +26,8 @@
int register_android_server_net_NetworkStatsFactory(JNIEnv* env);
int register_android_server_net_NetworkStatsService(JNIEnv* env);
int register_com_android_server_ServiceManagerWrapper(JNIEnv* env);
+int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+ char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv *env;
@@ -56,6 +58,12 @@
}
}
+ if (register_com_android_net_module_util_TimerFdUtils(
+ env, "android/net/connectivity/com/android/net/module/util/"
+ "TimerFdUtils") < 0) {
+ return JNI_ERR;
+ }
+
return JNI_VERSION_1_6;
}
diff --git a/service/libconnectivity/Android.bp b/service/libconnectivity/Android.bp
index 3a72134..9bfe3a9 100644
--- a/service/libconnectivity/Android.bp
+++ b/service/libconnectivity/Android.bp
@@ -42,6 +42,7 @@
],
llndk: {
symbol_file: "libconnectivity_native.map.txt",
+ moved_to_apex: true,
},
stubs: {
symbol_file: "libconnectivity_native.map.txt",
diff --git a/service/src/com/android/server/CallbackQueue.java b/service/src/com/android/server/CallbackQueue.java
index 060a984..4e068ea 100644
--- a/service/src/com/android/server/CallbackQueue.java
+++ b/service/src/com/android/server/CallbackQueue.java
@@ -34,8 +34,7 @@
* queue.forEach(netId, callbackId -> { [...] });
* queue.addCallback(netId, callbackId);
* [...]
- * queue.shrinkToLength();
- * storedCallbacks = queue.getBackingArray();
+ * storedCallbacks = queue.getMinimizedBackingArray();
* </pre>
*
* <p>This class is not thread-safe.
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
old mode 100755
new mode 100644
index 4d4dacf..0d0f6fc
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -38,8 +38,8 @@
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
-import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.BLOCKED_REASON_NETWORK_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.CALLBACK_AVAILABLE;
import static android.net.ConnectivityManager.CALLBACK_BLK_CHANGED;
import static android.net.ConnectivityManager.CALLBACK_CAP_CHANGED;
@@ -56,6 +56,8 @@
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
+import static android.net.ConnectivityManager.NETID_UNSET;
+import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_NONE;
import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
import static android.net.ConnectivityManager.TYPE_ETHERNET;
@@ -76,7 +78,6 @@
import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
import static android.net.ConnectivityManager.getNetworkTypeName;
import static android.net.ConnectivityManager.isNetworkTypeValid;
-import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
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;
@@ -120,6 +121,7 @@
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.provider.DeviceConfig.NAMESPACE_TETHERING;
import static android.system.OsConstants.ETH_P_ALL;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
@@ -144,10 +146,12 @@
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
+import static com.android.server.connectivity.ConnectivityFlags.CELLULAR_DATA_INACTIVITY_TIMEOUT;
import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
+import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
-import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
+import static com.android.server.connectivity.ConnectivityFlags.WIFI_DATA_INACTIVITY_TIMEOUT;
import android.Manifest;
import android.annotation.CheckResult;
@@ -406,6 +410,7 @@
import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.TreeSet;
+import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
@@ -1608,6 +1613,18 @@
connectivityServiceInternalHandler);
}
+ /** Returns the data inactivity timeout to be used for cellular networks */
+ public int getDefaultCellularDataInactivityTimeout() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING,
+ CELLULAR_DATA_INACTIVITY_TIMEOUT, 10);
+ }
+
+ /** Returns the data inactivity timeout to be used for WiFi networks */
+ public int getDefaultWifiDataInactivityTimeout() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING,
+ WIFI_DATA_INACTIVITY_TIMEOUT, 15);
+ }
+
/**
* @see DeviceConfigUtils#isTetheringFeatureEnabled
*/
@@ -1956,8 +1973,13 @@
// But reading the trunk stable flags from mainline modules is not supported yet.
// So enabling this feature on V+ release.
mTrackMultiNetworkActivities = mDeps.isAtLeastV();
+ final int defaultCellularDataInactivityTimeout =
+ mDeps.getDefaultCellularDataInactivityTimeout();
+ final int defaultWifiDataInactivityTimeout =
+ mDeps.getDefaultWifiDataInactivityTimeout();
mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, mHandler,
- mTrackMultiNetworkActivities);
+ mTrackMultiNetworkActivities, defaultCellularDataInactivityTimeout,
+ defaultWifiDataInactivityTimeout);
final NetdCallback netdCallback = new NetdCallback();
try {
@@ -2026,7 +2048,8 @@
mCdmps = null;
}
- mRoutingCoordinatorService = new RoutingCoordinatorService(netd);
+ mRoutingCoordinatorService =
+ new RoutingCoordinatorService(netd, this::getAllNetworks, mContext);
mMulticastRoutingCoordinatorService =
mDeps.makeMulticastRoutingCoordinatorService(mHandler);
@@ -2147,7 +2170,7 @@
}
@VisibleForTesting
- void updateMobileDataPreferredUids() {
+ public void updateMobileDataPreferredUids() {
mHandler.sendEmptyMessage(EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
}
@@ -3403,7 +3426,7 @@
}
@VisibleForTesting
- void handleBlockedReasonsChanged(List<Pair<Integer, Integer>> reasonsList) {
+ public void handleBlockedReasonsChanged(List<Pair<Integer, Integer>> reasonsList) {
for (Pair<Integer, Integer> reasons: reasonsList) {
final int uid = reasons.first;
final int blockedReasons = reasons.second;
@@ -3472,7 +3495,7 @@
private void handleFrozenUids(int[] uids, int[] frozenStates) {
ensureRunningOnConnectivityServiceThread();
handleDestroyFrozenSockets(uids, frozenStates);
- // TODO: handle freezing NetworkCallbacks
+ handleFreezeNetworkCallbacks(uids, frozenStates);
}
private void handleDestroyFrozenSockets(int[] uids, int[] frozenStates) {
@@ -3490,6 +3513,73 @@
}
}
+ private void handleFreezeNetworkCallbacks(int[] uids, int[] frozenStates) {
+ if (!mQueueCallbacksForFrozenApps) {
+ return;
+ }
+ for (int i = 0; i < uids.length; i++) {
+ final int uid = uids[i];
+ // These counters may be modified on different threads, but using them here is fine
+ // because this is only an optimization where wrong behavior would only happen if they
+ // are zero even though there is a request registered. This is not possible as they are
+ // always incremented before posting messages to register, and decremented on the
+ // handler thread when unregistering.
+ if (mSystemNetworkRequestCounter.get(uid) == 0
+ && mNetworkRequestCounter.get(uid) == 0) {
+ // Avoid iterating requests if there isn't any. The counters only track app requests
+ // and not internal requests (for example always-on requests which do not have a
+ // mMessenger), so it does not completely match the content of mRequests. This is OK
+ // as only app requests need to be frozen.
+ continue;
+ }
+
+ if (frozenStates[i] == UID_FROZEN_STATE_FROZEN) {
+ freezeNetworkCallbacksForUid(uid);
+ } else {
+ unfreezeNetworkCallbacksForUid(uid);
+ }
+ }
+ }
+
+ /**
+ * Suspend callbacks for a UID that was just frozen.
+ *
+ * <p>Note that it is not possible for a process to be frozen during a blocking binder call
+ * (see CachedAppOptimizer.freezeBinder), and IConnectivityManager callback registrations are
+ * blocking binder calls, so no callback can be registered while the UID is frozen. This means
+ * it is not necessary to check frozen state on new callback registrations, and calling this
+ * method when a UID is newly frozen is sufficient.
+ *
+ * <p>If it ever becomes possible for a process to be frozen during a blocking binder call,
+ * ConnectivityService will need to handle freezing callbacks that reach ConnectivityService
+ * after the app was frozen when being registered.
+ */
+ private void freezeNetworkCallbacksForUid(int uid) {
+ if (DDBG) Log.d(TAG, "Freezing callbacks for UID " + uid);
+ for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (nri.mUid != uid) continue;
+ // mNetworkRequests can have duplicate values for multilayer requests, but calling
+ // onFrozen multiple times is fine.
+ // If freezeNetworkCallbacksForUid was called multiple times in a raw for a frozen UID
+ // (which would be incorrect), this would also handle it gracefully.
+ nri.onFrozen();
+ }
+ }
+
+ private void unfreezeNetworkCallbacksForUid(int uid) {
+ // This sends all callbacks for one NetworkRequest at a time, which may not be the
+ // same order they were queued in, but different network requests use different
+ // binder objects, so the relative order of their callbacks is not guaranteed.
+ // If callbacks are not queued, callbacks from different binder objects may be
+ // posted on different threads when the process is unfrozen, so even if they were
+ // called a long time apart while the process was frozen, they may still appear in
+ // different order when unfreezing it.
+ for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (nri.mUid != uid) continue;
+ nri.sendQueuedCallbacks();
+ }
+ }
+
private void handleUpdateFirewallDestroySocketReasons(
List<Pair<Integer, Integer>> reasonsList) {
if (!shouldTrackFirewallDestroySocketReasons()) {
@@ -4371,9 +4461,8 @@
pw.println();
pw.println("Multicast routing supported: " +
(mMulticastRoutingCoordinatorService != null));
-
- pw.println();
pw.println("Background firewall chain enabled: " + mBackgroundFirewallChainEnabled);
+ pw.println("IngressToVpnAddressFiltering: " + mIngressToVpnAddressFiltering);
}
private void dumpNetworks(IndentingPrintWriter pw) {
@@ -5462,7 +5551,7 @@
}
// Delayed teardown.
- if (nai.isCreated()) {
+ if (nai.isCreated() && !nai.isDestroyed()) {
try {
mNetd.networkSetPermissionForNetwork(nai.network.netId, INetd.PERMISSION_SYSTEM);
} catch (RemoteException e) {
@@ -5936,10 +6025,8 @@
final NetworkAgentInfo satisfier = nri.getSatisfier();
if (null != satisfier) {
try {
- mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
- satisfier.network.getNetId(),
- toUidRangeStableParcels(nri.getUids()),
- nri.getPreferenceOrderForNetd()));
+ modifyNetworkUidRanges(false /* add */, satisfier, nri.getUids(),
+ nri.getPreferenceOrderForNetd());
} catch (RemoteException e) {
loge("Exception setting network preference default network", e);
}
@@ -7544,6 +7631,29 @@
// single NetworkRequest in mRequests.
final List<NetworkRequest> mRequests;
+ /**
+ * List of callbacks that are queued for sending later when the requesting app is unfrozen.
+ *
+ * <p>There may typically be hundreds of NetworkRequestInfo, so a memory-efficient structure
+ * (just an int[]) is used to keep queued callbacks. This reduces the number of object
+ * references.
+ *
+ * <p>This is intended to be used with {@link CallbackQueue} which defines the internal
+ * format.
+ */
+ @NonNull
+ private int[] mQueuedCallbacks = new int[0];
+
+ private static final int MATCHED_NETID_NOT_FROZEN = -1;
+
+ /**
+ * If this request was already satisfied by a network when the requesting UID was frozen,
+ * the netId that was matched at that time. Otherwise, NETID_UNSET if no network was
+ * satisfying this request when frozen (including if this is a listen and not a request),
+ * and MATCHED_NETID_NOT_FROZEN if not frozen.
+ */
+ private int mMatchedNetIdWhenFrozen = MATCHED_NETID_NOT_FROZEN;
+
// mSatisfier and mActiveRequest rely on one another therefore set them together.
void setSatisfier(
@Nullable final NetworkAgentInfo satisfier,
@@ -7715,6 +7825,8 @@
}
setSatisfier(satisfier, activeRequest);
}
+ mMatchedNetIdWhenFrozen = nri.mMatchedNetIdWhenFrozen;
+ mQueuedCallbacks = nri.mQueuedCallbacks;
mMessenger = nri.mMessenger;
mBinder = nri.mBinder;
mPid = nri.mPid;
@@ -7779,11 +7891,190 @@
}
}
+ /**
+ * Called when this NRI is being frozen.
+ *
+ * <p>Calling this method multiple times when the NRI is frozen is fine. This may happen
+ * if iterating through the NetworkRequest -> NRI map since there are duplicates in the
+ * NRI values for multilayer requests. It may also happen if an app is frozen, killed,
+ * restarted and refrozen since there is no callback sent when processes are killed, but in
+ * that case the callbacks to the killed app do not matter.
+ */
+ void onFrozen() {
+ if (mMatchedNetIdWhenFrozen != MATCHED_NETID_NOT_FROZEN) {
+ // Already frozen
+ return;
+ }
+ if (mSatisfier != null) {
+ mMatchedNetIdWhenFrozen = mSatisfier.network.netId;
+ } else {
+ mMatchedNetIdWhenFrozen = NETID_UNSET;
+ }
+ }
+
+ boolean maybeQueueCallback(@NonNull NetworkAgentInfo nai, int callbackId) {
+ if (mMatchedNetIdWhenFrozen == MATCHED_NETID_NOT_FROZEN) {
+ return false;
+ }
+
+ boolean ignoreThisCallback = false;
+ final int netId = nai.network.netId;
+ final CallbackQueue queue = new CallbackQueue(mQueuedCallbacks);
+ // Based on the new callback, clear previous callbacks that are no longer necessary.
+ // For example, if the network is lost, there is no need to send intermediate callbacks.
+ switch (callbackId) {
+ // PRECHECK is not an API and not very meaningful, do not deliver it for frozen apps
+ // Networks are likely to already be lost when the app is unfrozen, also skip LOSING
+ case CALLBACK_PRECHECK:
+ case CALLBACK_LOSING:
+ ignoreThisCallback = true;
+ break;
+ case CALLBACK_LOST:
+ // All callbacks for this netId before onLost are unnecessary. And onLost itself
+ // is also unnecessary if onAvailable was previously queued for this netId: the
+ // Network just appeared and disappeared while the app was frozen.
+ ignoreThisCallback = queue.hasCallback(netId, CALLBACK_AVAILABLE);
+ queue.removeCallbacksForNetId(netId);
+ break;
+ case CALLBACK_AVAILABLE:
+ if (mSatisfier != null) {
+ // For requests that are satisfied by individual networks (not LISTEN), when
+ // AVAILABLE is received, the request is matching a new Network, so previous
+ // callbacks (for other Networks) are unnecessary.
+ queue.clear();
+ }
+ break;
+ case CALLBACK_SUSPENDED:
+ case CALLBACK_RESUMED:
+ if (queue.hasCallback(netId, CALLBACK_AVAILABLE)) {
+ // AVAILABLE will already send the latest suspended status
+ ignoreThisCallback = true;
+ break;
+ }
+ // If SUSPENDED was queued, just remove it from the queue instead of sending
+ // RESUMED; and vice-versa.
+ final int otherCb = callbackId == CALLBACK_SUSPENDED
+ ? CALLBACK_RESUMED
+ : CALLBACK_SUSPENDED;
+ ignoreThisCallback = queue.removeCallbacks(netId, otherCb);
+ break;
+ case CALLBACK_CAP_CHANGED:
+ case CALLBACK_IP_CHANGED:
+ case CALLBACK_LOCAL_NETWORK_INFO_CHANGED:
+ case CALLBACK_BLK_CHANGED:
+ ignoreThisCallback = queue.hasCallback(netId, CALLBACK_AVAILABLE);
+ break;
+ default:
+ Log.wtf(TAG, "Unexpected callback type: "
+ + ConnectivityManager.getCallbackName(callbackId));
+ return false;
+ }
+
+ if (!ignoreThisCallback) {
+ // For non-listen (matching) callbacks, AVAILABLE can appear in the queue twice in a
+ // row for the same network if the new AVAILABLE suppressed intermediate AVAILABLEs
+ // for other networks. Example:
+ // A is matched, app is frozen, B is matched, A is matched again (removes callbacks
+ // for B), app is unfrozen.
+ // In that case call AVAILABLE sub-callbacks to update state, but not AVAILABLE
+ // itself.
+ if (callbackId == CALLBACK_AVAILABLE && netId == mMatchedNetIdWhenFrozen) {
+ // The queue should have been cleared here, since this is AVAILABLE on a
+ // non-listen callback (mMatchedNetIdWhenFrozen is set).
+ addAvailableSubCallbacks(nai, queue);
+ } else {
+ // When unfreezing, no need to send a callback multiple times for the same netId
+ queue.removeCallbacks(netId, callbackId);
+ // TODO: this code always adds the callback for simplicity. It would save
+ // some CPU/memory if the code instead only added to the queue callbacks where
+ // isCallbackOverridden=true, or which need to be in the queue because they
+ // affect other callbacks that are overridden.
+ queue.addCallback(netId, callbackId);
+ }
+ }
+ // Instead of shrinking the queue, possibly reallocating, the NRI could keep the array
+ // and length in memory for future adds, but this saves memory by avoiding the cost
+ // of an extra member and of unused array length (there are often hundreds of NRIs).
+ mQueuedCallbacks = queue.getMinimizedBackingArray();
+ return true;
+ }
+
+ /**
+ * Called when this NRI is being unfrozen to stop queueing, and send queued callbacks.
+ *
+ * <p>Calling this method multiple times when the NRI is unfrozen (for example iterating
+ * through the NetworkRequest -> NRI map where there are duplicate values for multilayer
+ * requests) is fine.
+ */
+ void sendQueuedCallbacks() {
+ mMatchedNetIdWhenFrozen = MATCHED_NETID_NOT_FROZEN;
+ if (mQueuedCallbacks.length == 0) {
+ return;
+ }
+ new CallbackQueue(mQueuedCallbacks).forEach((netId, callbackId) -> {
+ // For CALLBACK_LOST only, there will not be a NAI for the netId. Build and send the
+ // callback directly.
+ if (callbackId == CALLBACK_LOST) {
+ if (isCallbackOverridden(CALLBACK_LOST)) {
+ final Bundle cbBundle = makeCommonBundleForCallback(this,
+ new Network(netId));
+ callCallbackForRequest(this, CALLBACK_LOST, cbBundle, 0 /* arg1 */);
+ }
+ return; // Next item in forEach
+ }
+
+ // Other callbacks should always have a NAI, because if a Network disconnects
+ // LOST will be called, unless the request is no longer satisfied by that Network in
+ // which case AVAILABLE will have been called for another Network. In both cases
+ // previous callbacks are cleared.
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
+ if (nai == null) {
+ Log.wtf(TAG, "Missing NetworkAgentInfo for net " + netId
+ + " for callback " + callbackId);
+ return; // Next item in forEach
+ }
+
+ final int arg1 =
+ callbackId == CALLBACK_AVAILABLE || callbackId == CALLBACK_BLK_CHANGED
+ ? getBlockedState(nai, mAsUid)
+ : 0;
+ callCallbackForRequest(this, nai, callbackId, arg1);
+ });
+ mQueuedCallbacks = new int[0];
+ }
+
boolean isCallbackOverridden(int callbackId) {
return !mUseDeclaredMethodsForCallbacksEnabled
|| (mDeclaredMethodsFlags & (1 << callbackId)) != 0;
}
+ /**
+ * Queue all callbacks that are called by AVAILABLE, except onAvailable.
+ *
+ * <p>AVAILABLE may call SUSPENDED, CAP_CHANGED, IP_CHANGED, LOCAL_NETWORK_INFO_CHANGED,
+ * and BLK_CHANGED, in this order.
+ */
+ private void addAvailableSubCallbacks(
+ @NonNull NetworkAgentInfo nai, @NonNull CallbackQueue queue) {
+ final boolean callSuspended =
+ !nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+ final boolean callLocalInfoChanged = nai.isLocalNetwork();
+
+ final int cbCount = 3 + (callSuspended ? 1 : 0) + (callLocalInfoChanged ? 1 : 0);
+ // Avoid unnecessary re-allocations by reserving enough space for all callbacks to add.
+ queue.ensureHasCapacity(cbCount);
+ final int netId = nai.network.netId;
+ if (callSuspended) {
+ queue.addCallback(netId, CALLBACK_SUSPENDED);
+ }
+ queue.addCallback(netId, CALLBACK_CAP_CHANGED);
+ queue.addCallback(netId, CALLBACK_IP_CHANGED);
+ if (callLocalInfoChanged) {
+ queue.addCallback(netId, CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+ }
+ queue.addCallback(netId, CALLBACK_BLK_CHANGED);
+ }
+
boolean hasHigherOrderThan(@NonNull final NetworkRequestInfo target) {
// Compare two preference orders.
return mPreferenceOrder < target.mPreferenceOrder;
@@ -8661,9 +8952,15 @@
@NonNull
final NetworkRequestInfo mDefaultRequest;
// Collection of NetworkRequestInfo's used for default networks.
+ // This set is read and iterated on multiple threads.
+ // Using CopyOnWriteArraySet since number of default network request is small (system default
+ // network request + per-app default network requests) and updated infrequently but read
+ // frequently.
@VisibleForTesting
@NonNull
- final ArraySet<NetworkRequestInfo> mDefaultNetworkRequests = new ArraySet<>();
+ final CopyOnWriteArraySet<NetworkRequestInfo> mDefaultNetworkRequests =
+ new CopyOnWriteArraySet<>();
+
private boolean isPerAppDefaultRequest(@NonNull final NetworkRequestInfo nri) {
return (mDefaultNetworkRequests.contains(nri) && mDefaultRequest != nri);
@@ -8843,11 +9140,7 @@
}
private void ensureRunningOnConnectivityServiceThread() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
}
@VisibleForTesting
@@ -9400,8 +9693,10 @@
* interfaces.
* Ingress discard rule is added to the address iff
* 1. The address is not a link local address
- * 2. The address is used by a single VPN interface and not used by any other
- * interfaces even non-VPN ones
+ * 2. The address is used by a single interface of VPN whose VPN type is not TYPE_VPN_LEGACY
+ * or TYPE_VPN_OEM and the address is not used by any other interfaces even non-VPN ones
+ * Ingress discard rule is not be added to TYPE_VPN_LEGACY or TYPE_VPN_OEM VPN since these VPNs
+ * might need to receive packet to VPN address via non-VPN interface.
* This method can be called during network disconnects, when nai has already been removed from
* mNetworkAgentInfos.
*
@@ -9436,7 +9731,10 @@
// for different network.
final Set<Pair<InetAddress, String>> ingressDiscardRules = new ArraySet<>();
for (final NetworkAgentInfo agent : nais) {
- if (!agent.isVPN() || agent.isDestroyed()) {
+ final int vpnType = getVpnType(agent);
+ if (!agent.isVPN() || agent.isDestroyed()
+ || vpnType == VpnManager.TYPE_VPN_LEGACY
+ || vpnType == VpnManager.TYPE_VPN_OEM) {
continue;
}
final LinkProperties agentLp = (nai == agent) ? lp : agent.linkProperties;
@@ -9987,8 +10285,7 @@
return stableRanges;
}
- private void maybeCloseSockets(NetworkAgentInfo nai, Set<UidRange> ranges,
- UidRangeParcel[] uidRangeParcels, int[] exemptUids) {
+ private void maybeCloseSockets(NetworkAgentInfo nai, Set<UidRange> ranges, int[] exemptUids) {
if (nai.isVPN() && !nai.networkAgentConfig.allowBypass) {
try {
if (mDeps.isAtLeastU()) {
@@ -9998,7 +10295,7 @@
}
mDeps.destroyLiveTcpSockets(UidRange.toIntRanges(ranges), exemptUidSet);
} else {
- mNetd.socketDestroy(uidRangeParcels, exemptUids);
+ mNetd.socketDestroy(toUidRangeStableParcels(ranges), exemptUids);
}
} catch (Exception e) {
loge("Exception in socket destroy: ", e);
@@ -10006,6 +10303,28 @@
}
}
+ private void modifyNetworkUidRanges(boolean add, NetworkAgentInfo nai, UidRangeParcel[] ranges,
+ int preference) throws RemoteException {
+ // UID ranges can be added or removed to a network that has already been destroyed (e.g., if
+ // the network disconnects, or a a multilayer request is filed after
+ // unregisterAfterReplacement is called).
+ if (nai.isDestroyed()) {
+ return;
+ }
+ final NativeUidRangeConfig config = new NativeUidRangeConfig(nai.network.netId,
+ ranges, preference);
+ if (add) {
+ mNetd.networkAddUidRangesParcel(config);
+ } else {
+ mNetd.networkRemoveUidRangesParcel(config);
+ }
+ }
+
+ private void modifyNetworkUidRanges(boolean add, NetworkAgentInfo nai, Set<UidRange> uidRanges,
+ int preference) throws RemoteException {
+ modifyNetworkUidRanges(add, nai, toUidRangeStableParcels(uidRanges), preference);
+ }
+
private void updateVpnUidRanges(boolean add, NetworkAgentInfo nai, Set<UidRange> uidRanges) {
int[] exemptUids = new int[2];
// TODO: Excluding VPN_UID is necessary in order to not to kill the TCP connection used
@@ -10013,24 +10332,17 @@
// starting a legacy VPN, and remove VPN_UID here. (b/176542831)
exemptUids[0] = VPN_UID;
exemptUids[1] = nai.networkCapabilities.getOwnerUid();
- UidRangeParcel[] ranges = toUidRangeStableParcels(uidRanges);
// Close sockets before modifying uid ranges so that RST packets can reach to the server.
- maybeCloseSockets(nai, uidRanges, ranges, exemptUids);
+ maybeCloseSockets(nai, uidRanges, exemptUids);
try {
- if (add) {
- mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
- nai.network.netId, ranges, PREFERENCE_ORDER_VPN));
- } else {
- mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
- nai.network.netId, ranges, PREFERENCE_ORDER_VPN));
- }
+ modifyNetworkUidRanges(add, nai, uidRanges, PREFERENCE_ORDER_VPN);
} catch (Exception e) {
loge("Exception while " + (add ? "adding" : "removing") + " uid ranges " + uidRanges +
" on netId " + nai.network.netId + ". " + e);
}
// Close sockets that established connection while requesting netd.
- maybeCloseSockets(nai, uidRanges, ranges, exemptUids);
+ maybeCloseSockets(nai, uidRanges, exemptUids);
}
private boolean isProxySetOnAnyDefaultNetwork() {
@@ -10144,16 +10456,12 @@
toAdd.removeAll(prevUids);
try {
if (!toAdd.isEmpty()) {
- mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
- nai.network.netId,
- intsToUidRangeStableParcels(toAdd),
- PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT));
+ modifyNetworkUidRanges(true /* add */, nai, intsToUidRangeStableParcels(toAdd),
+ PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT);
}
if (!toRemove.isEmpty()) {
- mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
- nai.network.netId,
- intsToUidRangeStableParcels(toRemove),
- PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT));
+ modifyNetworkUidRanges(false /* add */, nai, intsToUidRangeStableParcels(toRemove),
+ PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT);
}
} catch (ServiceSpecificException e) {
// Has the interface disappeared since the network was built ?
@@ -10277,6 +10585,11 @@
// are Type.LISTEN, but should not have NetworkCallbacks invoked.
return;
}
+ // Even if a callback ends up not being sent, it may affect other callbacks in the queue, so
+ // queue callbacks before checking the declared methods flags.
+ if (networkAgent != null && nri.maybeQueueCallback(networkAgent, notificationType)) {
+ return;
+ }
if (!nri.isCallbackOverridden(notificationType)) {
// No need to send the notification as the recipient method is not overridden
return;
@@ -10503,16 +10816,12 @@
+ " any applications to set as the default." + nri);
}
if (null != newDefaultNetwork) {
- mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
- newDefaultNetwork.network.getNetId(),
- toUidRangeStableParcels(nri.getUids()),
- nri.getPreferenceOrderForNetd()));
+ modifyNetworkUidRanges(true /* add */, newDefaultNetwork, nri.getUids(),
+ nri.getPreferenceOrderForNetd());
}
if (null != oldDefaultNetwork) {
- mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
- oldDefaultNetwork.network.getNetId(),
- toUidRangeStableParcels(nri.getUids()),
- nri.getPreferenceOrderForNetd()));
+ modifyNetworkUidRanges(false /* add */, oldDefaultNetwork, nri.getUids(),
+ nri.getPreferenceOrderForNetd());
}
} catch (RemoteException | ServiceSpecificException e) {
loge("Exception setting app default network", e);
@@ -12738,6 +13047,8 @@
// Key is netId. Value is configured idle timer information.
private final SparseArray<IdleTimerParams> mActiveIdleTimers = new SparseArray<>();
private final boolean mTrackMultiNetworkActivities;
+ private final int mDefaultCellularDataInactivityTimeout;
+ private final int mDefaultWifiDataInactivityTimeout;
// Store netIds of Wi-Fi networks whose idletimers report that they are active
private final Set<Integer> mActiveWifiNetworks = new ArraySet<>();
// Store netIds of cellular networks whose idletimers report that they are active
@@ -12754,18 +13065,18 @@
}
LegacyNetworkActivityTracker(@NonNull Context context, @NonNull INetd netd,
- @NonNull Handler handler, boolean trackMultiNetworkActivities) {
+ @NonNull Handler handler, boolean trackMultiNetworkActivities,
+ int defaultCellularDataInactivityTimeout, int defaultWifiDataInactivityTimeout) {
mContext = context;
mNetd = netd;
mHandler = handler;
mTrackMultiNetworkActivities = trackMultiNetworkActivities;
+ mDefaultCellularDataInactivityTimeout = defaultCellularDataInactivityTimeout;
+ mDefaultWifiDataInactivityTimeout = defaultWifiDataInactivityTimeout;
}
private void ensureRunningOnConnectivityServiceThread() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException("Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
}
/**
@@ -12961,13 +13272,13 @@
NetworkCapabilities.TRANSPORT_CELLULAR)) {
timeout = Settings.Global.getInt(mContext.getContentResolver(),
ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_MOBILE,
- 10);
+ mDefaultCellularDataInactivityTimeout);
type = NetworkCapabilities.TRANSPORT_CELLULAR;
} else if (networkAgent.networkCapabilities.hasTransport(
NetworkCapabilities.TRANSPORT_WIFI)) {
timeout = Settings.Global.getInt(mContext.getContentResolver(),
ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_WIFI,
- 15);
+ mDefaultWifiDataInactivityTimeout);
type = NetworkCapabilities.TRANSPORT_WIFI;
} else {
return false; // do not track any other networks
@@ -13091,6 +13402,12 @@
public void dump(IndentingPrintWriter pw) {
pw.print("mTrackMultiNetworkActivities="); pw.println(mTrackMultiNetworkActivities);
+
+ pw.print("mDefaultCellularDataInactivityTimeout=");
+ pw.println(mDefaultCellularDataInactivityTimeout);
+ pw.print("mDefaultWifiDataInactivityTimeout=");
+ pw.println(mDefaultWifiDataInactivityTimeout);
+
pw.print("mIsDefaultNetworkActive="); pw.println(mIsDefaultNetworkActive);
pw.print("mDefaultNetwork="); pw.println(mDefaultNetwork);
pw.println("Idle timers:");
@@ -14224,4 +14541,14 @@
}
return features;
}
+
+ @Override
+ public boolean isConnectivityServiceFeatureEnabledForTesting(String featureFlag) {
+ switch (featureFlag) {
+ case INGRESS_TO_VPN_ADDRESS_FILTERING:
+ return mIngressToVpnAddressFiltering;
+ default:
+ throw new IllegalArgumentException("Unknown flag: " + featureFlag);
+ }
+ }
}
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 31108fc..c7d96de 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -25,6 +25,7 @@
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
import android.annotation.IntDef;
@@ -440,7 +441,7 @@
*/
@Nullable
public AutomaticOnOffKeepalive getKeepaliveForBinder(@NonNull final IBinder token) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
return CollectionUtils.findFirst(mAutomaticOnOffKeepalives,
it -> it.mCallback.asBinder().equals(token));
@@ -580,7 +581,7 @@
}
private void cleanupAutoOnOffKeepalive(@NonNull final AutomaticOnOffKeepalive autoKi) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
mKeepaliveStatsTracker.onStopKeepalive(autoKi.getNetwork(), autoKi.mKi.getSlot());
autoKi.close();
if (null != autoKi.mAlarmListener) mAlarmManager.cancel(autoKi.mAlarmListener);
@@ -693,7 +694,7 @@
* This should be only be called in ConnectivityService handler thread.
*/
public void dump(IndentingPrintWriter pw) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
mKeepaliveTracker.dump(pw);
// Reading DeviceConfig will check if the calling uid and calling package name are the same.
// Clear calling identity to align the calling uid and package so that it won't fail if cts
@@ -771,7 +772,7 @@
private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
int networkMask)
throws ErrnoException, InterruptedIOException {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
// Build SocketDiag messages and cache it.
if (mSockDiagMsg.get(family) == null) {
mSockDiagMsg.put(family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family));
@@ -843,13 +844,6 @@
return mark;
}
- private void ensureRunningOnHandlerThread() {
- if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on handler thread: " + Thread.currentThread().getName());
- }
- }
-
private long getTcpPollingIntervalMs(@NonNull AutomaticOnOffKeepalive ki) {
final boolean useLowTimer = mTestLowTcpPollingTimerUntilMs > System.currentTimeMillis();
// Adjust the polling interval to be smaller than the keepalive delay to preserve
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index f5fa4fb..14a935f 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -19,6 +19,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import static com.android.server.connectivity.ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK;
import android.annotation.NonNull;
@@ -168,7 +169,7 @@
private void simConfigChanged() {
// If mRequestRestrictedWifiEnabled is false, constructor calls simConfigChanged
if (mRequestRestrictedWifiEnabled) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mHandler);
}
synchronized (mLock) {
unregisterCarrierPrivilegesListeners();
@@ -212,7 +213,7 @@
public void onCarrierPrivilegesChanged(
@NonNull List<String> privilegedPackageNames,
@NonNull int[] privilegedUids) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mHandler);
if (mUseCallbacksForServiceChanged) return;
// Re-trigger the synchronous check (which is also very cheap due
// to caching in CarrierPrivilegesTracker). This allows consistency
@@ -223,7 +224,7 @@
@Override
public void onCarrierServiceChanged(@Nullable final String carrierServicePackageName,
final int carrierServiceUid) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mHandler);
if (!mUseCallbacksForServiceChanged) {
// Re-trigger the synchronous check (which is also very cheap due
// to caching in CarrierPrivilegesTracker). This allows consistency
@@ -465,13 +466,6 @@
}
}
- private void ensureRunningOnHandlerThread() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on handler thread: " + Thread.currentThread().getName());
- }
- }
-
public void dump(IndentingPrintWriter pw) {
pw.println("CarrierPrivilegeAuthenticator:");
pw.println("mRequestRestrictedWifiEnabled = " + mRequestRestrictedWifiEnabled);
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index df87316..93335f1 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -44,6 +44,11 @@
public static final String BACKGROUND_FIREWALL_CHAIN = "background_firewall_chain";
+ public static final String CELLULAR_DATA_INACTIVITY_TIMEOUT =
+ "cellular_data_inactivity_timeout";
+
+ public static final String WIFI_DATA_INACTIVITY_TIMEOUT = "wifi_data_inactivity_timeout";
+
public static final String DELAY_DESTROY_SOCKETS = "delay_destroy_sockets";
public static final String USE_DECLARED_METHODS_FOR_CALLBACKS =
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index 917ad4d..7a008c6 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -43,7 +43,7 @@
private static final String TAG = ConnectivityNativeService.class.getSimpleName();
private static final String BLOCKED_PORTS_MAP_PATH =
- "/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
+ "/sys/fs/bpf/netd_shared/map_netd_blocked_ports_map";
private final Context mContext;
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index ac02229..c940eec 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -29,6 +29,7 @@
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -41,6 +42,7 @@
import android.net.NetworkCapabilities;
import android.net.ResolverParamsParcel;
import android.net.Uri;
+import android.net.resolv.aidl.DohParamsParcel;
import android.net.shared.PrivateDnsConfig;
import android.os.Binder;
import android.os.RemoteException;
@@ -52,16 +54,17 @@
import android.util.Pair;
import java.net.InetAddress;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
/**
* Encapsulate the management of DNS settings for networks.
@@ -382,15 +385,14 @@
paramsParcel.domains = getDomainStrings(lp.getDomains());
paramsParcel.tlsName = strictMode ? privateDnsCfg.hostname : "";
paramsParcel.tlsServers =
- strictMode ? makeStrings(
- Arrays.stream(privateDnsCfg.ips)
- .filter((ip) -> lp.isReachable(ip))
- .collect(Collectors.toList()))
+ strictMode ? makeStrings(getReachableAddressList(privateDnsCfg.ips, lp))
: useTls ? paramsParcel.servers // Opportunistic
: new String[0]; // Off
paramsParcel.transportTypes = nc.getTransportTypes();
paramsParcel.meteredNetwork = nc.isMetered();
paramsParcel.interfaceNames = lp.getAllInterfaceNames().toArray(new String[0]);
+ paramsParcel.dohParams = makeDohParamsParcel(privateDnsCfg, lp);
+
// Prepare to track the validation status of the DNS servers in the
// resolver config when private DNS is in opportunistic or strict mode.
if (useTls) {
@@ -403,21 +405,11 @@
mPrivateDnsValidationMap.remove(netId);
}
- Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
- + "%d, %d, %s, %s, %s, %b, %s)", paramsParcel.netId,
- Arrays.toString(paramsParcel.servers), Arrays.toString(paramsParcel.domains),
- paramsParcel.sampleValiditySeconds, paramsParcel.successThreshold,
- paramsParcel.minSamples, paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
- paramsParcel.retryCount, paramsParcel.tlsName,
- Arrays.toString(paramsParcel.tlsServers),
- Arrays.toString(paramsParcel.transportTypes), paramsParcel.meteredNetwork,
- Arrays.toString(paramsParcel.interfaceNames)));
-
+ Log.d(TAG, "sendDnsConfigurationForNetwork(" + paramsParcel + ")");
try {
mDnsResolver.setResolverConfiguration(paramsParcel);
} catch (RemoteException | ServiceSpecificException e) {
Log.e(TAG, "Error setting DNS configuration: " + e);
- return;
}
}
@@ -498,4 +490,29 @@
private static String[] getDomainStrings(String domains) {
return (TextUtils.isEmpty(domains)) ? new String[0] : domains.split(" ");
}
+
+ @NonNull
+ private List<InetAddress> getReachableAddressList(@NonNull InetAddress[] ips,
+ @NonNull LinkProperties lp) {
+ final ArrayList<InetAddress> out = new ArrayList<InetAddress>(Arrays.asList(ips));
+ out.removeIf(ip -> !lp.isReachable(ip));
+ return out;
+ }
+
+ @Nullable
+ private DohParamsParcel makeDohParamsParcel(@NonNull PrivateDnsConfig cfg,
+ @NonNull LinkProperties lp) {
+ if (!cfg.ddrEnabled) {
+ return null;
+ }
+ if (cfg.mode == PRIVATE_DNS_MODE_OFF) {
+ return new DohParamsParcel.Builder().build();
+ }
+ return new DohParamsParcel.Builder()
+ .setName(cfg.dohName)
+ .setIps(makeStrings(getReachableAddressList(cfg.dohIps, lp)))
+ .setDohpath(cfg.dohPath)
+ .setPort(cfg.dohPort)
+ .build();
+ }
}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
index 7b11eda..a9100ac 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyValue.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -55,13 +55,17 @@
@Field(order = 7, type = Type.S8)
public final byte dscp;
- @Field(order = 8, type = Type.U8, padding = 3)
- public final short mask;
+ @Field(order = 8, type = Type.Bool)
+ public final boolean match_src_ip;
- private static final int SRC_IP_MASK = 0x1;
- private static final int DST_IP_MASK = 0x02;
- private static final int SRC_PORT_MASK = 0x4;
- private static final int PROTO_MASK = 0x8;
+ @Field(order = 9, type = Type.Bool)
+ public final boolean match_dst_ip;
+
+ @Field(order = 10, type = Type.Bool)
+ public final boolean match_src_port;
+
+ @Field(order = 11, type = Type.Bool)
+ public final boolean match_proto;
private boolean ipEmpty(final byte[] ip) {
for (int i = 0; i < ip.length; i++) {
@@ -98,24 +102,6 @@
private static final byte[] EMPTY_ADDRESS_FIELD =
InetAddress.parseNumericAddress("::").getAddress();
- private short makeMask(final byte[] src46, final byte[] dst46, final int srcPort,
- final int dstPortStart, final short proto, final byte dscp) {
- short mask = 0;
- if (src46 != EMPTY_ADDRESS_FIELD) {
- mask |= SRC_IP_MASK;
- }
- if (dst46 != EMPTY_ADDRESS_FIELD) {
- mask |= DST_IP_MASK;
- }
- if (srcPort != -1) {
- mask |= SRC_PORT_MASK;
- }
- if (proto != -1) {
- mask |= PROTO_MASK;
- }
- return mask;
- }
-
private DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int ifIndex,
final int srcPort, final int dstPortStart, final int dstPortEnd, final short proto,
final byte dscp) {
@@ -131,9 +117,10 @@
this.proto = proto != -1 ? proto : 0;
this.dscp = dscp;
- // Use member variables for IP since byte[] is needed and api variables for everything else
- // so -1 is passed into mask if parameter is not present.
- this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp);
+ this.match_src_ip = (src46 != null);
+ this.match_dst_ip = (dst46 != null);
+ this.match_src_port = (srcPort != -1);
+ this.match_proto = (proto != -1);
}
public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int ifIndex,
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 21dbb45..8acd1c8 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -18,6 +18,8 @@
import static android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
+
import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -466,7 +468,7 @@
int intervalSeconds,
int appUid,
boolean isAutoKeepalive) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
if (!isEnabled()) return;
final int keepaliveId = getKeepaliveId(network, slot);
if (keepaliveId == INVALID_KEEPALIVE_ID) return;
@@ -538,21 +540,21 @@
/** Inform the KeepaliveStatsTracker a keepalive has just been paused. */
public void onPauseKeepalive(@NonNull Network network, int slot) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
if (!isEnabled()) return;
onKeepaliveActive(network, slot, /* keepaliveActive= */ false);
}
/** Inform the KeepaliveStatsTracker a keepalive has just been resumed. */
public void onResumeKeepalive(@NonNull Network network, int slot) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
if (!isEnabled()) return;
onKeepaliveActive(network, slot, /* keepaliveActive= */ true);
}
/** Inform the KeepaliveStatsTracker a keepalive has just been stopped. */
public void onStopKeepalive(@NonNull Network network, int slot) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
if (!isEnabled()) return;
final int keepaliveId = getKeepaliveId(network, slot);
@@ -615,7 +617,7 @@
*/
@VisibleForTesting
public @NonNull DailykeepaliveInfoReported buildKeepaliveMetrics() {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
final long timeNow = mDependencies.getElapsedRealtime();
return buildKeepaliveMetrics(timeNow);
}
@@ -673,7 +675,7 @@
*/
@VisibleForTesting
public @NonNull DailykeepaliveInfoReported buildAndResetMetrics() {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
final long timeNow = mDependencies.getElapsedRealtime();
final DailykeepaliveInfoReported metrics = buildKeepaliveMetrics(timeNow);
@@ -750,7 +752,7 @@
/** Writes the stored metrics to ConnectivityStatsLog and resets. */
public void writeAndResetMetrics() {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
// Keepalive stats use repeated atoms, which are only supported on T+. If written to statsd
// on S- they will bootloop the system, so they must not be sent on S-. See b/289471411.
if (!SdkLevel.isAtLeastT()) {
@@ -771,17 +773,10 @@
/** Dump KeepaliveStatsTracker state. */
public void dump(IndentingPrintWriter pw) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
pw.println("KeepaliveStatsTracker enabled: " + isEnabled());
pw.increaseIndent();
pw.println(buildKeepaliveMetrics().toString());
pw.decreaseIndent();
}
-
- private void ensureRunningOnHandlerThread() {
- if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on handler thread: " + Thread.currentThread().getName());
- }
- }
}
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index a979681..37aef22 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -20,6 +20,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static com.android.net.module.util.CollectionUtils.contains;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -500,7 +501,7 @@
// Once this code is converted to StateMachine, it will be possible to use deferMessage to
// ensure it stays in STARTING state until the interfaceLinkStateChanged notification fires,
// and possibly use a timeout (or provide some guarantees at the lower layer) to address #1.
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mNetwork.handler());
if (!isStarting() || !up || !Objects.equals(mIface, iface)) {
return;
}
@@ -524,7 +525,7 @@
* Must be called on the handler thread.
*/
public void handleInterfaceRemoved(String iface) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mNetwork.handler());
if (!Objects.equals(mIface, iface)) {
return;
}
@@ -546,7 +547,7 @@
@Nullable
public Inet6Address translateV4toV6(@NonNull Inet4Address addr) {
// Variables in Nat464Xlat should only be accessed from handler thread.
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mNetwork.handler());
if (!isStarted()) return null;
return convertv4ToClatv6(mNat64PrefixInUse, addr);
@@ -574,7 +575,7 @@
@Nullable
public Inet6Address getClatv6SrcAddress() {
// Variables in Nat464Xlat should only be accessed from handler thread.
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mNetwork.handler());
return mIPv6Address;
}
@@ -585,7 +586,7 @@
@Nullable
public Inet4Address getClatv4SrcAddress() {
// Variables in Nat464Xlat should only be accessed from handler thread.
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mNetwork.handler());
if (!isStarted()) return null;
final LinkAddress v4Addr = getLinkAddress(mIface);
@@ -594,13 +595,6 @@
return (Inet4Address) v4Addr.getAddress();
}
- private void ensureRunningOnHandlerThread() {
- if (mNetwork.handler().getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on handler thread: " + Thread.currentThread().getName());
- }
- }
-
/**
* Dump the NAT64 xlat information.
*
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 76993a6..94b655f 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -68,6 +68,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.WakeupMessage;
+import com.android.net.module.util.HandlerUtils;
import com.android.server.ConnectivityService;
import java.io.PrintWriter;
@@ -1138,11 +1139,7 @@
* already present.
*/
public boolean addRequest(NetworkRequest networkRequest) {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
NetworkRequest existing = mNetworkRequests.get(networkRequest.requestId);
if (existing == networkRequest) return false;
if (existing != null) {
@@ -1161,11 +1158,7 @@
* Remove the specified request from this network.
*/
public void removeRequest(int requestId) {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
NetworkRequest existing = mNetworkRequests.get(requestId);
if (existing == null) return;
updateRequestCounts(REMOVE, existing);
@@ -1187,11 +1180,7 @@
* network.
*/
public NetworkRequest requestAt(int index) {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
return mNetworkRequests.valueAt(index);
}
@@ -1222,11 +1211,7 @@
* Returns the number of requests of any type currently satisfied by this network.
*/
public int numNetworkRequests() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
return mNetworkRequests.size();
}
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
index 3db37e5..8a2e72c 100644
--- a/service/src/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -567,7 +567,9 @@
@Override
public void close() {
- IoUtils.closeQuietly(mFileDescriptor);
+ if (mFileDescriptor != null) {
+ IoUtils.closeQuietly(mFileDescriptor);
+ }
}
}
@@ -611,6 +613,7 @@
setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0);
} catch (ErrnoException | IOException e) {
mMeasurement.recordFailure(e.toString());
+ close();
return;
}
mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}";
@@ -695,6 +698,7 @@
NetworkConstants.DNS_SERVER_PORT);
} catch (ErrnoException | IOException e) {
mMeasurement.recordFailure(e.toString());
+ close();
return;
}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 14bd5df..b4a3b8a 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -32,6 +32,8 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+// This library shouldn't be used anymore (no class should be added), and per-user libraries like
+// net-utils-service-connectivity or net-utils-framework-wifi should be used instead.
java_library {
name: "net-utils-device-common",
srcs: [
@@ -58,13 +60,8 @@
"//apex_available:platform",
],
visibility: [
- "//frameworks/base/packages/Tethering",
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/Connectivity/framework:__subpackages__",
- "//frameworks/opt/net/ike",
- "//frameworks/opt/net/wifi/service",
- "//packages/modules/Wifi/service",
- "//frameworks/opt/net/telephony",
"//packages/modules/NetworkStack:__subpackages__",
"//packages/modules/CaptivePortalLogin",
],
@@ -75,7 +72,7 @@
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
],
lint: {
@@ -269,7 +266,7 @@
],
libs: [
"framework-annotations-lib",
- "framework-connectivity",
+ "framework-connectivity.stubs.module_lib",
],
static_libs: [
"net-utils-device-common",
@@ -347,7 +344,7 @@
min_sdk_version: "30",
libs: [
"framework-annotations-lib",
- "framework-connectivity",
+ "framework-connectivity.stubs.module_lib",
"modules-utils-build_system",
],
// TODO: remove "apex_available:platform".
@@ -440,9 +437,11 @@
sdk_version: "core_platform",
srcs: [
"device/com/android/net/module/util/FdEventsReader.java",
+ "device/com/android/net/module/util/HandlerUtils.java",
"device/com/android/net/module/util/SharedLog.java",
"framework/com/android/net/module/util/ByteUtils.java",
"framework/com/android/net/module/util/CollectionUtils.java",
+ "framework/com/android/net/module/util/DnsUtils.java",
"framework/com/android/net/module/util/HexDump.java",
"framework/com/android/net/module/util/LinkPropertiesUtils.java",
],
@@ -454,6 +453,7 @@
visibility: ["//packages/modules/Connectivity/service-t"],
}
+// net-utils-framework-connectivity is only for framework-connectivity.
java_library {
name: "net-utils-framework-connectivity",
srcs: [
@@ -466,13 +466,12 @@
"//apex_available:platform",
],
visibility: [
- "//packages/modules/Connectivity:__subpackages__",
- "//packages/modules/NetworkStack:__subpackages__",
+ "//packages/modules/Connectivity/framework",
],
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
],
lint: {
@@ -485,16 +484,14 @@
name: "net-utils-non-bootclasspath-defaults",
sdk_version: "module_current",
min_sdk_version: "30",
- jarjar_rules: "jarjar-rules-shared.txt",
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-configinfrastructure",
- "framework-connectivity",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
"framework-connectivity-t.stubs.module_lib",
"framework-location.stubs.module_lib",
- "framework-tethering",
+ "framework-tethering.stubs.module_lib",
"unsupportedappusage",
],
static_libs: [
@@ -507,9 +504,6 @@
"com.android.tethering",
"//apex_available:platform",
],
- visibility: [
- "//packages/modules/Connectivity:__subpackages__",
- ],
defaults_visibility: [
"//visibility:private",
],
@@ -519,6 +513,7 @@
},
}
+// net-utils-service-connectivity is only for service-connectivity.
java_library {
name: "net-utils-service-connectivity",
srcs: [
@@ -529,17 +524,42 @@
],
libs: [
"net-utils-framework-connectivity",
+ "framework-connectivity.impl",
+ "framework-tethering.impl",
],
defaults: ["net-utils-non-bootclasspath-defaults"],
+ jarjar_rules: "jarjar-rules-shared.txt",
+ visibility: [
+ "//packages/modules/Connectivity/service",
+ "//packages/modules/Connectivity/staticlibs/tests/unit",
+ ],
}
+// net-utils-connectivity-apks is only for NetworkStack, CaptivePortalLogin and
+// Tethering.apk
+// It includes all the static libraries in this directory, which is safe because
+// these APKs use R8 to strip out unused code, and they do not depend on
+// bootclasspath jars that may have duplicate copies of the included classes
+// with the same jarjaring.
+// Tethering.apk does depend on a bootclasspath jar (framework-tethering.jar),
+// however it does not use any of the static libraries. If it did, Tethering.apk
+// would need to use another variant that excludes classes that are already
+// included in framework-tethering.jar (similarly to how framework-connectivity
+// and service-connectivity do it). Otherwise, static libs included in
+// framework-tethering and Tethering.apk and jarjared the same way would
+// conflict.
java_library {
- name: "net-utils-tethering",
+ name: "net-utils-connectivity-apks",
srcs: [
":net-utils-all-srcs",
":framework-connectivity-shared-srcs",
],
defaults: ["net-utils-non-bootclasspath-defaults"],
+ jarjar_rules: "jarjar-rules-shared.txt",
+ visibility: [
+ "//packages/modules/CaptivePortalLogin:__subpackages__",
+ "//packages/modules/Connectivity/Tethering",
+ ],
}
aidl_interface {
@@ -554,6 +574,8 @@
min_sdk_version: "30",
apex_available: [
"com.android.tethering",
+ "com.android.wifi",
+ "//apex_available:platform",
],
},
cpp: {
@@ -591,8 +613,7 @@
],
}
-// Use a filegroup and not a library for wifi sources, as this needs corresponding jar-jar
-// rules on the wifi side.
+// Filegroup to build lib used by Wifi framework
// Any class here *must* have a corresponding jarjar rule in the wifi build rules.
filegroup {
name: "net-utils-framework-wifi-common-srcs",
@@ -604,26 +625,32 @@
"framework/com/android/net/module/util/NetUtils.java",
],
path: "framework",
- visibility: [
- "//frameworks/base",
- ],
+ visibility: ["//visibility:private"],
}
-// Use a filegroup and not a library for wifi sources, as this needs corresponding jar-jar
-// rules on the wifi side.
-// Any class here *must* have a corresponding jarjar rule in the wifi build rules.
+// Filegroup to build lib used by IPsec/IKE framework
+// Any class here *must* have a corresponding jarjar rule in the IPsec build rules.
filegroup {
- name: "net-utils-wifi-service-common-srcs",
+ name: "net-utils-framework-ipsec-common-srcs",
srcs: [
- "device/android/net/NetworkFactory.java",
- "device/android/net/NetworkFactoryImpl.java",
- "device/android/net/NetworkFactoryLegacyImpl.java",
- "device/android/net/NetworkFactoryShim.java",
+ "framework/com/android/net/module/util/HexDump.java",
+ ],
+ path: "framework",
+ visibility: ["//visibility:private"],
+}
+
+java_library {
+ name: "net-utils-framework-ipsec",
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ srcs: [":net-utils-framework-ipsec-common-srcs"],
+ libs: [
+ "androidx.annotation_annotation",
],
visibility: [
- "//frameworks/opt/net/wifi/service",
- "//packages/modules/Wifi/service",
+ "//packages/modules/IPsec",
],
+ apex_available: ["com.android.ipsec"],
}
// Use a file group containing classes necessary for framework-connectivity. The file group should
@@ -646,6 +673,8 @@
visibility: ["//visibility:private"],
}
+// Sources outside of com.android.net.module.util should not be added because many modules depend on
+// them and need jarjar rules
filegroup {
name: "net-utils-all-srcs",
srcs: [
@@ -663,3 +692,69 @@
path: "device",
visibility: ["//visibility:private"],
}
+
+java_library {
+ name: "net-utils-service-wifi",
+ srcs: [
+ ":net-utils-all-srcs",
+ ],
+ exclude_srcs: [":net-utils-framework-wifi-common-srcs"],
+ libs: [
+ "net-utils-framework-wifi",
+ ],
+ defaults: ["net-utils-non-bootclasspath-defaults"],
+
+ visibility: [
+ "//packages/modules/Wifi/service",
+ ],
+ apex_available: [
+ "com.android.wifi",
+ ],
+}
+
+java_library {
+ name: "net-utils-framework-wifi",
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ srcs: [":net-utils-framework-wifi-common-srcs"],
+ libs: [
+ "framework-annotations-lib",
+ "framework-connectivity.stubs.module_lib",
+ "unsupportedappusage",
+ ],
+ visibility: [
+ "//packages/modules/Wifi/framework",
+ ],
+ apex_available: ["com.android.wifi"],
+}
+
+genrule {
+ name: "statslog-framework-connectivity-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module connectivity --javaPackage com.android.net.module.util --javaClass FrameworkConnectivityStatsLog",
+ out: ["com/android/net/module/util/FrameworkConnectivityStatsLog.java"],
+}
+
+java_library {
+ name: "net-utils-service-vcn",
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ srcs: [
+ "device/com/android/net/module/util/HandlerUtils.java",
+ ],
+ libs: [
+ "framework-annotations-lib",
+ ],
+ visibility: [
+ // TODO: b/374174952 Remove it when VCN modularization is released
+ "//frameworks/base/packages/Vcn/service-b",
+
+ "//packages/modules/Connectivity/service-b",
+ ],
+ apex_available: [
+ // TODO: b/374174952 Remove it when VCN modularization is released
+ "//apex_available:platform",
+
+ "com.android.tethering",
+ ],
+}
diff --git a/staticlibs/client-libs/tests/unit/Android.bp b/staticlibs/client-libs/tests/unit/Android.bp
index 7aafd69..79234f5 100644
--- a/staticlibs/client-libs/tests/unit/Android.bp
+++ b/staticlibs/client-libs/tests/unit/Android.bp
@@ -17,8 +17,8 @@
"netd-client",
],
libs: [
- "android.test.runner",
- "android.test.base",
+ "android.test.runner.stubs.system",
+ "android.test.base.stubs.system",
],
visibility: [
// Visible for Tethering and NetworkStack integration test and link NetdStaticLibTestsLib
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 0426ace..04ce2fa 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -22,6 +22,7 @@
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.net.module.util.FeatureVersions.CONNECTIVITY_MODULE_ID;
+import static com.android.net.module.util.FeatureVersions.DNS_RESOLVER_MODULE_ID;
import static com.android.net.module.util.FeatureVersions.MODULE_MASK;
import static com.android.net.module.util.FeatureVersions.NETWORK_STACK_MODULE_ID;
import static com.android.net.module.util.FeatureVersions.VERSION_MASK;
@@ -68,7 +69,8 @@
@VisibleForTesting
public static void resetPackageVersionCacheForTest() {
sPackageVersion = -1;
- sModuleVersion = -1;
+ sTetheringModuleVersion = -1;
+ sResolvModuleVersion = -1;
sNetworkStackModuleVersion = -1;
}
@@ -243,23 +245,23 @@
}
}
- // Guess the tethering module name based on the package prefix of the connectivity resources
- // Take the resource package name, cut it before "connectivity" and append "tethering".
+ // Guess an APEX module name based on the package prefix of the connectivity resources
+ // Take the resource package name, cut it before "connectivity" and append the module name.
// Then resolve that package version number with packageManager.
- // If that fails retry by appending "go.tethering" instead
- private static long resolveTetheringModuleVersion(@NonNull Context context)
+ // If that fails retry by appending "go.<moduleName>" instead.
+ private static long resolveApexModuleVersion(@NonNull Context context, String moduleName)
throws PackageManager.NameNotFoundException {
final String pkgPrefix = resolvePkgPrefix(context);
final PackageManager packageManager = context.getPackageManager();
try {
- return packageManager.getPackageInfo(pkgPrefix + "tethering",
+ return packageManager.getPackageInfo(pkgPrefix + moduleName,
PackageManager.MATCH_APEX).getLongVersionCode();
} catch (PackageManager.NameNotFoundException e) {
Log.d(TAG, "Device is using go modules");
// fall through
}
- return packageManager.getPackageInfo(pkgPrefix + "go.tethering",
+ return packageManager.getPackageInfo(pkgPrefix + "go." + moduleName,
PackageManager.MATCH_APEX).getLongVersionCode();
}
@@ -274,19 +276,35 @@
return connResourcesPackage.substring(0, pkgPrefixLen);
}
- private static volatile long sModuleVersion = -1;
+ private static volatile long sTetheringModuleVersion = -1;
+
private static long getTetheringModuleVersion(@NonNull Context context) {
- if (sModuleVersion >= 0) return sModuleVersion;
+ if (sTetheringModuleVersion >= 0) return sTetheringModuleVersion;
try {
- sModuleVersion = resolveTetheringModuleVersion(context);
+ sTetheringModuleVersion = resolveApexModuleVersion(context, "tethering");
} catch (PackageManager.NameNotFoundException e) {
// It's expected to fail tethering module version resolution on the devices with
// flattened apex
Log.e(TAG, "Failed to resolve tethering module version: " + e);
return DEFAULT_PACKAGE_VERSION;
}
- return sModuleVersion;
+ return sTetheringModuleVersion;
+ }
+
+ private static volatile long sResolvModuleVersion = -1;
+ private static long getResolvModuleVersion(@NonNull Context context) {
+ if (sResolvModuleVersion >= 0) return sResolvModuleVersion;
+
+ try {
+ sResolvModuleVersion = resolveApexModuleVersion(context, "resolv");
+ } catch (PackageManager.NameNotFoundException e) {
+ // It's expected to fail resolv module version resolution on the devices with
+ // flattened apex
+ Log.e(TAG, "Failed to resolve resolv module version: " + e);
+ return DEFAULT_PACKAGE_VERSION;
+ }
+ return sResolvModuleVersion;
}
private static volatile long sNetworkStackModuleVersion = -1;
@@ -342,6 +360,8 @@
moduleVersion = getTetheringModuleVersion(context);
} else if (moduleId == NETWORK_STACK_MODULE_ID) {
moduleVersion = getNetworkStackModuleVersion(context);
+ } else if (moduleId == DNS_RESOLVER_MODULE_ID) {
+ moduleVersion = getResolvModuleVersion(context);
} else {
throw new IllegalArgumentException("Unknown module " + moduleId);
}
diff --git a/staticlibs/device/com/android/net/module/util/FeatureVersions.java b/staticlibs/device/com/android/net/module/util/FeatureVersions.java
index d5f8124..d0cf3fd 100644
--- a/staticlibs/device/com/android/net/module/util/FeatureVersions.java
+++ b/staticlibs/device/com/android/net/module/util/FeatureVersions.java
@@ -37,6 +37,7 @@
public static final long VERSION_MASK = 0x00F_FFFF_FFFFL;
public static final long CONNECTIVITY_MODULE_ID = 0x01L << MODULE_SHIFT;
public static final long NETWORK_STACK_MODULE_ID = 0x02L << MODULE_SHIFT;
+ public static final long DNS_RESOLVER_MODULE_ID = 0x03L << MODULE_SHIFT;
// CLAT_ADDRESS_TRANSLATE is a feature of the network stack, which doesn't throw when system
// try to add a NAT-T keepalive packet filter with v6 address, introduced in version
// M-2023-Sept on July 3rd, 2023.
@@ -48,4 +49,11 @@
// by BPF for the given uid and conditions, introduced in version M-2024-Feb on Nov 6, 2023.
public static final long FEATURE_IS_UID_NETWORKING_BLOCKED =
CONNECTIVITY_MODULE_ID + 34_14_00_000L;
+
+ // DDR is a feature implemented across NetworkStack, ConnectivityService and DnsResolver.
+ // The flag that enables this feature is in NetworkStack.
+ public static final long FEATURE_DDR_IN_CONNECTIVITY =
+ CONNECTIVITY_MODULE_ID + 35_11_00_000L;
+ public static final long FEATURE_DDR_IN_DNSRESOLVER =
+ DNS_RESOLVER_MODULE_ID + 35_11_00_000L;
}
diff --git a/staticlibs/device/com/android/net/module/util/GrowingIntArray.java b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
index 4a81c10..d47738b 100644
--- a/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
+++ b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
@@ -168,7 +168,7 @@
* stop using this instance of {@link GrowingIntArray} if they use the array returned by this
* method.
*/
- public int[] getShrinkedBackingArray() {
+ public int[] getMinimizedBackingArray() {
shrinkToLength();
return mValues;
}
diff --git a/staticlibs/device/com/android/net/module/util/HandlerUtils.java b/staticlibs/device/com/android/net/module/util/HandlerUtils.java
index c620368..991df8f 100644
--- a/staticlibs/device/com/android/net/module/util/HandlerUtils.java
+++ b/staticlibs/device/com/android/net/module/util/HandlerUtils.java
@@ -102,4 +102,37 @@
if (e != null) throw e;
return true;
}
+
+ /**
+ * Ensures that the current running thread is the same as the thread associated with the given
+ * handler.
+ *
+ * @param handler The handler whose thread to compare.
+ * @throws IllegalStateException if the thread associated with the given handler is not the same
+ * as the current running thread.
+ * @hide
+ */
+ public static void ensureRunningOnHandlerThread(@NonNull Handler handler) {
+ if (!isRunningOnHandlerThread(handler)) {
+ throw new IllegalStateException(
+ "Not running on Handler thread: " + Thread.currentThread().getName());
+ }
+ }
+
+ /**
+ * Checks if the current running thread is the same as the thread associated with the given
+ * handler.
+ *
+ * @param handler The handler whose thread to compare.
+ * @return {@code true} if the thread associated with the given handler is the same as the
+ * current running thread, {@code false} otherwise.
+ *
+ * @hide
+ */
+ public static boolean isRunningOnHandlerThread(@NonNull Handler handler) {
+ if (handler.getLooper().getThread() == Thread.currentThread()) {
+ return true;
+ }
+ return false;
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/IIpv4PrefixRequest.aidl b/staticlibs/device/com/android/net/module/util/IIpv4PrefixRequest.aidl
new file mode 100644
index 0000000..cc1c19c
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/IIpv4PrefixRequest.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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.net.module.util;
+
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+
+/** @hide */
+// TODO: b/350630377 - This @Descriptor annotation workaround is to prevent the class from being
+// jarjared which changes the DESCRIPTOR and casues "java.lang.SecurityException: Binder invocation
+// to an incorrect interface" when calling the IPC.
+@Descriptor("value=no.jarjar.com.android.net.module.util.IIpv4PrefixRequest")
+interface IIpv4PrefixRequest {
+ void onIpv4PrefixConflict(in IpPrefix ipPrefix);
+}
diff --git a/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl b/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
index 72a4a94..7688e6a 100644
--- a/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
+++ b/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
@@ -16,8 +16,14 @@
package com.android.net.module.util;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.RouteInfo;
+import com.android.net.module.util.IIpv4PrefixRequest;
+
/** @hide */
// TODO: b/350630377 - This @Descriptor annotation workaround is to prevent the DESCRIPTOR from
// being jarjared which changes the DESCRIPTOR and casues "java.lang.SecurityException: Binder
@@ -96,4 +102,41 @@
* cause of the failure.
*/
void removeInterfaceForward(in String fromIface, in String toIface);
+
+ /** Update the prefix of an upstream. */
+ void updateUpstreamPrefix(in @nullable LinkProperties lp,
+ in @nullable NetworkCapabilities nc,
+ in Network network);
+
+ /** Remove the upstream prefix of the given {@link Network}. */
+ void removeUpstreamPrefix(in Network network);
+
+ /** Remove the deprecated upstream networks if any. */
+ void maybeRemoveDeprecatedUpstreams();
+
+ /**
+ * Request an IPv4 address for the downstream. Return the last time used address for the
+ * provided (interfaceType, scope) pair if possible.
+ *
+ * @param interfaceType the Tethering type (see TetheringManager#TETHERING_*).
+ * @param scope CONNECTIVITY_SCOPE_GLOBAL or CONNECTIVITY_SCOPE_LOCAL
+ * @param request a {@link IIpv4PrefixRequest} to report conflicts
+ * @return an IPv4 address allocated for the downstream, could be null
+ */
+ @nullable
+ LinkAddress requestStickyDownstreamAddress(
+ in int interfaceType,
+ in int scope,
+ in IIpv4PrefixRequest request);
+ /**
+ * Request an IPv4 address for the downstream.
+ *
+ * @param request a {@link IIpv4PrefixRequest} to report conflicts
+ * @return an IPv4 address allocated for the downstream, could be null
+ */
+ @nullable
+ LinkAddress requestDownstreamAddress(in IIpv4PrefixRequest request);
+
+ /** Release the IPv4 address allocated for the downstream. */
+ void releaseDownstream(in IIpv4PrefixRequest request);
}
diff --git a/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java b/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
new file mode 100644
index 0000000..bb95585
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.net.module.util;
+
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+
+import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
+import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTH;
+
+import static java.util.Arrays.asList;
+
+import android.content.Context;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.Supplier;
+
+/**
+ * This class coordinate IP addresses conflict problem.
+ *
+ * Tethering downstream IP addresses may conflict with network assigned addresses. This
+ * coordinator is responsible for recording all of network assigned addresses and dispatched
+ * free address to downstream interfaces.
+ *
+ * This class is not thread-safe.
+ * @hide
+ */
+public class PrivateAddressCoordinator {
+ // WARNING: Keep in sync with chooseDownstreamAddress
+ public static final int PREFIX_LENGTH = 24;
+
+ public static final String TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION =
+ "tether_force_random_prefix_base_selection";
+
+ // Upstream monitor would be stopped when tethering is down. When tethering restart, downstream
+ // address may be requested before coordinator get current upstream notification. To ensure
+ // coordinator do not select conflict downstream prefix, mUpstreamPrefixMap would not be cleared
+ // when tethering is down. Instead tethering would remove all deprecated upstreams from
+ // mUpstreamPrefixMap when tethering is starting. See #maybeRemoveDeprecatedUpstreams().
+ private final ArrayMap<Network, List<IpPrefix>> mUpstreamPrefixMap;
+ // The downstreams are indexed by Ipv4PrefixRequest, which is a wrapper of the Binder object of
+ // IIpv4PrefixRequest.
+ private final ArrayMap<Ipv4PrefixRequest, LinkAddress> mDownstreams;
+ private static final String LEGACY_WIFI_P2P_IFACE_ADDRESS = "192.168.49.1/24";
+ private static final String LEGACY_BLUETOOTH_IFACE_ADDRESS = "192.168.44.1/24";
+ private final List<IpPrefix> mTetheringPrefixes;
+ // A supplier that returns ConnectivityManager#getAllNetworks.
+ private final Supplier<Network[]> mGetAllNetworksSupplier;
+ private final Dependencies mDeps;
+ // keyed by downstream type(TetheringManager.TETHERING_*).
+ private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
+ private final Random mRandom;
+
+ /** Capture PrivateAddressCoordinator dependencies for injection. */
+ public static class Dependencies {
+ private final Context mContext;
+
+ Dependencies(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Check whether or not one specific experimental feature is enabled according to {@link
+ * DeviceConfigUtils}.
+ *
+ * @param featureName The feature's name to look up.
+ * @return true if this feature is enabled, or false if disabled.
+ */
+ public boolean isFeatureEnabled(String featureName) {
+ return DeviceConfigUtils.isTetheringFeatureEnabled(mContext, featureName);
+ }
+ }
+
+ public PrivateAddressCoordinator(Supplier<Network[]> getAllNetworksSupplier, Context context) {
+ this(getAllNetworksSupplier, new Dependencies(context));
+ }
+
+ @VisibleForTesting
+ public PrivateAddressCoordinator(Supplier<Network[]> getAllNetworksSupplier,
+ Dependencies deps) {
+ mDownstreams = new ArrayMap<>();
+ mUpstreamPrefixMap = new ArrayMap<>();
+ mGetAllNetworksSupplier = getAllNetworksSupplier;
+ mDeps = deps;
+ mCachedAddresses = new ArrayMap<AddressKey, LinkAddress>();
+ // Reserved static addresses for bluetooth and wifi p2p.
+ mCachedAddresses.put(new AddressKey(TETHERING_BLUETOOTH, CONNECTIVITY_SCOPE_GLOBAL),
+ new LinkAddress(LEGACY_BLUETOOTH_IFACE_ADDRESS));
+ mCachedAddresses.put(new AddressKey(TETHERING_WIFI_P2P, CONNECTIVITY_SCOPE_LOCAL),
+ new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS));
+
+ mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16"),
+ new IpPrefix("172.16.0.0/12"), new IpPrefix("10.0.0.0/8")));
+ mRandom = new Random();
+ }
+
+ /**
+ * Record a new upstream IpPrefix which may conflict with tethering downstreams. The downstreams
+ * will be notified if a conflict is found. When updateUpstreamPrefix is called,
+ * UpstreamNetworkState must have an already populated LinkProperties.
+ */
+ public void updateUpstreamPrefix(
+ final LinkProperties lp, final NetworkCapabilities nc, final Network network) {
+ // Do not support VPN as upstream. Normally, networkCapabilities is not expected to be null,
+ // but just checking to be sure.
+ if (nc != null && nc.hasTransport(TRANSPORT_VPN)) {
+ removeUpstreamPrefix(network);
+ return;
+ }
+
+ final ArrayList<IpPrefix> ipv4Prefixes = getIpv4Prefixes(lp.getAllLinkAddresses());
+ if (ipv4Prefixes.isEmpty()) {
+ removeUpstreamPrefix(network);
+ return;
+ }
+
+ mUpstreamPrefixMap.put(network, ipv4Prefixes);
+ handleMaybePrefixConflict(ipv4Prefixes);
+ }
+
+ private ArrayList<IpPrefix> getIpv4Prefixes(final List<LinkAddress> linkAddresses) {
+ final ArrayList<IpPrefix> list = new ArrayList<>();
+ for (LinkAddress address : linkAddresses) {
+ if (!address.isIpv4()) continue;
+
+ list.add(asIpPrefix(address));
+ }
+
+ return list;
+ }
+
+ private void handleMaybePrefixConflict(final List<IpPrefix> prefixes) {
+ for (Map.Entry<Ipv4PrefixRequest, LinkAddress> entry : mDownstreams.entrySet()) {
+ final Ipv4PrefixRequest request = entry.getKey();
+ final LinkAddress downstream = entry.getValue();
+ final IpPrefix target = asIpPrefix(downstream);
+
+ for (IpPrefix source : prefixes) {
+ if (isConflictPrefix(source, target)) {
+ try {
+ request.getRequest().onIpv4PrefixConflict(target);
+ } catch (RemoteException ignored) {
+ // ignore
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /** Remove IpPrefix records corresponding to input network. */
+ public void removeUpstreamPrefix(final Network network) {
+ mUpstreamPrefixMap.remove(network);
+ }
+
+ /**
+ * Maybe remove deprecated upstream records, this would be called once tethering started without
+ * any exiting tethered downstream.
+ */
+ public void maybeRemoveDeprecatedUpstreams() {
+ if (mUpstreamPrefixMap.isEmpty()) return;
+
+ // Remove all upstreams that are no longer valid networks
+ final Set<Network> toBeRemoved = new HashSet<>(mUpstreamPrefixMap.keySet());
+ toBeRemoved.removeAll(asList(mGetAllNetworksSupplier.get()));
+
+ mUpstreamPrefixMap.removeAll(toBeRemoved);
+ }
+
+ // TODO: There needs to be a reserveDownstreamAddress() method for the cases where
+ // TetheringRequest has been set a static IPv4 address.
+
+ /**
+ * Request a downstream address for the provided IIpv4PrefixRequest.
+ *
+ * This method will first try to return the last time used address for the provided
+ * (interfaceType, scope) pair if possible. If not, it will pick a random available address and
+ * mark its prefix as in use for the provided IIpv4PrefixRequest.
+ */
+ @Nullable
+ public LinkAddress requestStickyDownstreamAddress(int interfaceType, final int scope,
+ IIpv4PrefixRequest request) {
+ final Ipv4PrefixRequest wrappedRequest = new Ipv4PrefixRequest(request);
+ final AddressKey addrKey = new AddressKey(interfaceType, scope);
+ // This ensures that tethering isn't started on 2 different interfaces with the same type.
+ // Once tethering could support multiple interface with the same type,
+ // TetheringSoftApCallback would need to handle it among others.
+ final LinkAddress cachedAddress = mCachedAddresses.get(addrKey);
+ if (cachedAddress != null && !isConflictWithUpstream(asIpPrefix(cachedAddress))) {
+ mDownstreams.put(wrappedRequest, cachedAddress);
+ return cachedAddress;
+ }
+
+ final LinkAddress newAddress = requestDownstreamAddress(request);
+ if (newAddress != null) {
+ mCachedAddresses.put(addrKey, newAddress);
+ }
+ return newAddress;
+ }
+
+ /**
+ * Pick a random available address and mark its prefix as in use for the provided
+ * IIpv4PrefixRequest. Return null if there is no available address.
+ */
+ @Nullable
+ public LinkAddress requestDownstreamAddress(IIpv4PrefixRequest request) {
+ final Ipv4PrefixRequest wrappedRequest = new Ipv4PrefixRequest(request);
+ final int prefixIndex = getRandomPrefixIndex();
+ for (int i = 0; i < mTetheringPrefixes.size(); i++) {
+ final IpPrefix prefixRange = mTetheringPrefixes.get(
+ (prefixIndex + i) % mTetheringPrefixes.size());
+ final LinkAddress newAddress = chooseDownstreamAddress(prefixRange);
+ if (newAddress != null) {
+ mDownstreams.put(wrappedRequest, newAddress);
+ return newAddress;
+ }
+ }
+
+ // No available address.
+ return null;
+ }
+
+ private int getRandomPrefixIndex() {
+ if (!mDeps.isFeatureEnabled(TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION)) return 0;
+
+ final int random = getRandomInt() & 0xffffff;
+ // This is to select the starting prefix range (/8, /12, or /16) instead of the actual
+ // LinkAddress. To avoid complex operations in the selection logic and make the selected
+ // rate approximate consistency with that /8 is around 2^4 times of /12 and /12 is around
+ // 2^4 times of /16, we simply define a map between the value and the prefix value like
+ // this:
+ //
+ // Value 0 ~ 0xffff (65536/16777216 = 0.39%) -> 192.168.0.0/16
+ // Value 0x10000 ~ 0xfffff (983040/16777216 = 5.86%) -> 172.16.0.0/12
+ // Value 0x100000 ~ 0xffffff (15728640/16777216 = 93.7%) -> 10.0.0.0/8
+ if (random > 0xfffff) {
+ return 2;
+ } else if (random > 0xffff) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ private int getPrefixBaseAddress(final IpPrefix prefix) {
+ return inet4AddressToIntHTH((Inet4Address) prefix.getAddress());
+ }
+
+ /**
+ * Check whether input prefix conflict with upstream prefixes or in-use downstream prefixes.
+ * If yes, return one of them.
+ */
+ private IpPrefix getConflictPrefix(final IpPrefix prefix) {
+ final IpPrefix upstream = getConflictWithUpstream(prefix);
+ if (upstream != null) return upstream;
+
+ return getInUseDownstreamPrefix(prefix);
+ }
+
+ @VisibleForTesting
+ public LinkAddress chooseDownstreamAddress(final IpPrefix prefixRange) {
+ // The netmask of the prefix assignment block (e.g., 0xfff00000 for 172.16.0.0/12).
+ final int prefixRangeMask = prefixLengthToV4NetmaskIntHTH(prefixRange.getPrefixLength());
+
+ // The zero address in the block (e.g., 0xac100000 for 172.16.0.0/12).
+ final int baseAddress = getPrefixBaseAddress(prefixRange);
+
+ // Try to get an address within the given prefix that does not conflict with any other
+ // prefix in the system.
+ for (int i = 0; i < 20; ++i) {
+ final int randomSuffix = mRandom.nextInt() & ~prefixRangeMask;
+ final int randomAddress = baseAddress | randomSuffix;
+
+ // Avoid selecting x.x.x.[0, 1, 255] addresses.
+ switch (randomAddress & 0xFF) {
+ case 0:
+ case 1:
+ case 255:
+ // Try selecting a different address
+ continue;
+ }
+
+ // Avoid selecting commonly used subnets.
+ switch (randomAddress & 0xFFFFFF00) {
+ case 0xC0A80000: // 192.168.0.0/24
+ case 0xC0A80100: // 192.168.1.0/24
+ case 0xC0A85800: // 192.168.88.0/24
+ case 0xC0A86400: // 192.168.100.0/24
+ continue;
+ }
+
+ // Avoid 10.0.0.0 - 10.10.255.255
+ if (randomAddress >= 0x0A000000 && randomAddress <= 0x0A0AFFFF) {
+ continue;
+ }
+
+ final InetAddress address = intToInet4AddressHTH(randomAddress);
+ final IpPrefix prefix = new IpPrefix(address, PREFIX_LENGTH);
+ if (getConflictPrefix(prefix) != null) {
+ // Prefix is conflicting with another prefix used in the system, find another one.
+ continue;
+ }
+ return new LinkAddress(address, PREFIX_LENGTH);
+ }
+ // Could not find a prefix, return null and let caller try another range.
+ return null;
+ }
+
+ /** Get random int which could be used to generate random address. */
+ // TODO: get rid of this function and mock getRandomPrefixIndex in tests.
+ @VisibleForTesting
+ public int getRandomInt() {
+ return mRandom.nextInt();
+ }
+
+ /** Release downstream record for IpServer. */
+ public void releaseDownstream(IIpv4PrefixRequest request) {
+ mDownstreams.remove(new Ipv4PrefixRequest(request));
+ }
+
+ /** Clear current upstream prefixes records. */
+ public void clearUpstreamPrefixes() {
+ mUpstreamPrefixMap.clear();
+ }
+
+ private IpPrefix getConflictWithUpstream(final IpPrefix prefix) {
+ for (int i = 0; i < mUpstreamPrefixMap.size(); i++) {
+ final List<IpPrefix> list = mUpstreamPrefixMap.valueAt(i);
+ for (IpPrefix upstream : list) {
+ if (isConflictPrefix(prefix, upstream)) return upstream;
+ }
+ }
+ return null;
+ }
+
+ private boolean isConflictWithUpstream(final IpPrefix prefix) {
+ return getConflictWithUpstream(prefix) != null;
+ }
+
+ private boolean isConflictPrefix(final IpPrefix prefix1, final IpPrefix prefix2) {
+ if (prefix2.getPrefixLength() < prefix1.getPrefixLength()) {
+ return prefix2.contains(prefix1.getAddress());
+ }
+
+ return prefix1.contains(prefix2.getAddress());
+ }
+
+ // InUse Prefixes are prefixes of mCachedAddresses which are active downstream addresses, last
+ // downstream addresses(reserved for next time) and static addresses(e.g. bluetooth, wifi p2p).
+ private IpPrefix getInUseDownstreamPrefix(final IpPrefix prefix) {
+ for (int i = 0; i < mCachedAddresses.size(); i++) {
+ final IpPrefix downstream = asIpPrefix(mCachedAddresses.valueAt(i));
+ if (isConflictPrefix(prefix, downstream)) return downstream;
+ }
+
+ // IpServer may use manually-defined address (mStaticIpv4ServerAddr) which does not include
+ // in mCachedAddresses.
+ for (LinkAddress downstream : mDownstreams.values()) {
+ final IpPrefix target = asIpPrefix(downstream);
+
+ if (isConflictPrefix(prefix, target)) return target;
+ }
+
+ return null;
+ }
+
+ private static IpPrefix asIpPrefix(LinkAddress addr) {
+ return new IpPrefix(addr.getAddress(), addr.getPrefixLength());
+ }
+
+ private static final class Ipv4PrefixRequest {
+ private final IIpv4PrefixRequest mRequest;
+
+ Ipv4PrefixRequest(IIpv4PrefixRequest request) {
+ mRequest = request;
+ }
+
+ public IIpv4PrefixRequest getRequest() {
+ return mRequest;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof Ipv4PrefixRequest)) return false;
+ return Objects.equals(
+ mRequest.asBinder(), ((Ipv4PrefixRequest) obj).mRequest.asBinder());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mRequest.asBinder());
+ }
+ }
+
+ private static class AddressKey {
+ private final int mTetheringType;
+ private final int mScope;
+
+ private AddressKey(int type, int scope) {
+ mTetheringType = type;
+ mScope = scope;
+ }
+
+ @Override
+ public int hashCode() {
+ return (mTetheringType << 16) + mScope;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof AddressKey)) return false;
+ final AddressKey other = (AddressKey) obj;
+
+ return mTetheringType == other.mTetheringType && mScope == other.mScope;
+ }
+
+ @Override
+ public String toString() {
+ return "AddressKey(" + mTetheringType + ", " + mScope + ")";
+ }
+ }
+
+ // TODO: dump PrivateAddressCoordinator when dumping RoutingCoordinatorService and apply
+ // indentation.
+ void dump(final PrintWriter pw) {
+ pw.println("mTetheringPrefixes:");
+ for (IpPrefix prefix : mTetheringPrefixes) {
+ pw.println(prefix);
+ }
+
+ pw.println("mUpstreamPrefixMap:");
+ for (int i = 0; i < mUpstreamPrefixMap.size(); i++) {
+ pw.println(mUpstreamPrefixMap.keyAt(i) + " - " + mUpstreamPrefixMap.valueAt(i));
+ }
+
+ pw.println("mDownstreams:");
+ for (LinkAddress downstream : mDownstreams.values()) {
+ pw.println(downstream);
+ }
+
+ pw.println("mCachedAddresses:");
+ for (int i = 0; i < mCachedAddresses.size(); i++) {
+ pw.println(mCachedAddresses.keyAt(i) + " - " + mCachedAddresses.valueAt(i));
+ }
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
index 02e3643..f5af30c 100644
--- a/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
+++ b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
@@ -17,17 +17,27 @@
package com.android.net.module.util;
import android.content.Context;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.RouteInfo;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
/**
* A manager class for talking to the routing coordinator service.
*
* This class should only be used by the connectivity and tethering module. This is enforced
* by the build rules. Do not change build rules to gain access to this class from elsewhere.
+ *
+ * This class has following functionalities:
+ * - Manage routes and forwarding for networks.
+ * - Manage IPv4 prefix allocation for network interfaces.
+ *
* @hide
*/
public class RoutingCoordinatorManager {
@@ -154,4 +164,77 @@
throw e.rethrowFromSystemServer();
}
}
+
+ // PrivateAddressCoordinator methods:
+
+ /** Update the prefix of an upstream. */
+ public void updateUpstreamPrefix(LinkProperties lp, NetworkCapabilities nc, Network network) {
+ try {
+ mService.updateUpstreamPrefix(lp, nc, network);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Remove the upstream prefix of the given {@link Network}. */
+ public void removeUpstreamPrefix(Network network) {
+ try {
+ mService.removeUpstreamPrefix(network);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Remove the deprecated upstream networks if any. */
+ public void maybeRemoveDeprecatedUpstreams() {
+ try {
+ mService.maybeRemoveDeprecatedUpstreams();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Request an IPv4 address for the downstream. Return the last time used address for the
+ * provided (interfaceType, scope) pair if possible.
+ *
+ * @param interfaceType the Tethering type (see TetheringManager#TETHERING_*).
+ * @param scope CONNECTIVITY_SCOPE_GLOBAL or CONNECTIVITY_SCOPE_LOCAL
+ * @param request a {@link IIpv4PrefixRequest} to report conflicts
+ * @return an IPv4 address allocated for the downstream, could be null
+ */
+ @Nullable
+ public LinkAddress requestStickyDownstreamAddress(
+ int interfaceType,
+ int scope,
+ IIpv4PrefixRequest request) {
+ try {
+ return mService.requestStickyDownstreamAddress(interfaceType, scope, request);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Request an IPv4 address for the downstream.
+ *
+ * @param request a {@link IIpv4PrefixRequest} to report conflicts
+ * @return an IPv4 address allocated for the downstream, could be null
+ */
+ public LinkAddress requestDownstreamAddress(IIpv4PrefixRequest request) {
+ try {
+ return mService.requestDownstreamAddress(request);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Release the IPv4 address allocated for the downstream. */
+ public void releaseDownstream(IIpv4PrefixRequest request) {
+ try {
+ mService.releaseDownstream(request);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
index c75b860..51eb47c 100644
--- a/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
+++ b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
@@ -19,8 +19,13 @@
import static com.android.net.module.util.NetdUtils.toRouteInfoParcel;
import android.annotation.NonNull;
+import android.content.Context;
import android.net.INetd;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.RouteInfo;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
@@ -28,8 +33,10 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.Objects;
+import java.util.function.Supplier;
/**
* Class to coordinate routing across multiple clients.
@@ -45,8 +52,22 @@
private static final String TAG = RoutingCoordinatorService.class.getSimpleName();
private final INetd mNetd;
- public RoutingCoordinatorService(@NonNull INetd netd) {
+ private final Object mPrivateAddressCoordinatorLock = new Object();
+ @GuardedBy("mPrivateAddressCoordinatorLock")
+ private final PrivateAddressCoordinator mPrivateAddressCoordinator;
+
+ public RoutingCoordinatorService(@NonNull INetd netd,
+ @NonNull Supplier<Network[]> getAllNetworksSupplier,
+ @NonNull Context context) {
+ this(netd, getAllNetworksSupplier, new PrivateAddressCoordinator.Dependencies(context));
+ }
+
+ @VisibleForTesting
+ public RoutingCoordinatorService(@NonNull INetd netd,
+ @NonNull Supplier<Network[]> getAllNetworksSupplier,
+ @NonNull PrivateAddressCoordinator.Dependencies pacDeps) {
mNetd = netd;
+ mPrivateAddressCoordinator = new PrivateAddressCoordinator(getAllNetworksSupplier, pacDeps);
}
/**
@@ -225,4 +246,91 @@
}
}
}
+
+ // PrivateAddressCoordinator methods:
+
+ /** Update the prefix of an upstream. */
+ @Override
+ public void updateUpstreamPrefix(LinkProperties lp, NetworkCapabilities nc, Network network) {
+ BinderUtils.withCleanCallingIdentity(
+ () -> {
+ synchronized (mPrivateAddressCoordinatorLock) {
+ mPrivateAddressCoordinator.updateUpstreamPrefix(lp, nc, network);
+ }
+ });
+ }
+
+ /** Remove the upstream prefix of the given {@link Network}. */
+ @Override
+ public void removeUpstreamPrefix(Network network) {
+ Objects.requireNonNull(network);
+ BinderUtils.withCleanCallingIdentity(
+ () -> {
+ synchronized (mPrivateAddressCoordinatorLock) {
+ mPrivateAddressCoordinator.removeUpstreamPrefix(network);
+ }
+ });
+ }
+
+ /** Remove the deprecated upstream networks if any. */
+ @Override
+ public void maybeRemoveDeprecatedUpstreams() {
+ BinderUtils.withCleanCallingIdentity(
+ () -> {
+ synchronized (mPrivateAddressCoordinatorLock) {
+ mPrivateAddressCoordinator.maybeRemoveDeprecatedUpstreams();
+ }
+ });
+ }
+
+ /**
+ * Request an IPv4 address for the downstream. Return the last time used address for the
+ * provided (interfaceType, scope) pair if possible.
+ *
+ * @param interfaceType the Tethering type (see TetheringManager#TETHERING_*).
+ * @param scope CONNECTIVITY_SCOPE_GLOBAL or CONNECTIVITY_SCOPE_LOCAL
+ * @param request a {@link IIpv4PrefixRequest} to report conflicts
+ * @return an IPv4 address allocated for the downstream, could be null
+ */
+ @Override
+ public LinkAddress requestStickyDownstreamAddress(int interfaceType, int scope,
+ IIpv4PrefixRequest request) {
+ Objects.requireNonNull(request);
+ return BinderUtils.withCleanCallingIdentity(
+ () -> {
+ synchronized (mPrivateAddressCoordinatorLock) {
+ return mPrivateAddressCoordinator.requestStickyDownstreamAddress(
+ interfaceType, scope, request);
+ }
+ });
+ }
+
+ /**
+ * Request an IPv4 address for the downstream.
+ *
+ * @param request a {@link IIpv4PrefixRequest} to report conflicts
+ * @return an IPv4 address allocated for the downstream, could be null
+ */
+ @Override
+ public LinkAddress requestDownstreamAddress(IIpv4PrefixRequest request) {
+ Objects.requireNonNull(request);
+ return BinderUtils.withCleanCallingIdentity(
+ () -> {
+ synchronized (mPrivateAddressCoordinatorLock) {
+ return mPrivateAddressCoordinator.requestDownstreamAddress(request);
+ }
+ });
+ }
+
+ /** Release the IPv4 address allocated for the downstream. */
+ @Override
+ public void releaseDownstream(IIpv4PrefixRequest request) {
+ Objects.requireNonNull(request);
+ BinderUtils.withCleanCallingIdentity(
+ () -> {
+ synchronized (mPrivateAddressCoordinatorLock) {
+ mPrivateAddressCoordinator.releaseDownstream(request);
+ }
+ });
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/Struct.java b/staticlibs/device/com/android/net/module/util/Struct.java
index ff7a711..69ca678 100644
--- a/staticlibs/device/com/android/net/module/util/Struct.java
+++ b/staticlibs/device/com/android/net/module/util/Struct.java
@@ -105,6 +105,7 @@
*/
public class Struct {
public enum Type {
+ Bool, // bool, size = 1 byte
U8, // unsigned byte, size = 1 byte
U16, // unsigned short, size = 2 bytes
U32, // unsigned int, size = 4 bytes
@@ -169,6 +170,9 @@
private static void checkAnnotationType(final Field annotation, final Class fieldType) {
switch (annotation.type()) {
+ case Bool:
+ if (fieldType == Boolean.TYPE) return;
+ break;
case U8:
case S16:
if (fieldType == Short.TYPE) return;
@@ -218,6 +222,7 @@
private static int getFieldLength(final Field annotation) {
int length = 0;
switch (annotation.type()) {
+ case Bool:
case U8:
case S8:
length = 1;
@@ -357,6 +362,9 @@
final Object value;
checkAnnotationType(fieldInfo.annotation, fieldInfo.field.getType());
switch (fieldInfo.annotation.type()) {
+ case Bool:
+ value = buf.get() != 0;
+ break;
case U8:
value = (short) (buf.get() & 0xFF);
break;
@@ -457,6 +465,9 @@
private static void putFieldValue(final ByteBuffer output, final FieldInfo fieldInfo,
final Object value) throws BufferUnderflowException {
switch (fieldInfo.annotation.type()) {
+ case Bool:
+ output.put((byte) (value != null && (boolean) value ? 1 : 0));
+ break;
case U8:
output.put((byte) (((short) value) & 0xFF));
break;
@@ -748,6 +759,16 @@
return sb.toString();
}
+ /** A simple Struct which only contains a bool field. */
+ public static class Bool extends Struct {
+ @Struct.Field(order = 0, type = Struct.Type.Bool)
+ public final boolean val;
+
+ public Bool(final boolean val) {
+ this.val = val;
+ }
+ }
+
/** A simple Struct which only contains a u8 field. */
public static class U8 extends Struct {
@Struct.Field(order = 0, type = Struct.Type.U8)
diff --git a/staticlibs/device/com/android/net/module/util/SyncStateMachine.java b/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
index da184d3..fc0161b 100644
--- a/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
+++ b/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
@@ -225,7 +225,8 @@
consideredState = mStateInfo.get(consideredState.parent);
}
if (null == consideredState) {
- Log.wtf(mName, "Message " + msg.what + " was not handled");
+ final String state = mCurrentState == null ? "null" : mCurrentState.getName();
+ Log.wtf(mName, "Message " + msg.what + " was not handled. Current state: " + state);
}
performTransitions();
diff --git a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
new file mode 100644
index 0000000..310dbc9
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -0,0 +1,79 @@
+/*
+ * 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.net.module.util;
+
+import android.os.Process;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Contains mostly timerfd functionality.
+ */
+public class TimerFdUtils {
+ static {
+ if (Process.myUid() == Process.SYSTEM_UID) {
+ // This library is part of service-connectivity.jar when in the system server,
+ // so libservice-connectivity.so is the library to load.
+ System.loadLibrary("service-connectivity");
+ } else {
+ System.loadLibrary(JniUtil.getJniLibraryName(TimerFdUtils.class.getPackage()));
+ }
+ }
+
+ private static final String TAG = TimerFdUtils.class.getSimpleName();
+
+ /**
+ * Create a timerfd.
+ *
+ * @throws IOException if the timerfd creation is failed.
+ */
+ private static native int createTimerFd() throws IOException;
+
+ /**
+ * Set given time to the timerfd.
+ *
+ * @param timeMs target time
+ * @throws IOException if setting expiration time is failed.
+ */
+ private static native void setTime(int fd, long timeMs) throws IOException;
+
+ /**
+ * Create a timerfd
+ */
+ static int createTimerFileDescriptor() {
+ try {
+ return createTimerFd();
+ } catch (IOException e) {
+ Log.e(TAG, "createTimerFd failed", e);
+ return -1;
+ }
+ }
+
+ /**
+ * Set expiration time to timerfd
+ */
+ static boolean setExpirationTime(int id, long expirationTimeMs) {
+ try {
+ setTime(id, expirationTimeMs);
+ } catch (IOException e) {
+ Log.e(TAG, "setExpirationTime failed", e);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/OsAccess.java b/staticlibs/device/com/android/net/module/util/netlink/OsAccess.java
new file mode 100644
index 0000000..7591d5c
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/OsAccess.java
@@ -0,0 +1,37 @@
+/*
+ * 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.net.module.util.netlink;
+
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+
+/**
+ * This class wraps the static methods of {@link android.system.Os} for mocking and testing.
+ */
+public class OsAccess {
+ /**
+ * Constant indicating that the {@code if_nametoindex()} function could not find the network
+ * interface index corresponding to the given interface name.
+ */
+ public static int INVALID_INTERFACE_INDEX = 0;
+
+ /** Wraps {@link Os#if_nametoindex(String)}. */
+ public int if_nametoindex(@NonNull String name) {
+ return Os.if_nametoindex(name);
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index 0c49edc..037d95f 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -16,6 +16,15 @@
package com.android.net.module.util.netlink;
+import static android.system.OsConstants.AF_UNSPEC;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_GETLINK;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST_ACK;
+
import android.net.MacAddress;
import android.system.OsConstants;
@@ -24,6 +33,7 @@
import androidx.annotation.VisibleForTesting;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
/**
* A NetlinkMessage subclass for rtnetlink link messages.
@@ -46,29 +56,55 @@
public static final short IN6_ADDR_GEN_MODE_NONE = 1;
- private int mMtu;
- @NonNull
- private StructIfinfoMsg mIfinfomsg;
- @Nullable
- private MacAddress mHardwareAddress;
- @Nullable
- private String mInterfaceName;
+ // The maximum buffer size to hold an interface name including the null-terminator '\0'.
+ private static final int IFNAMSIZ = 16;
+ // The default value of MTU, which means the MTU is unspecified.
+ private static final int DEFAULT_MTU = 0;
- private RtNetlinkLinkMessage(@NonNull StructNlMsgHdr header) {
- super(header);
- mIfinfomsg = null;
- mMtu = 0;
- mHardwareAddress = null;
- mInterfaceName = null;
+ @NonNull
+ private final StructIfinfoMsg mIfinfomsg;
+ private final int mMtu;
+ @Nullable
+ private final MacAddress mHardwareAddress;
+ @Nullable
+ private final String mInterfaceName;
+
+ /**
+ * Creates an {@link RtNetlinkLinkMessage} instance.
+ *
+ * <p>This method validates the arguments and returns {@code null} if any of them are invalid.
+ * nlmsghdr's nlmsg_len will be updated to the correct length before creation.
+ *
+ * @param nlmsghdr The Netlink message header. Must not be {@code null}.
+ * @param ifinfomsg The interface information message. Must not be {@code null}.
+ * @param mtu The Maximum Transmission Unit (MTU) value for the link.
+ * @param hardwareAddress The hardware address (MAC address) of the link. May be {@code null}.
+ * @param interfaceName The name of the interface. May be {@code null}.
+ * @return A new {@link RtNetlinkLinkMessage} instance, or {@code null} if the input arguments
+ * are invalid.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage build(@NonNull StructNlMsgHdr nlmsghdr,
+ @NonNull StructIfinfoMsg ifinfomsg, int mtu, @Nullable MacAddress hardwareAddress,
+ @Nullable String interfaceName) {
+ if (mtu < 0) {
+ return null;
+ }
+ if (interfaceName != null
+ && (interfaceName.isEmpty() || interfaceName.length() + 1 > IFNAMSIZ)) {
+ return null;
+ }
+
+ nlmsghdr.nlmsg_len = calculateMessageLength(mtu, hardwareAddress, interfaceName);
+ return new RtNetlinkLinkMessage(nlmsghdr, ifinfomsg, mtu, hardwareAddress, interfaceName);
}
- @VisibleForTesting
- public RtNetlinkLinkMessage(@NonNull StructNlMsgHdr nlmsghdr,
- int mtu, @NonNull StructIfinfoMsg ifinfomsg, @NonNull MacAddress hardwareAddress,
- @NonNull String interfaceName) {
+ private RtNetlinkLinkMessage(@NonNull StructNlMsgHdr nlmsghdr,
+ @NonNull StructIfinfoMsg ifinfomsg, int mtu, @Nullable MacAddress hardwareAddress,
+ @Nullable String interfaceName) {
super(nlmsghdr);
- mMtu = mtu;
mIfinfomsg = ifinfomsg;
+ mMtu = mtu;
mHardwareAddress = hardwareAddress;
mInterfaceName = interfaceName;
}
@@ -102,33 +138,46 @@
@Nullable
public static RtNetlinkLinkMessage parse(@NonNull final StructNlMsgHdr header,
@NonNull final ByteBuffer byteBuffer) {
- final RtNetlinkLinkMessage linkMsg = new RtNetlinkLinkMessage(header);
-
- linkMsg.mIfinfomsg = StructIfinfoMsg.parse(byteBuffer);
- if (linkMsg.mIfinfomsg == null) return null;
+ final StructIfinfoMsg ifinfoMsg = StructIfinfoMsg.parse(byteBuffer);
+ if (ifinfoMsg == null) {
+ return null;
+ }
// IFLA_MTU
+ int mtu = DEFAULT_MTU;
final int baseOffset = byteBuffer.position();
StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(IFLA_MTU, byteBuffer);
if (nlAttr != null) {
- linkMsg.mMtu = nlAttr.getValueAsInt(0 /* default value */);
+ mtu = nlAttr.getValueAsInt(DEFAULT_MTU);
}
// IFLA_ADDRESS
+ MacAddress hardwareAddress = null;
byteBuffer.position(baseOffset);
nlAttr = StructNlAttr.findNextAttrOfType(IFLA_ADDRESS, byteBuffer);
if (nlAttr != null) {
- linkMsg.mHardwareAddress = nlAttr.getValueAsMacAddress();
+ hardwareAddress = nlAttr.getValueAsMacAddress();
}
// IFLA_IFNAME
+ String interfaceName = null;
byteBuffer.position(baseOffset);
nlAttr = StructNlAttr.findNextAttrOfType(IFLA_IFNAME, byteBuffer);
if (nlAttr != null) {
- linkMsg.mInterfaceName = nlAttr.getValueAsString();
+ interfaceName = nlAttr.getValueAsString();
}
- return linkMsg;
+ return new RtNetlinkLinkMessage(header, ifinfoMsg, mtu, hardwareAddress, interfaceName);
+ }
+
+ /**
+ * Write a rtnetlink link message to {@link byte} array.
+ */
+ public byte[] pack(ByteOrder order) {
+ byte[] bytes = new byte[mHeader.nlmsg_len];
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(order);
+ pack(buffer);
+ return bytes;
}
/**
@@ -136,10 +185,10 @@
*/
@VisibleForTesting
protected void pack(ByteBuffer byteBuffer) {
- getHeader().pack(byteBuffer);
+ mHeader.pack(byteBuffer);
mIfinfomsg.pack(byteBuffer);
- if (mMtu != 0) {
+ if (mMtu != DEFAULT_MTU) {
final StructNlAttr mtu = new StructNlAttr(IFLA_MTU, mMtu);
mtu.pack(byteBuffer);
}
@@ -153,11 +202,121 @@
}
}
+ /**
+ * Calculate the byte length of the packed buffer.
+ */
+ private static int calculateMessageLength(int mtu, MacAddress hardwareAddress,
+ String interfaceName) {
+ int length = StructNlMsgHdr.STRUCT_SIZE + StructIfinfoMsg.STRUCT_SIZE;
+
+ if (mtu != DEFAULT_MTU) {
+ length += NetlinkConstants.alignedLengthOf(StructNlAttr.NLA_HEADERLEN + Integer.BYTES);
+ }
+ if (hardwareAddress != null) {
+ length += NetlinkConstants.alignedLengthOf(
+ StructNlAttr.NLA_HEADERLEN + ETHER_ADDR_LEN);
+ }
+ if (interfaceName != null) {
+ length += NetlinkConstants.alignedLengthOf(
+ // The string should be end with '\0', so the length should plus 1.
+ StructNlAttr.NLA_HEADERLEN + interfaceName.length() + 1);
+ }
+
+ return length;
+ }
+
+ /**
+ * Create a link message to set the operational state (up or down) of a network interface.
+ *
+ * @param interfaceName The network interface name.
+ * @param sequenceNumber The sequence number to use for the Netlink message.
+ * @param isUp {@code true} to set the interface up, {@code false} to set it down.
+ * @return A `RtNetlinkLinkMessage` instance configured to set the link state.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createSetLinkStateMessage(@NonNull String interfaceName,
+ int sequenceNumber, boolean isUp) {
+ return createSetLinkStateMessage(interfaceName, sequenceNumber, isUp, new OsAccess());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createSetLinkStateMessage(@NonNull String interfaceName,
+ int sequenceNumber, boolean isUp, OsAccess osAccess) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(0, RTM_NEWLINK, NLM_F_REQUEST_ACK, sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, (short) 0, interfaceIndex,
+ isUp ? IFF_UP : 0, IFF_UP), DEFAULT_MTU, null, null);
+ }
+
+ /**
+ * Create a link message to rename the network interface.
+ *
+ * @param interfaceName The network interface name.
+ * @param sequenceNumber The sequence number to use for the Netlink message.
+ * @param newName The new name of the network interface.
+ * @return A `RtNetlinkLinkMessage` instance configured to rename the network interface.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createSetLinkNameMessage(@NonNull String interfaceName,
+ int sequenceNumber, @NonNull String newName) {
+ return createSetLinkNameMessage(interfaceName, sequenceNumber, newName, new OsAccess());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createSetLinkNameMessage(@NonNull String interfaceName,
+ int sequenceNumber, @NonNull String newName, OsAccess osAccess) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(0, RTM_NEWLINK, NLM_F_REQUEST_ACK, sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, (short) 0, interfaceIndex, 0, 0),
+ DEFAULT_MTU, null, newName);
+ }
+
+ /**
+ * Creates an {@link RtNetlinkLinkMessage} instance that can be used to get the link information
+ * of a network interface.
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param sequenceNumber The sequence number for the Netlink message.
+ * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createGetLinkMessage(@NonNull String interfaceName,
+ int sequenceNumber) {
+ return createGetLinkMessage(interfaceName, sequenceNumber, new OsAccess());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createGetLinkMessage(@NonNull String interfaceName,
+ int sequenceNumber, @NonNull OsAccess osAccess) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(0, RTM_GETLINK, NLM_F_REQUEST, sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, (short) 0, interfaceIndex, 0, 0),
+ DEFAULT_MTU, null, null);
+ }
+
@Override
public String toString() {
return "RtNetlinkLinkMessage{ "
+ "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
- + "Ifinfomsg{" + mIfinfomsg.toString() + "}, "
+ + "Ifinfomsg{" + mIfinfomsg + "}, "
+ "Hardware Address{" + mHardwareAddress + "}, "
+ "MTU{" + mMtu + "}, "
+ "Ifname{" + mInterfaceName + "} "
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
index 5272366..7cc95de 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
@@ -32,10 +32,11 @@
// Already aligned.
public static final int STRUCT_SIZE = 16;
- public static final short NLM_F_REQUEST = 0x0001;
- public static final short NLM_F_MULTI = 0x0002;
- public static final short NLM_F_ACK = 0x0004;
- public static final short NLM_F_ECHO = 0x0008;
+ public static final short NLM_F_REQUEST = 0x0001;
+ public static final short NLM_F_MULTI = 0x0002;
+ public static final short NLM_F_ACK = 0x0004;
+ public static final short NLM_F_ECHO = 0x0008;
+ public static final short NLM_F_REQUEST_ACK = NLM_F_REQUEST | NLM_F_ACK;
// Flags for a GET request.
public static final short NLM_F_ROOT = 0x0100;
public static final short NLM_F_MATCH = 0x0200;
diff --git a/staticlibs/framework/com/android/net/module/util/DnsPacket.java b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
index 63106a1..b0c5e2e 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsPacket.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
@@ -50,7 +50,7 @@
*
* @hide
*/
-public abstract class DnsPacket {
+public class DnsPacket {
/**
* Type of the canonical name for an alias. Refer to RFC 1035 section 3.2.2.
*/
@@ -515,7 +515,14 @@
protected final DnsHeader mHeader;
protected final List<DnsRecord>[] mRecords;
- protected DnsPacket(@NonNull byte[] data) throws ParseException {
+ /**
+ * Returns the list of DNS records for a given section.
+ */
+ public List<DnsRecord> getRecords(@RecordType int section) {
+ return mRecords[section];
+ }
+
+ public DnsPacket(@NonNull byte[] data) throws ParseException {
if (null == data) {
throw new ParseException("Parse header failed, null input data");
}
@@ -548,7 +555,7 @@
*
* Note that authority records section and additional records section is not supported.
*/
- protected DnsPacket(@NonNull DnsHeader header, @NonNull List<DnsRecord> qd,
+ public DnsPacket(@NonNull DnsHeader header, @NonNull List<DnsRecord> qd,
@NonNull List<DnsRecord> an) {
mHeader = Objects.requireNonNull(header);
mRecords = new List[NUM_SECTIONS];
diff --git a/staticlibs/framework/com/android/net/module/util/DnsUtils.java b/staticlibs/framework/com/android/net/module/util/DnsUtils.java
new file mode 100644
index 0000000..19ffd72
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsUtils.java
@@ -0,0 +1,95 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Dns common utility functions.
+ *
+ * @hide
+ */
+public class DnsUtils {
+
+ private DnsUtils() { }
+
+ /**
+ * Convert the string to DNS case-insensitive uppercase.
+ *
+ * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to
+ * their unaccented counterparts. So the "DNS uppercase" should be if character is a-z then they
+ * transform into A-Z. Otherwise, they are kept as-is.
+ */
+ public static String toDnsUpperCase(@NonNull String string) {
+ final char[] outChars = new char[string.length()];
+ for (int i = 0; i < string.length(); i++) {
+ outChars[i] = toDnsUpperCase(string.charAt(i));
+ }
+ return new String(outChars);
+ }
+
+ /**
+ * Convert the array of labels to DNS case-insensitive uppercase.
+ */
+ public static String[] toDnsLabelsUpperCase(@NonNull String[] labels) {
+ final String[] outStrings = new String[labels.length];
+ for (int i = 0; i < labels.length; ++i) {
+ outStrings[i] = toDnsUpperCase(labels[i]);
+ }
+ return outStrings;
+ }
+
+ /**
+ * Compare two strings by DNS case-insensitive uppercase.
+ */
+ public static boolean equalsIgnoreDnsCase(@Nullable String a, @Nullable String b) {
+ if (a == null || b == null) {
+ return a == null && b == null;
+ }
+ if (a.length() != b.length()) return false;
+ for (int i = 0; i < a.length(); i++) {
+ if (toDnsUpperCase(a.charAt(i)) != toDnsUpperCase(b.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Compare two set of DNS labels by DNS case-insensitive uppercase.
+ */
+ public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
+ if (a == b) {
+ return true;
+ }
+ int length = a.length;
+ if (b.length != length) {
+ return false;
+ }
+ for (int i = 0; i < length; i++) {
+ if (!equalsIgnoreDnsCase(a[i], b[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static char toDnsUpperCase(char a) {
+ return a >= 'a' && a <= 'z' ? (char) (a - ('a' - 'A')) : a;
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
index f6bee69..e4d25cd 100644
--- a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
+++ b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
@@ -16,6 +16,8 @@
package com.android.net.module.util;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
import android.Manifest;
import android.annotation.IntDef;
import android.annotation.Nullable;
@@ -112,48 +114,14 @@
*
* @return {@link LocationPermissionCheckStatus} the result of the location permission check.
*/
- public @LocationPermissionCheckStatus int checkLocationPermissionWithDetailInfo(
+ @VisibleForTesting(visibility = PRIVATE)
+ public @LocationPermissionCheckStatus int checkLocationPermissionInternal(
String pkgName, @Nullable String featureId, int uid, @Nullable String message) {
- final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
- switch (result) {
- case ERROR_LOCATION_MODE_OFF:
- Log.e(TAG, "Location mode is disabled for the device");
- break;
- case ERROR_LOCATION_PERMISSION_MISSING:
- Log.e(TAG, "UID " + uid + " has no location permission");
- break;
+ try {
+ checkPackage(uid, pkgName);
+ } catch (SecurityException e) {
+ return ERROR_LOCATION_PERMISSION_MISSING;
}
- return result;
- }
-
- /**
- * Enforce the caller has location permission.
- *
- * This API determines if the location mode enabled for the caller and the caller has
- * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
- * SecurityException is thrown if the caller has no permission or the location mode is disabled.
- *
- * @param pkgName package name of the application requesting access
- * @param featureId The feature in the package
- * @param uid The uid of the package
- * @param message A message describing why the permission was checked. Only needed if this is
- * not inside of a two-way binder call from the data receiver
- */
- public void enforceLocationPermission(String pkgName, @Nullable String featureId, int uid,
- @Nullable String message) throws SecurityException {
- final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
-
- switch (result) {
- case ERROR_LOCATION_MODE_OFF:
- throw new SecurityException("Location mode is disabled for the device");
- case ERROR_LOCATION_PERMISSION_MISSING:
- throw new SecurityException("UID " + uid + " has no location permission");
- }
- }
-
- private int checkLocationPermissionInternal(String pkgName, @Nullable String featureId,
- int uid, @Nullable String message) {
- checkPackage(uid, pkgName);
// Apps with NETWORK_SETTINGS, NETWORK_SETUP_WIZARD, NETWORK_STACK & MAINLINE_NETWORK_STACK
// are granted a bypass.
@@ -221,7 +189,7 @@
/**
* Retrieves a handle to LocationManager (if not already done) and check if location is enabled.
*/
- public boolean isLocationModeEnabled() {
+ private boolean isLocationModeEnabled() {
final LocationManager LocationManager = mContext.getSystemService(LocationManager.class);
try {
return LocationManager.isLocationEnabledForUser(UserHandle.of(
@@ -278,7 +246,7 @@
/**
* Returns true if the |uid| holds NETWORK_SETTINGS permission.
*/
- public boolean checkNetworkSettingsPermission(int uid) {
+ private boolean checkNetworkSettingsPermission(int uid) {
return getUidPermission(android.Manifest.permission.NETWORK_SETTINGS, uid)
== PackageManager.PERMISSION_GRANTED;
}
@@ -286,7 +254,7 @@
/**
* Returns true if the |uid| holds NETWORK_SETUP_WIZARD permission.
*/
- public boolean checkNetworkSetupWizardPermission(int uid) {
+ private boolean checkNetworkSetupWizardPermission(int uid) {
return getUidPermission(android.Manifest.permission.NETWORK_SETUP_WIZARD, uid)
== PackageManager.PERMISSION_GRANTED;
}
@@ -294,7 +262,7 @@
/**
* Returns true if the |uid| holds NETWORK_STACK permission.
*/
- public boolean checkNetworkStackPermission(int uid) {
+ private boolean checkNetworkStackPermission(int uid) {
return getUidPermission(android.Manifest.permission.NETWORK_STACK, uid)
== PackageManager.PERMISSION_GRANTED;
}
@@ -302,7 +270,7 @@
/**
* Returns true if the |uid| holds MAINLINE_NETWORK_STACK permission.
*/
- public boolean checkMainlineNetworkStackPermission(int uid) {
+ private boolean checkMainlineNetworkStackPermission(int uid) {
return getUidPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, uid)
== PackageManager.PERMISSION_GRANTED;
}
diff --git a/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
new file mode 100644
index 0000000..80088b9
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
@@ -0,0 +1,149 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.LruCache;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Clock;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * An LRU cache that stores key-value pairs with an expiry time.
+ *
+ * <p>This cache uses an {@link LruCache} to store entries and evicts the least
+ * recently used entries when the cache reaches its maximum capacity. It also
+ * supports an expiry time for each entry, allowing entries to be automatically
+ * removed from the cache after a certain duration.
+ *
+ * @param <K> The type of keys used to identify cached entries.
+ * @param <V> The type of values stored in the cache.
+ *
+ * @hide
+ */
+public class LruCacheWithExpiry<K, V> {
+ private final Clock mClock;
+ private final long mExpiryDurationMs;
+ @GuardedBy("mMap")
+ private final LruCache<K, CacheValue<V>> mMap;
+ private final Predicate<V> mShouldCacheValue;
+
+ /**
+ * Constructs a new {@link LruCacheWithExpiry} with the specified parameters.
+ *
+ * @param clock The {@link Clock} to use for determining timestamps.
+ * @param expiryDurationMs The expiry duration for cached entries in milliseconds.
+ * @param maxSize The maximum number of entries to hold in the cache.
+ * @param shouldCacheValue A {@link Predicate} that determines whether a given value should be
+ * cached. This can be used to filter out certain values from being
+ * stored in the cache.
+ */
+ public LruCacheWithExpiry(@NonNull Clock clock, long expiryDurationMs, int maxSize,
+ Predicate<V> shouldCacheValue) {
+ mClock = clock;
+ mExpiryDurationMs = expiryDurationMs;
+ mMap = new LruCache<>(maxSize);
+ mShouldCacheValue = shouldCacheValue;
+ }
+
+ /**
+ * Retrieves a value from the cache, associated with the given key.
+ *
+ * @param key The key to look up in the cache.
+ * @return The cached value, or {@code null} if not found or expired.
+ */
+ @Nullable
+ public V get(@NonNull K key) {
+ synchronized (mMap) {
+ final CacheValue<V> value = mMap.get(key);
+ if (value != null && !isExpired(value.timestamp)) {
+ return value.entry;
+ } else {
+ mMap.remove(key); // Remove expired entries
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Retrieves a value from the cache, associated with the given key.
+ * If the entry is not found in the cache or has expired, computes it using the provided
+ * {@code supplier} and stores the result in the cache.
+ *
+ * @param key The key to look up in the cache.
+ * @param supplier The {@link Supplier} to compute the value if not found or expired.
+ * @return The cached or computed value, or {@code null} if the {@code supplier} returns null.
+ */
+ @Nullable
+ public V getOrCompute(@NonNull K key, @NonNull Supplier<V> supplier) {
+ synchronized (mMap) {
+ final V cachedValue = get(key);
+ if (cachedValue != null) {
+ return cachedValue;
+ }
+
+ // Entry not found or expired, compute it
+ final V computedValue = supplier.get();
+ if (computedValue != null && mShouldCacheValue.test(computedValue)) {
+ put(key, computedValue);
+ }
+ return computedValue;
+ }
+ }
+
+ /**
+ * Stores a value in the cache, associated with the given key.
+ *
+ * @param key The key to associate with the value.
+ * @param value The value to store in the cache.
+ */
+ public void put(@NonNull K key, @NonNull V value) {
+ Objects.requireNonNull(value);
+ synchronized (mMap) {
+ mMap.put(key, new CacheValue<>(mClock.millis(), value));
+ }
+ }
+
+ /**
+ * Clear the cache.
+ */
+ public void clear() {
+ synchronized (mMap) {
+ mMap.evictAll();
+ }
+ }
+
+ private boolean isExpired(long timestamp) {
+ return mClock.millis() > timestamp + mExpiryDurationMs;
+ }
+
+ private static class CacheValue<V> {
+ public final long timestamp;
+ @NonNull
+ public final V entry;
+
+ CacheValue(long timestamp, V entry) {
+ this.timestamp = timestamp;
+ this.entry = entry;
+ }
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index f1ff2e4..5d588cc 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -100,6 +100,15 @@
public static final int IPV4_ADDR_LEN = 4;
public static final int IPV4_FLAG_MF = 0x2000;
public static final int IPV4_FLAG_DF = 0x4000;
+ public static final int IPV4_PROTOCOL_IGMP = 2;
+ public static final int IPV4_IGMP_MIN_SIZE = 8;
+ public static final int IPV4_IGMP_GROUP_RECORD_SIZE = 8;
+ public static final int IPV4_IGMP_TYPE_V1_REPORT = 0x12;
+ public static final int IPV4_IGMP_TYPE_V2_JOIN_REPORT = 0x16;
+ public static final int IPV4_IGMP_TYPE_V2_LEAVE_REPORT = 0x17;
+ public static final int IPV4_IGMP_TYPE_V3_REPORT = 0x22;
+ public static final int IPV4_OPTION_TYPE_ROUTER_ALERT = 0x94;
+ public static final int IPV4_OPTION_LEN_ROUTER_ALERT = 4;
// getSockOpt() for v4 MTU
public static final int IP_MTU = 14;
public static final Inet4Address IPV4_ADDR_ALL = makeInet4Address(
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
index 41a9428..28ff770 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
@@ -16,23 +16,12 @@
package com.android.net.module.util;
-import android.app.usage.NetworkStats;
-
-import com.android.internal.annotations.VisibleForTesting;
-
/**
* Various utilities used for NetworkStats related code.
*
* @hide
*/
public class NetworkStatsUtils {
- // These constants must be synced with the definition in android.net.NetworkStats.
- // TODO: update to formal APIs once all downstreams have these APIs.
- private static final int SET_ALL = -1;
- private static final int METERED_ALL = -1;
- private static final int ROAMING_ALL = -1;
- private static final int DEFAULT_NETWORK_ALL = -1;
-
/**
* Safely multiple a value by a rational.
* <p>
@@ -99,77 +88,4 @@
if (low > high) throw new IllegalArgumentException("low(" + low + ") > high(" + high + ")");
return amount < low ? low : (amount > high ? high : amount);
}
-
- /**
- * Convert structure from android.app.usage.NetworkStats to android.net.NetworkStats.
- */
- public static android.net.NetworkStats fromPublicNetworkStats(
- NetworkStats publiceNetworkStats) {
- android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);
- while (publiceNetworkStats.hasNextBucket()) {
- NetworkStats.Bucket bucket = new NetworkStats.Bucket();
- publiceNetworkStats.getNextBucket(bucket);
- final android.net.NetworkStats.Entry entry = fromBucket(bucket);
- stats = stats.addEntry(entry);
- }
- return stats;
- }
-
- @VisibleForTesting
- public static android.net.NetworkStats.Entry fromBucket(NetworkStats.Bucket bucket) {
- return new android.net.NetworkStats.Entry(
- null /* IFACE_ALL */, bucket.getUid(), convertBucketState(bucket.getState()),
- convertBucketTag(bucket.getTag()), convertBucketMetered(bucket.getMetered()),
- convertBucketRoaming(bucket.getRoaming()),
- convertBucketDefaultNetworkStatus(bucket.getDefaultNetworkStatus()),
- bucket.getRxBytes(), bucket.getRxPackets(),
- bucket.getTxBytes(), bucket.getTxPackets(), 0 /* operations */);
- }
-
- private static int convertBucketState(int networkStatsSet) {
- switch (networkStatsSet) {
- case NetworkStats.Bucket.STATE_ALL: return SET_ALL;
- case NetworkStats.Bucket.STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT;
- case NetworkStats.Bucket.STATE_FOREGROUND:
- return android.net.NetworkStats.SET_FOREGROUND;
- }
- return 0;
- }
-
- private static int convertBucketTag(int tag) {
- switch (tag) {
- case NetworkStats.Bucket.TAG_NONE: return android.net.NetworkStats.TAG_NONE;
- }
- return tag;
- }
-
- private static int convertBucketMetered(int metered) {
- switch (metered) {
- case NetworkStats.Bucket.METERED_ALL: return METERED_ALL;
- case NetworkStats.Bucket.METERED_NO: return android.net.NetworkStats.METERED_NO;
- case NetworkStats.Bucket.METERED_YES: return android.net.NetworkStats.METERED_YES;
- }
- return 0;
- }
-
- private static int convertBucketRoaming(int roaming) {
- switch (roaming) {
- case NetworkStats.Bucket.ROAMING_ALL: return ROAMING_ALL;
- case NetworkStats.Bucket.ROAMING_NO: return android.net.NetworkStats.ROAMING_NO;
- case NetworkStats.Bucket.ROAMING_YES: return android.net.NetworkStats.ROAMING_YES;
- }
- return 0;
- }
-
- private static int convertBucketDefaultNetworkStatus(int defaultNetworkStatus) {
- switch (defaultNetworkStatus) {
- case NetworkStats.Bucket.DEFAULT_NETWORK_ALL:
- return DEFAULT_NETWORK_ALL;
- case NetworkStats.Bucket.DEFAULT_NETWORK_NO:
- return android.net.NetworkStats.DEFAULT_NETWORK_NO;
- case NetworkStats.Bucket.DEFAULT_NETWORK_YES:
- return android.net.NetworkStats.DEFAULT_NETWORK_YES;
- }
- return 0;
- }
}
diff --git a/staticlibs/framework/com/android/net/module/util/PerUidCounter.java b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
index 463b0c4..98d91a5 100644
--- a/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
+++ b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
@@ -87,7 +87,9 @@
}
}
- @VisibleForTesting
+ /**
+ * Get the current counter value for the given uid.
+ */
public synchronized int get(int uid) {
return mUidToCount.get(uid, 0);
}
diff --git a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
index 0d7d96f..0fa91d5 100644
--- a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
@@ -192,6 +192,8 @@
/**
* Enforces that the given package name belongs to the given uid.
+ * Note: b/377758490 - Figure out how to correct this to avoid mis-usage.
+ * Meanwhile, avoid calling this method from the networkstack.
*
* @param context {@link android.content.Context} for the process.
* @param uid User ID to check the package ownership for.
diff --git a/staticlibs/native/README.md b/staticlibs/native/README.md
index 1f505c4..7e0e963 100644
--- a/staticlibs/native/README.md
+++ b/staticlibs/native/README.md
@@ -27,4 +27,4 @@
library (`.so`) file, and different versions of the library loaded in the same process by
different modules will in general have different versions. It's important that each of these
libraries loads the common function from its own library. Static linkage should guarantee this
- because static linkage resolves symbols at build time, not runtime.
\ No newline at end of file
+ because static linkage resolves symbols at build time, not runtime.
diff --git a/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
index 969ebd4..9a58a93 100644
--- a/staticlibs/native/bpfmapjni/Android.bp
+++ b/staticlibs/native/bpfmapjni/Android.bp
@@ -26,6 +26,7 @@
header_libs: [
"bpf_headers",
"jni_headers",
+ "libbase_headers",
],
shared_libs: [
"liblog",
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
index 1923ceb..d862f6b 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -24,37 +24,38 @@
#include "nativehelper/scoped_primitive_array.h"
#include "nativehelper/scoped_utf_chars.h"
-#define BPF_FD_JUST_USE_INT
+#include <android-base/unique_fd.h>
#include "BpfSyscallWrappers.h"
-
#include "bpf/KernelUtils.h"
namespace android {
+using ::android::base::unique_fd;
+
static jint com_android_net_module_util_BpfMap_nativeBpfFdGet(JNIEnv *env, jclass clazz,
jstring path, jint mode, jint keySize, jint valueSize) {
ScopedUtfChars pathname(env, path);
- jint fd = -1;
+ unique_fd fd;
switch (mode) {
case 0:
- fd = bpf::mapRetrieveRW(pathname.c_str());
+ fd.reset(bpf::mapRetrieveRW(pathname.c_str()));
break;
case BPF_F_RDONLY:
- fd = bpf::mapRetrieveRO(pathname.c_str());
+ fd.reset(bpf::mapRetrieveRO(pathname.c_str()));
break;
case BPF_F_WRONLY:
- fd = bpf::mapRetrieveWO(pathname.c_str());
+ fd.reset(bpf::mapRetrieveWO(pathname.c_str()));
break;
case BPF_F_RDONLY|BPF_F_WRONLY:
- fd = bpf::mapRetrieveExclusiveRW(pathname.c_str());
+ fd.reset(bpf::mapRetrieveExclusiveRW(pathname.c_str()));
break;
default:
errno = EINVAL;
break;
}
- if (fd < 0) {
+ if (!fd.ok()) {
jniThrowErrnoException(env, "nativeBpfFdGet", errno);
return -1;
}
@@ -62,18 +63,16 @@
if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
// These likely fail with -1 and set errno to EINVAL on <4.14
if (bpf::bpfGetFdKeySize(fd) != keySize) {
- close(fd);
jniThrowErrnoException(env, "nativeBpfFdGet KeySize", EBADFD);
return -1;
}
if (bpf::bpfGetFdValueSize(fd) != valueSize) {
- close(fd);
jniThrowErrnoException(env, "nativeBpfFdGet ValueSize", EBADFD);
return -1;
}
}
- return fd;
+ return fd.release();
}
static void com_android_net_module_util_BpfMap_nativeWriteToMapEntry(JNIEnv *env, jobject self,
diff --git a/staticlibs/native/timerfdutils/Android.bp b/staticlibs/native/timerfdutils/Android.bp
new file mode 100644
index 0000000..939a2d2
--- /dev/null
+++ b/staticlibs/native/timerfdutils/Android.bp
@@ -0,0 +1,46 @@
+// 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_core_networking",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+ name: "libnet_utils_device_common_timerfdjni",
+ srcs: [
+ "com_android_net_module_util_TimerFdUtils.cpp",
+ ],
+ header_libs: [
+ "jni_headers",
+ ],
+ shared_libs: [
+ "liblog",
+ "libnativehelper_compat_libc++",
+ ],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ ],
+ sdk_version: "current",
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+ visibility: [
+ "//packages/modules/Connectivity:__subpackages__",
+ ],
+}
diff --git a/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp b/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
new file mode 100644
index 0000000..c4c960d
--- /dev/null
+++ b/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+#include <errno.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/scoped_utf_chars.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <sys/timerfd.h>
+#include <time.h>
+#include <unistd.h>
+
+#define MSEC_PER_SEC 1000
+#define NSEC_PER_MSEC 1000000
+
+namespace android {
+
+static jint
+com_android_net_module_util_TimerFdUtils_createTimerFd(JNIEnv *env,
+ jclass clazz) {
+ int tfd;
+ tfd = timerfd_create(CLOCK_BOOTTIME, 0);
+ if (tfd == -1) {
+ jniThrowErrnoException(env, "createTimerFd", tfd);
+ }
+ return tfd;
+}
+
+static void
+com_android_net_module_util_TimerFdUtils_setTime(JNIEnv *env, jclass clazz,
+ jint tfd, jlong milliseconds) {
+ struct itimerspec new_value;
+ new_value.it_value.tv_sec = milliseconds / MSEC_PER_SEC;
+ new_value.it_value.tv_nsec = (milliseconds % MSEC_PER_SEC) * NSEC_PER_MSEC;
+ // Set the interval time to 0 because it's designed for repeated timer expirations after the
+ // initial expiration, which doesn't fit the current usage.
+ new_value.it_interval.tv_sec = 0;
+ new_value.it_interval.tv_nsec = 0;
+
+ int ret = timerfd_settime(tfd, 0, &new_value, NULL);
+ if (ret == -1) {
+ jniThrowErrnoException(env, "setTime", ret);
+ }
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"createTimerFd", "()I",
+ (void *)com_android_net_module_util_TimerFdUtils_createTimerFd},
+ {"setTime", "(IJ)V",
+ (void *)com_android_net_module_util_TimerFdUtils_setTime},
+};
+
+int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+ char const *class_name) {
+ return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 91f94b5..8c54e6a 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -30,8 +30,8 @@
"net-utils-service-connectivity",
],
libs: [
- "android.test.runner",
- "android.test.base",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
],
visibility: [
"//frameworks/base/packages/Tethering/tests/integration",
@@ -70,6 +70,7 @@
],
main: "host/python/run_tests.py",
libs: [
+ "absl-py",
"mobly",
"net-tests-utils-host-python-common",
],
@@ -81,10 +82,4 @@
test_options: {
unit_test: false,
},
- // Needed for applying VirtualEnv.
- version: {
- py3: {
- embedded_launcher: false,
- },
- },
}
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index caaf959..419b338 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -25,12 +25,21 @@
get_apf_capabilities,
get_apf_counter,
get_apf_counters_from_dumpsys,
+ get_ipv4_addresses,
+ get_ipv6_addresses,
get_hardware_address,
- send_broadcast_empty_ethercat_packet,
+ is_send_raw_packet_downstream_supported,
+ is_packet_capture_supported,
+ start_capture_packets,
+ stop_capture_packets,
+ get_matched_packet_counts,
send_raw_packet_downstream,
)
from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
+TEST_IFACE_NAME = "eth0"
+TEST_PACKET_IN_HEX = "AABBCCDDEEFF"
+
class TestApfUtils(base_test.BaseTestClass, parameterized.TestCase):
@@ -108,30 +117,67 @@
with asserts.assert_raises(PatternNotFoundException):
get_hardware_address(self.mock_ad, "wlan0")
- @patch("net_tests_utils.host.python.apf_utils.get_hardware_address")
- @patch("net_tests_utils.host.python.apf_utils.send_raw_packet_downstream")
- def test_send_broadcast_empty_ethercat_packet(
- self,
- mock_send_raw_packet_downstream: MagicMock,
- mock_get_hardware_address: MagicMock,
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_ipv4_addresses_success(
+ self, mock_adb_shell: MagicMock
) -> None:
- mock_get_hardware_address.return_value = "12:34:56:78:90:AB"
- send_broadcast_empty_ethercat_packet(self.mock_ad, "eth0")
- # Assuming you'll mock the packet construction part, verify calls to send_raw_packet_downstream.
- mock_send_raw_packet_downstream.assert_called_once()
+ mock_adb_shell.return_value = """
+54: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
+ inet 192.168.195.162/24 brd 192.168.195.255 scope global wlan0
+ valid_lft forever preferred_lft forever
+ inet 192.168.200.1/24 brd 192.168.200.255 scope global wlan0
+ valid_lft forever preferred_lft forever
+"""
+ ip_addresses = get_ipv4_addresses(self.mock_ad, "wlan0")
+ asserts.assert_equal(ip_addresses, ["192.168.195.162", "192.168.200.1"])
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_ipv4_addresses_not_found(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = ""
+ ip_addresses = get_ipv4_addresses(self.mock_ad, "wlan0")
+ asserts.assert_equal(ip_addresses, [])
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_ipv6_addresses_success(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = """
+54: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
+ inet6 fe80::10a3:5dff:fe52:de32/64 scope link
+ valid_lft forever preferred_lft forever
+ inet6 2001:b400:e53f:164e:9c1e:780e:d1:4658/64 scope global dynamic mngtmpaddr noprefixroute
+ valid_lft 6995sec preferred_lft 6995sec
+ inet6 fe80::3aff:2199:2d8e:20d1/64 scope link noprefixroute
+ valid_lft forever preferred_lft forever
+"""
+ ip_addresses = get_ipv6_addresses(self.mock_ad, "wlan0")
+ asserts.assert_equal(ip_addresses,
+ ["fe80::10a3:5dff:fe52:de32",
+ "2001:b400:e53f:164e:9c1e:780e:d1:4658",
+ "fe80::3aff:2199:2d8e:20d1"])
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_ipv6_address_not_found(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = ""
+ ip_addresses = get_ipv6_addresses(self.mock_ad, "wlan0")
+ asserts.assert_equal(ip_addresses, [])
@patch("net_tests_utils.host.python.adb_utils.adb_shell")
def test_send_raw_packet_downstream_success(
self, mock_adb_shell: MagicMock
) -> None:
mock_adb_shell.return_value = "" # Successful command output
- iface_name = "eth0"
- packet_in_hex = "AABBCCDDEEFF"
- send_raw_packet_downstream(self.mock_ad, iface_name, packet_in_hex)
+ send_raw_packet_downstream(
+ self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+ )
mock_adb_shell.assert_called_once_with(
self.mock_ad,
"cmd network_stack send-raw-packet-downstream"
- f" {iface_name} {packet_in_hex}",
+ f" {TEST_IFACE_NAME} {TEST_PACKET_IN_HEX}",
)
@patch("net_tests_utils.host.python.adb_utils.adb_shell")
@@ -142,7 +188,13 @@
"Any Unexpected Output"
)
with asserts.assert_raises(UnexpectedBehaviorError):
- send_raw_packet_downstream(self.mock_ad, "eth0", "AABBCCDDEEFF")
+ send_raw_packet_downstream(
+ self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+ )
+ asserts.assert_true(
+ is_send_raw_packet_downstream_supported(self.mock_ad),
+ "Send raw packet should be supported.",
+ )
@patch("net_tests_utils.host.python.adb_utils.adb_shell")
def test_send_raw_packet_downstream_unsupported(
@@ -152,7 +204,151 @@
cmd="", stdout="Unknown command", stderr="", ret_code=3
)
with asserts.assert_raises(UnsupportedOperationException):
- send_raw_packet_downstream(self.mock_ad, "eth0", "AABBCCDDEEFF")
+ send_raw_packet_downstream(
+ self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+ )
+ asserts.assert_false(
+ is_send_raw_packet_downstream_supported(self.mock_ad),
+ "Send raw packet should not be supported.",
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_start_capture_success(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = "success" # Successful command output
+ start_capture_packets(
+ self.mock_ad, TEST_IFACE_NAME
+ )
+ mock_adb_shell.assert_called_once_with(
+ self.mock_ad,
+ "cmd network_stack capture start"
+ f" {TEST_IFACE_NAME}"
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_start_capture_failure(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = ( # Unexpected command output
+ "Any Unexpected Output"
+ )
+ with asserts.assert_raises(UnexpectedBehaviorError):
+ start_capture_packets(
+ self.mock_ad, TEST_IFACE_NAME
+ )
+ asserts.assert_true(
+ is_packet_capture_supported(self.mock_ad),
+ "Start capturing packets should be supported.",
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_start_capture_unsupported(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.side_effect = AdbError(
+ cmd="", stdout="Unknown command", stderr="", ret_code=3
+ )
+ with asserts.assert_raises(UnsupportedOperationException):
+ start_capture_packets(
+ self.mock_ad, TEST_IFACE_NAME
+ )
+ asserts.assert_false(
+ is_packet_capture_supported(self.mock_ad),
+ "Start capturing packets should not be supported.",
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_stop_capture_success(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = "success" # Successful command output
+ stop_capture_packets(
+ self.mock_ad, TEST_IFACE_NAME
+ )
+ mock_adb_shell.assert_called_once_with(
+ self.mock_ad,
+ "cmd network_stack capture stop"
+ f" {TEST_IFACE_NAME}"
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_stop_capture_failure(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = ( # Unexpected command output
+ "Any Unexpected Output"
+ )
+ with asserts.assert_raises(UnexpectedBehaviorError):
+ stop_capture_packets(
+ self.mock_ad, TEST_IFACE_NAME
+ )
+ asserts.assert_true(
+ is_packet_capture_supported(self.mock_ad),
+ "Stop capturing packets should be supported.",
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_stop_capture_unsupported(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.side_effect = AdbError(
+ cmd="", stdout="Unknown command", stderr="", ret_code=3
+ )
+ with asserts.assert_raises(UnsupportedOperationException):
+ stop_capture_packets(
+ self.mock_ad, TEST_IFACE_NAME
+ )
+ asserts.assert_false(
+ is_packet_capture_supported(self.mock_ad),
+ "Stop capturing packets should not be supported.",
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_matched_packet_counts_success(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = "10" # Successful command output
+ get_matched_packet_counts(
+ self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+ )
+ mock_adb_shell.assert_called_once_with(
+ self.mock_ad,
+ "cmd network_stack capture matched-packet-counts"
+ f" {TEST_IFACE_NAME} {TEST_PACKET_IN_HEX}"
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_matched_packet_counts_failure(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.return_value = ( # Unexpected command output
+ "Any Unexpected Output"
+ )
+ with asserts.assert_raises(UnexpectedBehaviorError):
+ get_matched_packet_counts(
+ self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+ )
+ asserts.assert_true(
+ is_packet_capture_supported(self.mock_ad),
+ "Get matched packet counts should be supported.",
+ )
+
+ @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+ def test_get_matched_packet_counts_unsupported(
+ self, mock_adb_shell: MagicMock
+ ) -> None:
+ mock_adb_shell.side_effect = AdbError(
+ cmd="", stdout="Unknown command", stderr="", ret_code=3
+ )
+ with asserts.assert_raises(UnsupportedOperationException):
+ get_matched_packet_counts(
+ self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+ )
+ asserts.assert_false(
+ is_packet_capture_supported(self.mock_ad),
+ "Get matched packet counts should not be supported.",
+ )
@parameterized.parameters(
("2,2048,1", ApfCapabilities(2, 2048, 1)), # Valid input
diff --git a/staticlibs/tests/unit/host/python/assert_utils_test.py b/staticlibs/tests/unit/host/python/assert_utils_test.py
index 7a33373..1d85a12 100644
--- a/staticlibs/tests/unit/host/python/assert_utils_test.py
+++ b/staticlibs/tests/unit/host/python/assert_utils_test.py
@@ -14,7 +14,9 @@
from mobly import asserts
from mobly import base_test
-from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError, expect_with_retry
+from net_tests_utils.host.python.assert_utils import (
+ UnexpectedBehaviorError, UnexpectedExceptionError, expect_with_retry, expect_throws
+)
class TestAssertUtils(base_test.BaseTestClass):
@@ -92,3 +94,22 @@
retry_interval_sec=0,
)
asserts.assert_true(retry_action_called, "retry_action not called.")
+
+ def test_expect_exception_throws(self):
+ def raise_unexpected_behavior_error():
+ raise UnexpectedBehaviorError()
+
+ expect_throws(raise_unexpected_behavior_error, UnexpectedBehaviorError)
+
+ def test_unexpect_exception_throws(self):
+ def raise_value_error():
+ raise ValueError()
+
+ with asserts.assert_raises(UnexpectedExceptionError):
+ expect_throws(raise_value_error, UnexpectedBehaviorError)
+
+ def test_no_exception_throws(self):
+ def raise_no_error():
+ return
+
+ expect_throws(raise_no_error, UnexpectedBehaviorError)
\ No newline at end of file
diff --git a/staticlibs/tests/unit/host/python/packet_utils_test.py b/staticlibs/tests/unit/host/python/packet_utils_test.py
new file mode 100644
index 0000000..8ad9576
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/packet_utils_test.py
@@ -0,0 +1,72 @@
+# 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.
+
+from mobly import asserts
+from mobly import base_test
+from net_tests_utils.host.python import packet_utils
+
+class TestPacketUtils(base_test.BaseTestClass):
+ def test_unicast_arp_request(self):
+ # Using scapy to generate unicast arp request packet:
+ # eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+ # arp = ARP(op=1, pdst="192.168.1.1", hwsrc="00:01:02:03:04:05", psrc="192.168.1.2")
+ # pkt = eth/arp
+ expect_arp_request = """
+ 01020304050600010203040508060001080006040001000102030405c0a80102000000000000c0a80101
+ """.upper().replace(" ", "").replace("\n", "")
+ arp_request = packet_utils.construct_arp_packet(
+ src_mac="00:01:02:03:04:05",
+ dst_mac="01:02:03:04:05:06",
+ src_ip="192.168.1.2",
+ dst_ip="192.168.1.1",
+ op=packet_utils.ARP_REQUEST_OP
+ )
+ asserts.assert_equal(expect_arp_request, arp_request)
+
+ def test_broadcast_arp_request(self):
+ # Using scapy to generate unicast arp request packet:
+ # eth = Ether(src="00:01:02:03:04:05", dst="FF:FF:FF:FF:FF:FF")
+ # arp = ARP(op=1, pdst="192.168.1.1", hwsrc="00:01:02:03:04:05", psrc="192.168.1.2")
+ # pkt = eth/arp
+ expect_arp_request = """
+ ffffffffffff00010203040508060001080006040001000102030405c0a80102000000000000c0a80101
+ """.upper().replace(" ", "").replace("\n", "")
+ arp_request = packet_utils.construct_arp_packet(
+ src_mac="00:01:02:03:04:05",
+ dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
+ src_ip="192.168.1.2",
+ dst_ip="192.168.1.1",
+ op=packet_utils.ARP_REQUEST_OP
+ )
+ asserts.assert_equal(expect_arp_request, arp_request)
+
+ def test_arp_reply(self):
+ # Using scapy to generate unicast arp request packet:
+ # eth = Ether(src="01:02:03:04:05:06", dst="00:01:02:03:04:05")
+ # arp = ARP(op=2, pdst="192.168.1.2", \
+ # hwsrc="01:02:03:04:05:06", \
+ # psrc="192.168.1.1", \
+ # hwdst="00:01:02:03:04:05")
+ # pkt = eth/arp
+ expect_arp_reply = """
+ 00010203040501020304050608060001080006040002010203040506c0a80101000102030405c0a80102
+ """.upper().replace(" ", "").replace("\n", "")
+ arp_reply = packet_utils.construct_arp_packet(
+ src_mac="01:02:03:04:05:06",
+ dst_mac="00:01:02:03:04:05",
+ src_ip="192.168.1.1",
+ dst_ip="192.168.1.2",
+ op=packet_utils.ARP_REPLY_OP
+ )
+ asserts.assert_equal(expect_arp_reply, arp_reply)
diff --git a/staticlibs/tests/unit/host/python/run_tests.py b/staticlibs/tests/unit/host/python/run_tests.py
index fa6a310..498dbaf 100644
--- a/staticlibs/tests/unit/host/python/run_tests.py
+++ b/staticlibs/tests/unit/host/python/run_tests.py
@@ -18,6 +18,7 @@
from host.python.adb_utils_test import TestAdbUtils
from host.python.apf_utils_test import TestApfUtils
from host.python.assert_utils_test import TestAssertUtils
+from host.python.packet_utils_test import TestPacketUtils
from mobly import suite_runner
@@ -31,5 +32,5 @@
sys.argv.pop(1)
# TODO: make the tests can be executed without manually list classes.
suite_runner.run_suite(
- [TestAssertUtils, TestAdbUtils, TestApfUtils], sys.argv
+ [TestAssertUtils, TestAdbUtils, TestApfUtils, TestPacketUtils], sys.argv
)
diff --git a/staticlibs/tests/unit/host/python/test_config.xml b/staticlibs/tests/unit/host/python/test_config.xml
index d3b200a..fed9d11 100644
--- a/staticlibs/tests/unit/host/python/test_config.xml
+++ b/staticlibs/tests/unit/host/python/test_config.xml
@@ -14,9 +14,6 @@
limitations under the License.
-->
<configuration description="Config for NetworkStaticLibHostPythonTests">
- <target_preparer class="com.android.tradefed.targetprep.PythonVirtualenvPreparer">
- <option name="dep-module" value="absl-py" />
- </target_preparer>
<test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest" >
<option name="mobly-par-file-name" value="NetworkStaticLibHostPythonTests" />
<option name="mobly-test-timeout" value="3m" />
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
index 851d09a..bde55c3 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
@@ -17,14 +17,19 @@
package com.android.net.module.util
import android.util.Log
+import com.android.testutils.TryTestConfig
import com.android.testutils.tryTest
+import java.util.function.Consumer
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertTrue
-import kotlin.test.fail
private val TAG = CleanupTest::class.simpleName
@@ -34,6 +39,18 @@
class TestException2 : Exception()
class TestException3 : Exception()
+ private var originalDiagnosticsCollector: Consumer<Throwable>? = null
+
+ @Before
+ fun setUp() {
+ originalDiagnosticsCollector = TryTestConfig.swapDiagnosticsCollector(null)
+ }
+
+ @After
+ fun tearDown() {
+ TryTestConfig.swapDiagnosticsCollector(originalDiagnosticsCollector)
+ }
+
@Test
fun testNotThrow() {
var x = 1
@@ -220,4 +237,74 @@
assertTrue(thrown.suppressedExceptions[1] is TestException3)
assert(x == 7)
}
+
+ @Test
+ fun testNoErrorReportingWhenCaught() {
+ var error: Throwable? = null
+ TryTestConfig.swapDiagnosticsCollector {
+ error = it
+ }
+ var x = 1
+ tryTest {
+ x = 2
+ throw TestException1()
+ x = 3
+ }.catch<TestException1> {
+ x = 4
+ } cleanup {
+ x = 5
+ }
+
+ assertEquals(5, x)
+ assertNull(error)
+ }
+
+ @Test
+ fun testErrorReportingInTry() {
+ var error: Throwable? = null
+ TryTestConfig.swapDiagnosticsCollector {
+ assertNull(error)
+ error = it
+ }
+ var x = 1
+ assertFailsWith<TestException1> {
+ tryTest {
+ x = 2
+ throw TestException1()
+ x = 3
+ } cleanupStep {
+ throw TestException2()
+ x = 4
+ } cleanup {
+ x = 5
+ }
+ }
+
+ assertEquals(5, x)
+ assertTrue(error is TestException1)
+ }
+
+ @Test
+ fun testErrorReportingInCatch() {
+ var error: Throwable? = null
+ TryTestConfig.swapDiagnosticsCollector {
+ assertNull(error)
+ error = it
+ }
+ var x = 1
+ assertFailsWith<TestException2> {
+ tryTest {
+ throw TestException1()
+ x = 2
+ }.catch<TestException1> {
+ throw TestException2()
+ x = 3
+ } cleanup {
+ x = 4
+ }
+ }
+
+ assertEquals(4, x)
+ assertTrue(error is TestException2)
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
index 4ed3afd..7244803 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
@@ -183,17 +183,17 @@
@Test
fun testGetIndexForValue() {
- val sparseArray = SparseArray<String>();
- sparseArray.put(5, "hello");
- sparseArray.put(10, "abcd");
- sparseArray.put(20, null);
+ val sparseArray = SparseArray<String>()
+ sparseArray.put(5, "hello")
+ sparseArray.put(10, "abcd")
+ sparseArray.put(20, null)
- val value1 = "abcd";
+ val value1 = "abcd"
val value1Copy = String(value1.toCharArray())
- val value2 = null;
+ val value2 = null
- assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1));
- assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1Copy));
- assertEquals(2, CollectionUtils.getIndexForValue(sparseArray, value2));
+ assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1))
+ assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1Copy))
+ assertEquals(2, CollectionUtils.getIndexForValue(sparseArray, value2))
}
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index 9fb61d9..a5af09b 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -24,6 +24,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.net.module.util.FeatureVersions.CONNECTIVITY_MODULE_ID;
+import static com.android.net.module.util.FeatureVersions.DNS_RESOLVER_MODULE_ID;
import static com.android.net.module.util.FeatureVersions.NETWORK_STACK_MODULE_ID;
import static org.junit.Assert.assertEquals;
@@ -77,7 +78,9 @@
private static final int TEST_DEFAULT_FLAG_VALUE = 0;
private static final int TEST_MAX_FLAG_VALUE = 1000;
private static final int TEST_MIN_FLAG_VALUE = 100;
- private static final long TEST_PACKAGE_VERSION = 290000000;
+ private static final long TEST_PACKAGE_VERSION = 290500000;
+ private static final long TEST_GO_PACKAGE_VERSION = 290000000; // Not updated
+ private static final long TEST_RESOLV_PACKAGE_VERSION = 290300000; // Updated, but older.
private static final String TEST_PACKAGE_NAME = "test.package.name";
// The APEX name is the name of the APEX module, as in android.content.pm.ModuleInfo, and is
// used for its mount point in /apex. APEX packages are actually APKs with a different
@@ -85,14 +88,18 @@
// that manifest, and is reflected in android.content.pm.ApplicationInfo. Contrary to the APEX
// (module) name, different package names are typically used to identify the organization that
// built and signed the APEX modules.
- private static final String TEST_APEX_PACKAGE_NAME = "com.prefix.android.tethering";
- private static final String TEST_GO_APEX_PACKAGE_NAME = "com.prefix.android.go.tethering";
+ private static final String TEST_TETHERING_PACKAGE_NAME = "com.prefix.android.tethering";
+ private static final String TEST_GO_TETHERING_PACKAGE_NAME = "com.prefix.android.go.tethering";
+ private static final String TEST_RESOLV_PACKAGE_NAME = "com.prefix.android.resolv";
+ private static final String TEST_GO_RESOLV_PACKAGE_NAME = "com.prefix.android.go.resolv";
private static final String TEST_CONNRES_PACKAGE_NAME =
"com.prefix.android.connectivity.resources";
private static final String TEST_NETWORKSTACK_NAME = "com.prefix.android.networkstack";
private static final String TEST_GO_NETWORKSTACK_NAME = "com.prefix.android.go.networkstack";
private final PackageInfo mPackageInfo = new PackageInfo();
- private final PackageInfo mApexPackageInfo = new PackageInfo();
+ private final PackageInfo mGoApexPackageInfo = new PackageInfo();
+ private final PackageInfo mTetheringApexPackageInfo = new PackageInfo();
+ private final PackageInfo mResolvApexPackageInfo = new PackageInfo();
private MockitoSession mSession;
@Mock private Context mContext;
@@ -105,13 +112,22 @@
mSession = mockitoSession().spyStatic(DeviceConfig.class).startMocking();
mPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
- mApexPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+ mTetheringApexPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+ mGoApexPackageInfo.setLongVersionCode(TEST_GO_PACKAGE_VERSION);
+ mResolvApexPackageInfo.setLongVersionCode(TEST_RESOLV_PACKAGE_VERSION);
doReturn(mPm).when(mContext).getPackageManager();
doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(anyString(), anyInt());
doReturn(mPackageInfo).when(mPm).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt());
- doReturn(mApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_APEX_PACKAGE_NAME), anyInt());
+ doReturn(mTetheringApexPackageInfo).when(mPm).getPackageInfo(
+ eq(TEST_TETHERING_PACKAGE_NAME), anyInt());
+ doReturn(mResolvApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_RESOLV_PACKAGE_NAME),
+ anyInt());
+ doReturn(mGoApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_GO_TETHERING_PACKAGE_NAME),
+ anyInt());
+ doReturn(mGoApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_GO_RESOLV_PACKAGE_NAME),
+ anyInt());
doReturn(mResources).when(mContext).getResources();
@@ -342,9 +358,9 @@
@Test
public void testFeatureIsEnabledOnGo() throws Exception {
doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(
- eq(TEST_APEX_PACKAGE_NAME), anyInt());
- doReturn(mApexPackageInfo).when(mPm).getPackageInfo(
- eq(TEST_GO_APEX_PACKAGE_NAME), anyInt());
+ eq(TEST_TETHERING_PACKAGE_NAME), anyInt());
+ doReturn(mTetheringApexPackageInfo).when(mPm).getPackageInfo(
+ eq(TEST_GO_TETHERING_PACKAGE_NAME), anyInt());
doReturn("0").when(() -> DeviceConfig.getProperty(
NAMESPACE_CONNECTIVITY, TEST_EXPERIMENT_FLAG));
doReturn("0").when(() -> DeviceConfig.getProperty(
@@ -483,6 +499,31 @@
mContext, 889900000L + CONNECTIVITY_MODULE_ID));
}
+
+ @Test
+ public void testIsFeatureSupported_resolvFeature() throws Exception {
+ assertTrue(DeviceConfigUtils.isFeatureSupported(
+ mContext, TEST_RESOLV_PACKAGE_VERSION + DNS_RESOLVER_MODULE_ID));
+ // Return false because feature requires a future version.
+ assertFalse(DeviceConfigUtils.isFeatureSupported(
+ mContext, 889900000L + DNS_RESOLVER_MODULE_ID));
+ }
+
+ @Test
+ public void testIsFeatureSupported_goResolvFeature() throws Exception {
+ doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(eq(TEST_RESOLV_PACKAGE_NAME),
+ anyInt());
+ doReturn(mGoApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_GO_RESOLV_PACKAGE_NAME),
+ anyInt());
+ assertFalse(DeviceConfigUtils.isFeatureSupported(
+ mContext, TEST_RESOLV_PACKAGE_VERSION + DNS_RESOLVER_MODULE_ID));
+ assertTrue(DeviceConfigUtils.isFeatureSupported(
+ mContext, TEST_GO_PACKAGE_VERSION + DNS_RESOLVER_MODULE_ID));
+ // Return false because feature requires a future version.
+ assertFalse(DeviceConfigUtils.isFeatureSupported(
+ mContext, 889900000L + DNS_RESOLVER_MODULE_ID));
+ }
+
@Test
public void testIsFeatureSupported_illegalModule() throws Exception {
assertThrows(IllegalArgumentException.class,
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt
new file mode 100644
index 0000000..7b1f08a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.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.net.module.util
+
+import com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase
+import com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase
+import com.android.net.module.util.DnsUtils.toDnsLabelsUpperCase
+import com.android.net.module.util.DnsUtils.toDnsUpperCase
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+class DnsUtilsTest {
+ @Test
+ fun testToDnsUpperCase() {
+ assertEquals("TEST", toDnsUpperCase("TEST"))
+ assertEquals("TEST", toDnsUpperCase("TeSt"))
+ assertEquals("TEST", toDnsUpperCase("test"))
+ assertEquals("TÉST", toDnsUpperCase("TÉST"))
+ assertEquals("ţéST", toDnsUpperCase("ţést"))
+ // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
+ // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
+ assertEquals(
+ "TEST: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+ toDnsUpperCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ")
+ )
+ // Also test some characters where the first surrogate is not \ud800
+ assertEquals(
+ "TEST: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
+ toDnsUpperCase(
+ "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+ )
+ )
+ }
+
+ @Test
+ fun testToDnsLabelsUpperCase() {
+ assertArrayEquals(
+ arrayOf("TEST", "TÉST", "ţéST"),
+ toDnsLabelsUpperCase(arrayOf("TeSt", "TÉST", "ţést"))
+ )
+ }
+
+ @Test
+ fun testEqualsIgnoreDnsCase() {
+ assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
+ assertTrue(equalsIgnoreDnsCase("TEST", "test"))
+ assertTrue(equalsIgnoreDnsCase("test", "TeSt"))
+ assertTrue(equalsIgnoreDnsCase("Tést", "tést"))
+ assertFalse(equalsIgnoreDnsCase("ŢÉST", "ţést"))
+ // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
+ // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
+ assertTrue(equalsIgnoreDnsCase(
+ "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+ "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "
+ ))
+ // Also test some characters where the first surrogate is not \ud800
+ assertTrue(equalsIgnoreDnsCase(
+ "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
+ "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+ ))
+ }
+
+ @Test
+ fun testEqualsLabelIgnoreDnsCase() {
+ assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
+ assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
+ assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
+ assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
index bdcb8c0..4b740e3 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
@@ -109,12 +109,12 @@
}
@Test
- fun testGetShrinkedBackingArray() {
+ fun testGetMinimizedBackingArray() {
val array = GrowingIntArray(10)
array.add(-1)
array.add(2)
- assertContentEquals(intArrayOf(-1, 2), array.shrinkedBackingArray)
+ assertContentEquals(intArrayOf(-1, 2), array.minimizedBackingArray)
}
@Test
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
index f2c902f..845a2c3 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
@@ -19,11 +19,14 @@
import android.os.HandlerThread
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.DevSdkIgnoreRunner.MonitorThreadLeak
+import com.android.testutils.waitForIdle
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
const val THREAD_BLOCK_TIMEOUT_MS = 1000L
const val TEST_REPEAT_COUNT = 100
@@ -52,6 +55,24 @@
}
}
+ @Test
+ fun testIsRunningOnHandlerThread() {
+ assertFalse(HandlerUtils.isRunningOnHandlerThread(handler))
+ handler.post{
+ assertTrue(HandlerUtils.isRunningOnHandlerThread(handler))
+ }
+ handler.waitForIdle(THREAD_BLOCK_TIMEOUT_MS)
+ }
+
+ @Test
+ fun testEnsureRunningOnHandlerThread() {
+ assertFailsWith<IllegalStateException>{ HandlerUtils.ensureRunningOnHandlerThread(handler) }
+ handler.post{
+ HandlerUtils.ensureRunningOnHandlerThread(handler)
+ }
+ handler.waitForIdle(THREAD_BLOCK_TIMEOUT_MS)
+ }
+
@After
fun tearDown() {
handlerThread.quitSafely()
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
index 84018a5..d773374 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
@@ -18,17 +18,17 @@
import static android.Manifest.permission.NETWORK_SETTINGS;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import android.Manifest;
import android.app.AppOpsManager;
@@ -46,7 +46,6 @@
import com.android.testutils.DevSdkIgnoreRule;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -106,17 +105,18 @@
}
private void setupMocks() throws Exception {
- when(mMockPkgMgr.getApplicationInfoAsUser(eq(TEST_PKG_NAME), eq(0), any()))
- .thenReturn(mMockApplInfo);
- when(mMockContext.getPackageManager()).thenReturn(mMockPkgMgr);
- when(mMockAppOps.noteOp(AppOpsManager.OPSTR_WIFI_SCAN, mUid, TEST_PKG_NAME,
- TEST_FEATURE_ID, null)).thenReturn(mWifiScanAllowApps);
- when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_COARSE_LOCATION), eq(mUid),
- eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class)))
- .thenReturn(mAllowCoarseLocationApps);
- when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_FINE_LOCATION), eq(mUid),
- eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class)))
- .thenReturn(mAllowFineLocationApps);
+ doReturn(mMockApplInfo).when(mMockPkgMgr)
+ .getApplicationInfoAsUser(eq(TEST_PKG_NAME), eq(0), any());
+ doReturn(mMockPkgMgr).when(mMockContext).getPackageManager();
+ doReturn(mWifiScanAllowApps).when(mMockAppOps).noteOp(
+ AppOpsManager.OPSTR_WIFI_SCAN, mUid, TEST_PKG_NAME,
+ TEST_FEATURE_ID, null);
+ doReturn(mAllowCoarseLocationApps).when(mMockAppOps).noteOp(
+ eq(AppOpsManager.OPSTR_COARSE_LOCATION), eq(mUid),
+ eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class));
+ doReturn(mAllowFineLocationApps).when(mMockAppOps).noteOp(
+ eq(AppOpsManager.OPSTR_FINE_LOCATION), eq(mUid),
+ eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class));
if (mThrowSecurityException) {
doThrow(new SecurityException("Package " + TEST_PKG_NAME + " doesn't belong"
+ " to application bound to user " + mUid))
@@ -128,10 +128,10 @@
}
private <T> void mockSystemService(String name, Class<T> clazz, T service) {
- when(mMockContext.getSystemService(name)).thenReturn(service);
- when(mMockContext.getSystemServiceName(clazz)).thenReturn(name);
+ doReturn(service).when(mMockContext).getSystemService(name);
+ doReturn(name).when(mMockContext).getSystemServiceName(clazz);
// Do not use mockito extended final method mocking
- when(mMockContext.getSystemService(clazz)).thenCallRealMethod();
+ doCallRealMethod().when(mMockContext).getSystemService(clazz);
}
private void setupTestCase() throws Exception {
@@ -167,16 +167,17 @@
Binder.restoreCallingIdentity((((long) mUid) << 32) | Binder.getCallingPid());
doAnswer(mReturnPermission).when(mMockContext).checkPermission(
anyString(), anyInt(), anyInt());
- when(mMockUserManager.isSameProfileGroup(UserHandle.SYSTEM,
- UserHandle.getUserHandleForUid(MANAGED_PROFILE_UID)))
- .thenReturn(true);
- when(mMockContext.checkPermission(mManifestStringCoarse, -1, mUid))
- .thenReturn(mCoarseLocationPermission);
- when(mMockContext.checkPermission(mManifestStringFine, -1, mUid))
- .thenReturn(mFineLocationPermission);
- when(mMockContext.checkPermission(NETWORK_SETTINGS, -1, mUid))
- .thenReturn(mNetworkSettingsPermission);
- when(mLocationManager.isLocationEnabledForUser(any())).thenReturn(mIsLocationEnabled);
+ doReturn(true).when(mMockUserManager)
+ .isSameProfileGroup(UserHandle.SYSTEM,
+ UserHandle.getUserHandleForUid(MANAGED_PROFILE_UID));
+ doReturn(mCoarseLocationPermission).when(mMockContext)
+ .checkPermission(mManifestStringCoarse, -1, mUid);
+ doReturn(mFineLocationPermission).when(mMockContext)
+ .checkPermission(mManifestStringFine, -1, mUid);
+ doReturn(mNetworkSettingsPermission).when(mMockContext)
+ .checkPermission(NETWORK_SETTINGS, -1, mUid);
+ doReturn(mIsLocationEnabled).when(mLocationManager)
+ .isLocationEnabledForUser(any());
}
private Answer<Integer> createPermissionAnswer() {
@@ -208,7 +209,7 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.SUCCEEDED, result);
}
@@ -225,7 +226,7 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.SUCCEEDED, result);
}
@@ -239,9 +240,9 @@
mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
setupTestCase();
- assertThrows(SecurityException.class,
- () -> mChecker.checkLocationPermissionWithDetailInfo(
- TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
+ final int result = mChecker.checkLocationPermissionInternal(
+ TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+ assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
}
@Test
@@ -251,7 +252,7 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
}
@@ -267,7 +268,7 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
verify(mMockAppOps, never()).noteOp(anyInt(), anyInt(), anyString());
@@ -284,7 +285,7 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.ERROR_LOCATION_MODE_OFF, result);
}
@@ -298,18 +299,8 @@
setupTestCase();
final int result =
- mChecker.checkLocationPermissionWithDetailInfo(
+ mChecker.checkLocationPermissionInternal(
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.SUCCEEDED, result);
}
-
-
- private static void assertThrows(Class<? extends Exception> exceptionClass, Runnable r) {
- try {
- r.run();
- Assert.fail("Expected " + exceptionClass + " to be thrown.");
- } catch (Exception exception) {
- assertTrue(exceptionClass.isInstance(exception));
- }
- }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
index 2785ea9..9981b6a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
@@ -16,16 +16,12 @@
package com.android.net.module.util
-import android.net.NetworkStats
-import android.text.TextUtils
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
-import org.junit.Test
-import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
+import org.junit.Test
+import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@SmallTest
@@ -75,68 +71,4 @@
assertEquals(11, NetworkStatsUtils.constrain(11, 11, 11))
assertEquals(11, NetworkStatsUtils.constrain(1, 11, 11))
}
-
- @Test
- fun testBucketToEntry() {
- val bucket = makeMockBucket(android.app.usage.NetworkStats.Bucket.UID_ALL,
- android.app.usage.NetworkStats.Bucket.TAG_NONE,
- android.app.usage.NetworkStats.Bucket.STATE_DEFAULT,
- android.app.usage.NetworkStats.Bucket.METERED_YES,
- android.app.usage.NetworkStats.Bucket.ROAMING_NO,
- android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12)
- val entry = NetworkStatsUtils.fromBucket(bucket)
- val expectedEntry = NetworkStats.Entry(null /* IFACE_ALL */, NetworkStats.UID_ALL,
- NetworkStats.SET_DEFAULT, NetworkStats.TAG_NONE, NetworkStats.METERED_YES,
- NetworkStats.ROAMING_NO, NetworkStats.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12,
- 0 /* operations */)
-
- // TODO: Use assertEquals once all downstreams accept null iface in
- // NetworkStats.Entry#equals.
- assertEntryEquals(expectedEntry, entry)
- }
-
- private fun makeMockBucket(
- uid: Int,
- tag: Int,
- state: Int,
- metered: Int,
- roaming: Int,
- defaultNetwork: Int,
- rxBytes: Long,
- rxPackets: Long,
- txBytes: Long,
- txPackets: Long
- ): android.app.usage.NetworkStats.Bucket {
- val ret: android.app.usage.NetworkStats.Bucket =
- mock(android.app.usage.NetworkStats.Bucket::class.java)
- doReturn(uid).`when`(ret).getUid()
- doReturn(tag).`when`(ret).getTag()
- doReturn(state).`when`(ret).getState()
- doReturn(metered).`when`(ret).getMetered()
- doReturn(roaming).`when`(ret).getRoaming()
- doReturn(defaultNetwork).`when`(ret).getDefaultNetworkStatus()
- doReturn(rxBytes).`when`(ret).getRxBytes()
- doReturn(rxPackets).`when`(ret).getRxPackets()
- doReturn(txBytes).`when`(ret).getTxBytes()
- doReturn(txPackets).`when`(ret).getTxPackets()
- return ret
- }
-
- /**
- * Assert that the two {@link NetworkStats.Entry} are equals.
- */
- private fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
- TextUtils.equals(left.iface, right.iface)
- assertEquals(left.uid, right.uid)
- assertEquals(left.set, right.set)
- assertEquals(left.tag, right.tag)
- assertEquals(left.metered, right.metered)
- assertEquals(left.roaming, right.roaming)
- assertEquals(left.defaultNetwork, right.defaultNetwork)
- assertEquals(left.rxBytes, right.rxBytes)
- assertEquals(left.rxPackets, right.rxPackets)
- assertEquals(left.txBytes, right.txBytes)
- assertEquals(left.txPackets, right.txPackets)
- assertEquals(left.operations, right.operations)
- }
}
\ No newline at end of file
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
index b04561c..035ce0f 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
@@ -16,7 +16,9 @@
package com.android.net.module.util
+import android.content.Context
import android.net.INetd
+import android.net.Network
import android.os.Build
import android.util.Log
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
@@ -34,7 +36,9 @@
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
class RoutingCoordinatorServiceTest {
val mNetd = mock(INetd::class.java)
- val mService = RoutingCoordinatorService(mNetd)
+ val mGetAllNetworksSupplier = { emptyArray<Network>() }
+ val mContext = mock(Context::class.java)
+ val mService = RoutingCoordinatorService(mNetd, mGetAllNetworksSupplier, mContext)
@Test
fun testInterfaceForward() {
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
index a39b7a3..0c2605f 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
@@ -32,6 +32,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.Struct.Bool;
import com.android.net.module.util.Struct.Field;
import com.android.net.module.util.Struct.Type;
@@ -133,6 +134,29 @@
verifyHeaderParsing(msg);
}
+ @Test
+ public void testBoolStruct() {
+ assertEquals(1, Struct.getSize(Bool.class));
+
+ assertEquals(false, Struct.parse(Bool.class, toByteBuffer("00")).val);
+ assertEquals(true, Struct.parse(Bool.class, toByteBuffer("01")).val);
+ // maybe these should throw instead, but currently only 0 is false...
+ assertEquals(true, Struct.parse(Bool.class, toByteBuffer("02")).val);
+ assertEquals(true, Struct.parse(Bool.class, toByteBuffer("7F")).val);
+ assertEquals(true, Struct.parse(Bool.class, toByteBuffer("80")).val);
+ assertEquals(true, Struct.parse(Bool.class, toByteBuffer("FF")).val);
+
+ final var f = new Bool(false);
+ final var t = new Bool(true);
+ assertEquals(f.val, false);
+ assertEquals(t.val, true);
+
+ assertArrayEquals(toByteBuffer("00").array(), f.writeToBytes(ByteOrder.BIG_ENDIAN));
+ assertArrayEquals(toByteBuffer("00").array(), f.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+ assertArrayEquals(toByteBuffer("01").array(), t.writeToBytes(ByteOrder.BIG_ENDIAN));
+ assertArrayEquals(toByteBuffer("01").array(), t.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+ }
+
public static class HeaderMsgWithoutConstructor extends Struct {
static int sType;
static int sLength;
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index 9db63db..bd0e31d 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -24,24 +24,28 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
import android.net.MacAddress;
import android.system.OsConstants;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.net.module.util.HexDump;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-@RunWith(AndroidJUnit4.class)
+@RunWith(MockitoJUnitRunner.class)
@SmallTest
public class RtNetlinkLinkMessageTest {
+ @Mock
+ private OsAccess mOsAccess;
// An example of the full RTM_NEWLINK message.
private static final String RTM_NEWLINK_HEX =
@@ -124,14 +128,14 @@
}
private static final String RTM_NEWLINK_PACK_HEX =
- "34000000100000000000000000000000" // struct nlmsghr
+ "40000000100000000000000000000000" // struct nlmsghr
+ "000001001E0000000210000000000000" // struct ifinfo
+ "08000400DC050000" // IFLA_MTU
+ "0A00010092C3E3C9374E0000" // IFLA_ADDRESS
+ "0A000300776C616E30000000"; // IFLA_IFNAME(wlan0)
@Test
- public void testPackRtmNewLink() {
+ public void testParseAndPackRtmNewLink() {
final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_PACK_HEX);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
@@ -145,6 +149,21 @@
assertEquals(RTM_NEWLINK_PACK_HEX, HexDump.toHexString(packBuffer.array()));
}
+ @Test
+ public void testPackRtmNewLink() {
+ final RtNetlinkLinkMessage linkMsg = RtNetlinkLinkMessage.build(
+ // nlmsg_len will be updated inside create() method, so it's ok to set 0 here.
+ new StructNlMsgHdr(0 /*nlmsg_len*/, (short) 0x10, (short) 0, 0),
+ new StructIfinfoMsg((byte) 0, (short) 1, 0x1e, 0x1002, 0),
+ 1500,
+ MacAddress.fromString("92:c3:e3:c9:37:4e"),
+ "wlan0");
+ assertNotNull(linkMsg);
+
+ final byte[] packBytes = linkMsg.pack(ByteOrder.LITTLE_ENDIAN);
+ assertEquals(RTM_NEWLINK_PACK_HEX, HexDump.toHexString(packBytes));
+ }
+
private static final String RTM_NEWLINK_TRUNCATED_HEX =
"54000000100000000000000000000000" // struct nlmsghr
+ "000001001E0000000210000000000000" // struct ifinfo
@@ -171,6 +190,122 @@
}
@Test
+ public void testCreateSetLinkUpMessage() {
+ final String expectedHexBytes =
+ "20000000100005006824000000000000" // struct nlmsghdr
+ + "00000000080000000100000001000000"; // struct ifinfomsg
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+ final boolean isUp = true;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetLinkStateMessage(
+ interfaceName, sequenceNumber, isUp, mOsAccess);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
+ public void testCreateSetLinkDownMessage() {
+ final String expectedHexBytes =
+ "20000000100005006824000000000000" // struct nlmsghdr
+ + "00000000080000000000000001000000"; // struct ifinfomsg
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+ final boolean isUp = false;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetLinkStateMessage(
+ interfaceName, sequenceNumber, isUp, mOsAccess);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
+ public void testCreateSetLinkStateMessage_InvalidInterface() {
+ final String interfaceName = "wlan0";
+ final int sequenceNumber = 0x2468;
+ final boolean isUp = false;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(OsAccess.INVALID_INTERFACE_INDEX);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetLinkStateMessage(
+ interfaceName, sequenceNumber, isUp, mOsAccess);
+ assertNull(msg);
+ }
+
+ @Test
+ public void testCreateSetLinkNameMessage() {
+ final String expectedHexBytes =
+ "2C000000100005006824000000000000" // struct nlmsghdr
+ + "00000000080000000000000000000000" // struct ifinfomsg
+ + "0A000300776C616E31000000"; // IFLA_IFNAME(wlan1)
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+ final String newName = "wlan1";
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetLinkNameMessage(
+ interfaceName, sequenceNumber, newName, mOsAccess);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
+ public void testCreateSetLinkNameMessage_InterfaceNotFound() {
+ final String interfaceName = "wlan0";
+ final int sequenceNumber = 0x2468;
+ final String newName = "wlan1";
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(OsAccess.INVALID_INTERFACE_INDEX);
+
+ assertNull(RtNetlinkLinkMessage.createSetLinkNameMessage(
+ interfaceName, sequenceNumber, newName, mOsAccess));
+ }
+
+ @Test
+ public void testCreateSetLinkNameMessage_InvalidNewName() {
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final String[] invalidNames = {"", "interface_name_longer_than_limit"};
+ for (String invalidName : invalidNames) {
+ assertNull(RtNetlinkLinkMessage.createSetLinkNameMessage(
+ interfaceName, sequenceNumber, invalidName, mOsAccess));
+ }
+ }
+
+ @Test
+ public void testCreateGetLinkMessage() {
+ final String expectedHexBytes =
+ "20000000120001006824000000000000" // struct nlmsghdr
+ + "00000000080000000000000000000000"; // struct ifinfomsg
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createGetLinkMessage(
+ interfaceName, sequenceNumber, mOsAccess);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
public void testToString() {
final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
diff --git a/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
new file mode 100644
index 0000000..57920fc
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 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.testutils
+
+import android.net.NetworkStats
+import android.net.NetworkStats.DEFAULT_NETWORK_NO
+import android.net.NetworkStats.METERED_NO
+import android.net.NetworkStats.ROAMING_NO
+import android.net.NetworkStats.SET_DEFAULT
+import android.net.NetworkStats.TAG_NONE
+import android.os.Build
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val TEST_IFACE = "test0"
+private val TEST_IFACE2: String? = null
+private const val TEST_START = 1194220800000L
+
+@RunWith(JUnit4::class)
+class NetworkStatsUtilsTest {
+ // This is a unit test for a test utility that uses R APIs
+ @Rule @JvmField
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
+
+ @Test
+ fun testOrderInsensitiveEquals() {
+ val testEntry = arrayOf(
+ NetworkStats.Entry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+ DEFAULT_NETWORK_NO, 128L, 8L, 0L, 2L, 20L),
+ NetworkStats.Entry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+ DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L)
+ )
+
+ // Verify equals of empty stats regardless of initial capacity.
+ val red = NetworkStats(TEST_START, 0)
+ val blue = NetworkStats(TEST_START, 1)
+ assertTrue(orderInsensitiveEquals(red, blue))
+ assertTrue(orderInsensitiveEquals(blue, red))
+
+ // Verify not equal.
+ red.combineValues(testEntry[1])
+ blue.combineValues(testEntry[0]).combineValues(testEntry[1])
+ assertFalse(orderInsensitiveEquals(red, blue))
+ assertFalse(orderInsensitiveEquals(blue, red))
+
+ // Verify equals even if the order of entries are not the same.
+ red.combineValues(testEntry[0])
+ assertTrue(orderInsensitiveEquals(red, blue))
+ assertTrue(orderInsensitiveEquals(blue, red))
+ }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 440f5ca..86aa8f1 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -33,6 +33,7 @@
],
static_libs: [
"androidx.test.ext.junit",
+ "collector-device-lib",
"kotlin-reflect",
"libnanohttpd",
"net-tests-utils-host-device-common",
@@ -42,7 +43,6 @@
"net-utils-device-common-struct",
"net-utils-device-common-struct-base",
"net-utils-device-common-wear",
- "net-utils-framework-connectivity",
"modules-utils-build_system",
],
lint: {
@@ -100,7 +100,7 @@
"mts-tethering",
"mcts-tethering",
],
- data: [":ConnectivityTestPreparer"],
+ device_common_data: [":ConnectivityTestPreparer"],
}
python_library_host {
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/CarrierConfigSetupTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/CarrierConfigSetupTest.kt
new file mode 100644
index 0000000..78b34a8
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/CarrierConfigSetupTest.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.testutils.connectivitypreparer
+
+import android.Manifest.permission.MODIFY_PHONE_STATE
+import android.Manifest.permission.READ_PHONE_STATE
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager.FEATURE_TELEPHONY_IMS
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.os.Build
+import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+import android.os.ParcelFileDescriptor
+import android.os.PersistableBundle
+import android.telephony.CarrierConfigManager
+import android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
+import android.telephony.SubscriptionManager
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.runAsShell
+import kotlin.test.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val CONFIG_CHANGE_TIMEOUT_MS = 10_000L
+private val TAG = CarrierConfigSetupTest::class.simpleName
+
+@RunWith(AndroidJUnit4::class)
+class CarrierConfigSetupTest {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val pm by lazy { context.packageManager }
+ private val carrierConfigManager by lazy {
+ context.getSystemService(CarrierConfigManager::class.java)
+ }
+
+ @Test
+ fun testSetCarrierConfig() {
+ if (!shouldDisableIwlan()) return
+ overrideAllSubscriptions(PersistableBundle().apply {
+ putBoolean(CarrierConfigManager.KEY_CARRIER_WFC_IMS_AVAILABLE_BOOL, false)
+ })
+ }
+
+ @Test
+ fun testClearCarrierConfig() {
+ // set/clear are in different test runs so it is difficult to share state between them.
+ // The conditions to disable IWLAN should not change over time (in particular
+ // force_iwlan_mms is a readonly flag), so just perform the same check again on teardown.
+ // CarrierConfigManager overrides are cleared on reboot by default anyway, so any missed
+ // cleanup should not be too damaging.
+ if (!shouldDisableIwlan()) return
+ overrideAllSubscriptions(null)
+ }
+
+ private class ConfigChangedReceiver : BroadcastReceiver() {
+ val receivedSubIds = ArrayTrackRecord<Int>()
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != ACTION_CARRIER_CONFIG_CHANGED) return
+ val subIdx = intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, -1)
+ // It is possible this is a configuration change for a different setting, so the test
+ // may not wait for long enough. Unfortunately calling CarrierConfigManager to check
+ // if the config was applied does not help because it will always return the override,
+ // even if it was not applied to the subscription yet.
+ // In practice, it is very unlikely that a different broadcast arrives, and then a test
+ // flakes because of the iwlan behavior in the time it takes for the config to be
+ // applied.
+ Log.d(TAG, "Received config change for sub $subIdx")
+ receivedSubIds.add(subIdx)
+ }
+ }
+
+ private fun overrideAllSubscriptions(bundle: PersistableBundle?) {
+ runAsShell(READ_PHONE_STATE, MODIFY_PHONE_STATE) {
+ val receiver = ConfigChangedReceiver()
+ context.registerReceiver(receiver, IntentFilter(ACTION_CARRIER_CONFIG_CHANGED))
+ val subscriptions = context.getSystemService(SubscriptionManager::class.java)
+ .activeSubscriptionInfoList
+ subscriptions?.forEach { subInfo ->
+ Log.d(TAG, "Overriding config for subscription $subInfo")
+ carrierConfigManager.overrideConfig(subInfo.subscriptionId, bundle)
+ }
+ // Don't wait after each update before the next one, but expect all updates to be done
+ // eventually
+ subscriptions?.forEach { subInfo ->
+ assertNotNull(receiver.receivedSubIds.poll(CONFIG_CHANGE_TIMEOUT_MS, pos = 0) {
+ it == subInfo.subscriptionId
+ }, "Config override broadcast not received for subscription $subInfo")
+ }
+ }
+ }
+
+ private fun shouldDisableIwlan(): Boolean {
+ // IWLAN on U 24Q2 release (U QPR3) causes cell data to reconnect when Wi-Fi is toggled due
+ // to the implementation of the force_iwlan_mms feature, which does not work well with
+ // multinetworking tests. Disable the feature on such builds (b/368477391).
+ // The behavior changed in more recent releases (V) so only U 24Q2 is affected.
+ return pm.hasSystemFeature(FEATURE_TELEPHONY_IMS) && pm.hasSystemFeature(FEATURE_WIFI) &&
+ Build.VERSION.SDK_INT == UPSIDE_DOWN_CAKE &&
+ isForceIwlanMmsEnabled()
+ }
+
+ private fun isForceIwlanMmsEnabled(): Boolean {
+ val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
+ val flagEnabledRegex = Regex(
+ """telephony/com\.android\.internal\.telephony\.flags\.force_iwlan_mms:""" +
+ """.*ENABLED \(system\)""")
+ ParcelFileDescriptor.AutoCloseInputStream(
+ // If the command fails (for example if printflags is missing) this will return false
+ // and the IWLAN disable will be skipped, which should be fine at it only helps with
+ // flakiness.
+ // This uses "sh -c" to cover that case as if "printflags" is used directly and the
+ // binary is missing, the remote end will crash and the InputStream EOF is never
+ // reached, so the read would hang.
+ uiAutomation.executeShellCommand("sh -c printflags")).bufferedReader().use { reader ->
+ return reader.lines().anyMatch {
+ it.contains(flagEnabledRegex)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
index e634f0e..8e27c62 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -16,27 +16,167 @@
package com.android.testutils.connectivitypreparer
+import android.Manifest.permission.NETWORK_SETTINGS
import android.content.pm.PackageManager.FEATURE_TELEPHONY
import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.LinkAddress
+import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.WifiInfo
import android.telephony.TelephonyManager
+import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.HexDump
+import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY
+import com.android.testutils.AutoReleaseNetworkCallbackRule
import com.android.testutils.ConnectUtil
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import java.io.IOException
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.util.Random
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+private const val QUIC_SOCKET_TIMEOUT_MS = 5_000
+private const val QUIC_RETRY_COUNT = 5
+
@RunWith(AndroidJUnit4::class)
class ConnectivityCheckTest {
+ @get:Rule
+ val networkCallbackRule = AutoReleaseNetworkCallbackRule()
+
+ private val logTag = ConnectivityCheckTest::class.simpleName
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val pm by lazy { context.packageManager }
private val connectUtil by lazy { ConnectUtil(context) }
+ // Skip IPv6 checks on virtual devices which do not support it. Tests that require IPv6 will
+ // still fail even if the preparer does not.
+ private fun ipv6Unsupported(wifiSsid: String?) = ConnectUtil.VIRTUAL_SSIDS.contains(
+ WifiInfo.sanitizeSsid(wifiSsid))
+
@Test
fun testCheckWifiSetup() {
if (!pm.hasSystemFeature(FEATURE_WIFI)) return
connectUtil.ensureWifiValidated()
+
+ val (wifiNetwork, wifiSsid) = runAsShell(NETWORK_SETTINGS) {
+ val cb = networkCallbackRule.requestNetwork(
+ NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ )
+ val capChanged = cb.eventuallyExpect<CapabilitiesChanged>(from = 0)
+ val network = capChanged.network
+ val ssid = capChanged.caps.ssid
+ assertFalse(ssid.isNullOrEmpty(), "No SSID for wifi network $network")
+ // Expect a global IPv6 address, and native or stacked IPv4
+ val lpChange = cb.history.poll(
+ pos = 0,
+ timeoutMs = 30_000L
+ ) {
+ it is LinkPropertiesChanged &&
+ it.network == network &&
+ it.lp.allLinkAddresses.any(LinkAddress::isIpv4) &&
+ (ipv6Unsupported(ssid) || it.lp.hasGlobalIpv6Address())
+ }
+ assertNotNull(lpChange, "Wifi network $network needs an IPv4 address" +
+ if (ipv6Unsupported(ssid)) "" else " and a global IPv6 address")
+
+ Pair(network, ssid)
+ }
+
+ // Checking QUIC is more important on Wi-Fi than cellular, as it finds firewall
+ // configuration problems on Wi-Fi, but cellular is not actionable by the test lab.
+ checkQuic(wifiNetwork, wifiSsid, ipv6 = false)
+ if (!ipv6Unsupported(wifiSsid)) {
+ checkQuic(wifiNetwork, wifiSsid, ipv6 = true)
+ }
+ }
+
+ /**
+ * Check that QUIC is working on the specified network.
+ *
+ * Some tests require QUIC (UDP), and some lab networks have been observed to not let it
+ * through due to firewalling. Ensure that devices are setup on a network that has the proper
+ * allowlists before trying to run the tests.
+ */
+ private fun checkQuic(network: Network, ssid: String, ipv6: Boolean) {
+ // Same endpoint as used in MultinetworkApiTest in CTS
+ val hostname = "connectivitycheck.android.com"
+ val targetAddrs = network.getAllByName(hostname)
+ val bindAddr = if (ipv6) IPV6_ADDR_ANY else IPV4_ADDR_ANY
+ if (targetAddrs.isEmpty()) {
+ Log.d(logTag, "No addresses found for $hostname")
+ return
+ }
+
+ val socket = DatagramSocket(0, bindAddr)
+ tryTest {
+ socket.soTimeout = QUIC_SOCKET_TIMEOUT_MS
+ network.bindSocket(socket)
+
+ // For reference see Version-Independent Properties of QUIC:
+ // https://datatracker.ietf.org/doc/html/rfc8999
+ // This packet just contains a long header with an unsupported version number, to force
+ // a version-negotiation packet in response.
+ val connectionId = ByteArray(8).apply { Random().nextBytes(this) }
+ val quicData = byteArrayOf(
+ // long header
+ 0xc0.toByte(),
+ // version number (should be an unknown version for the server)
+ 0xaa.toByte(), 0xda.toByte(), 0xca.toByte(), 0xca.toByte(),
+ // destination connection ID length
+ 0x08,
+ ) + connectionId + byteArrayOf(
+ // source connection ID length
+ 0x00,
+ ) + ByteArray(1185) // Ensure the packet is 1200 bytes long
+ val targetAddr = targetAddrs.firstOrNull { it.javaClass == bindAddr.javaClass }
+ ?: fail("No ${bindAddr.javaClass} found for $hostname " +
+ "(got ${targetAddrs.joinToString()})")
+ repeat(QUIC_RETRY_COUNT) { i ->
+ socket.send(DatagramPacket(quicData, quicData.size, targetAddr, 443))
+
+ val receivedPacket = DatagramPacket(ByteArray(1500), 1500)
+ try {
+ socket.receive(receivedPacket)
+ } catch (e: IOException) {
+ Log.d(logTag, "No response from $hostname ($targetAddr) on QUIC try $i", e)
+ return@repeat
+ }
+
+ val receivedConnectionId = receivedPacket.data.copyOfRange(7, 7 + 8)
+ if (connectionId.contentEquals(receivedConnectionId)) {
+ return@tryTest
+ } else {
+ val headerBytes = receivedPacket.data.copyOfRange(
+ 0, receivedPacket.length.coerceAtMost(15))
+ Log.d(logTag, "Received invalid connection ID on QUIC try $i: " +
+ HexDump.toHexString(headerBytes))
+ }
+ }
+ fail("QUIC is not working on SSID $ssid connecting to $targetAddr " +
+ "with local source port ${socket.localPort}: check the firewall for UDP port " +
+ "443 access."
+ )
+ } cleanup {
+ socket.close()
+ }
}
@Test
@@ -53,12 +193,16 @@
if (tm.simState == TelephonyManager.SIM_STATE_ABSENT) {
fail("The device has no SIM card inserted. $commonError")
} else if (tm.simState != TelephonyManager.SIM_STATE_READY) {
- fail("The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
- commonError)
+ fail(
+ "The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
+ commonError
+ )
}
- assertTrue(tm.isDataConnectivityPossible,
+ assertTrue(
+ tm.isDataConnectivityPossible,
"The device has a SIM card, but it does not supports data connectivity. " +
- "Check the data plan, and verify that mobile data is working. " + commonError)
+ "Check the data plan, and verify that mobile data is working. " + commonError
+ )
connectUtil.ensureCellularValidated()
}
}
diff --git a/staticlibs/testutils/devicetests/NSResponder.kt b/staticlibs/testutils/devicetests/NSResponder.kt
index f7619cd..f094407 100644
--- a/staticlibs/testutils/devicetests/NSResponder.kt
+++ b/staticlibs/testutils/devicetests/NSResponder.kt
@@ -35,12 +35,12 @@
private const val NS_TYPE = 135.toShort()
/**
- * A class that can be used to reply to Neighbor Solicitation packets on a [TapPacketReader].
+ * A class that can be used to reply to Neighbor Solicitation packets on a [PollPacketReader].
*/
class NSResponder(
- reader: TapPacketReader,
- table: Map<Inet6Address, MacAddress>,
- name: String = NSResponder::class.java.simpleName
+ reader: PollPacketReader,
+ table: Map<Inet6Address, MacAddress>,
+ name: String = NSResponder::class.java.simpleName
) : PacketResponder(reader, Icmpv6Filter(), name) {
companion object {
private val TAG = NSResponder::class.simpleName
@@ -49,7 +49,7 @@
// Copy the map if not already immutable (toMap) to make sure it is not modified
private val table = table.toMap()
- override fun replyToPacket(packet: ByteArray, reader: TapPacketReader) {
+ override fun replyToPacket(packet: ByteArray, reader: PollPacketReader) {
if (packet.size < IPV6_HEADER_LENGTH) {
return
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt b/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt
index cf0490c..f4c8657 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt
@@ -30,17 +30,17 @@
private val ARP_REPLY_IPV4 = byteArrayOf(0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x02)
/**
- * A class that can be used to reply to ARP packets on a [TapPacketReader].
+ * A class that can be used to reply to ARP packets on a [PollPacketReader].
*/
class ArpResponder(
- reader: TapPacketReader,
- table: Map<Inet4Address, MacAddress>,
- name: String = ArpResponder::class.java.simpleName
+ reader: PollPacketReader,
+ table: Map<Inet4Address, MacAddress>,
+ name: String = ArpResponder::class.java.simpleName
) : PacketResponder(reader, ArpRequestFilter(), name) {
// Copy the map if not already immutable (toMap) to make sure it is not modified
private val table = table.toMap()
- override fun replyToPacket(packet: ByteArray, reader: TapPacketReader) {
+ override fun replyToPacket(packet: ByteArray, reader: PollPacketReader) {
val targetIp = InetAddress.getByAddress(
packet.copyFromIndexWithLength(ARP_TARGET_IPADDR_OFFSET, 4))
as Inet4Address
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
new file mode 100644
index 0000000..89de0b3
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
@@ -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.
+ */
+
+package com.android.testutils
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class AutoCloseTestInterfaceRule(
+ private val context: Context,
+ ) : TestRule {
+ private val tnm = runAsShell(MANAGE_TEST_NETWORKS) {
+ context.getSystemService(TestNetworkManager::class.java)!!
+ }
+ private val ifaces = ArrayList<TestNetworkInterface>()
+
+ fun createTapInterface(): TestNetworkInterface {
+ return runAsShell(MANAGE_TEST_NETWORKS) {
+ tnm.createTapInterface()
+ }.also {
+ ifaces.add(it)
+ }
+ }
+
+ private fun closeAllInterfaces() {
+ // TODO: wait on RTM_DELLINK before proceeding.
+ for (iface in ifaces) {
+ // ParcelFileDescriptor prevents the fd from being double closed.
+ iface.getFileDescriptor().close()
+ }
+ }
+
+ private inner class AutoCloseTestInterfaceRuleStatement(
+ private val base: Statement,
+ private val description: Description
+ ) : Statement() {
+ override fun evaluate() {
+ tryTest {
+ base.evaluate()
+ } cleanup {
+ closeAllInterfaces()
+ }
+ }
+ }
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return AutoCloseTestInterfaceRuleStatement(base, description)
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
index 93422ad..be6947f 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
@@ -98,10 +98,10 @@
cellRequestCb = null
}
- private fun addCallback(
- cb: TestableNetworkCallback,
- registrar: (TestableNetworkCallback) -> Unit
- ): TestableNetworkCallback {
+ private fun <T> addCallback(
+ cb: T,
+ registrar: (NetworkCallback) -> Unit
+ ): T where T : NetworkCallback {
registrar(cb)
cbToCleanup.add(cb)
return cb
@@ -142,17 +142,24 @@
/**
* File a callback for a NetworkRequest.
*
- * This will fail tests (throw) if the cell network cannot be obtained, or if it was already
- * requested.
- *
* Tests may call [unregisterNetworkCallback] once they are done using the returned [Network],
* otherwise it will be automatically unrequested after the test.
*/
@JvmOverloads
fun registerNetworkCallback(
+ request: NetworkRequest
+ ): TestableNetworkCallback = registerNetworkCallback(request, TestableNetworkCallback())
+
+ /**
+ * File a callback for a NetworkRequest.
+ *
+ * Tests may call [unregisterNetworkCallback] once they are done using the returned [Network],
+ * otherwise it will be automatically unrequested after the test.
+ */
+ fun <T> registerNetworkCallback(
request: NetworkRequest,
- cb: TestableNetworkCallback = TestableNetworkCallback()
- ) = addCallback(cb) { cm.registerNetworkCallback(request, it) }
+ cb: T
+ ) where T : NetworkCallback = addCallback(cb) { cm.registerNetworkCallback(request, it) }
/**
* @see ConnectivityManager.registerDefaultNetworkCallback
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index 3857810..d60ab59 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -53,6 +53,10 @@
private const val WIFI_ERROR_BUSY = 2
class ConnectUtil(private val context: Context) {
+ companion object {
+ @JvmStatic
+ val VIRTUAL_SSIDS = listOf("VirtWifi", "AndroidWifi")
+ }
private val TAG = ConnectUtil::class.java.simpleName
private val cm = context.getSystemService(ConnectivityManager::class.java)
@@ -207,9 +211,8 @@
*/
private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
// Virtual wifi networks used on the emulator and cloud testing infrastructure
- val virtualSsids = listOf("VirtWifi", "AndroidWifi")
Log.d(TAG, "Wifi scan results: $scanResults")
- val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) }
+ val virtualScanResult = scanResults.firstOrNull { VIRTUAL_SSIDS.contains(it.SSID) }
?: return null
// Only add the virtual configuration if the virtual AP is detected in scans
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
new file mode 100644
index 0000000..e5b8471
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -0,0 +1,353 @@
+/*
+ * 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.testutils
+
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.device.collectors.BaseMetricListener
+import android.device.collectors.DataRecord
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.WifiInfo
+import android.net.wifi.WifiManager
+import android.os.Build
+import android.os.ParcelFileDescriptor
+import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.SIM_STATE_UNKNOWN
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.io.PrintWriter
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import kotlin.test.assertNull
+import org.json.JSONObject
+import org.junit.AssumptionViolatedException
+import org.junit.runner.Description
+import org.junit.runner.Result
+import org.junit.runner.notification.Failure
+
+/**
+ * A diagnostics collector that outputs diagnostics files as test artifacts.
+ *
+ * <p>Collects diagnostics automatically by default on non-local builds. Can be enabled/disabled
+ * manually with:
+ * ```
+ * atest MyModule -- \
+ * --module-arg MyModule:instrumentation-arg:connectivity-diagnostics-on-failure:=false
+ * ```
+ */
+class ConnectivityDiagnosticsCollector : BaseMetricListener() {
+ companion object {
+ private const val ARG_RUN_ON_FAILURE = "connectivity-diagnostics-on-failure"
+ private const val COLLECTOR_DIR = "run_listeners/connectivity_diagnostics"
+ private const val FILENAME_SUFFIX = "_conndiag.txt"
+ private const val MAX_DUMPS = 20
+
+ private val TAG = ConnectivityDiagnosticsCollector::class.simpleName
+ @JvmStatic
+ var instance: ConnectivityDiagnosticsCollector? = null
+ }
+
+ private var failureHeader: String? = null
+ private val buffer = ByteArrayOutputStream()
+ private val failureHeaderExtras = mutableMapOf<String, Any>()
+ private val collectorDir: File by lazy {
+ createAndEmptyDirectory(COLLECTOR_DIR)
+ }
+ private val outputFiles = mutableSetOf<String>()
+ private val cbHelper = NetworkCallbackHelper()
+ private val networkCallback = MonitoringNetworkCallback()
+
+ inner class MonitoringNetworkCallback : NetworkCallback() {
+ val currentMobileDataNetworks = mutableMapOf<Network, NetworkCapabilities>()
+ val currentVpnNetworks = mutableMapOf<Network, NetworkCapabilities>()
+ val currentWifiNetworks = mutableMapOf<Network, NetworkCapabilities>()
+
+ override fun onLost(network: Network) {
+ currentWifiNetworks.remove(network)
+ currentMobileDataNetworks.remove(network)
+ }
+
+ override fun onCapabilitiesChanged(network: Network, nc: NetworkCapabilities) {
+ if (nc.hasTransport(TRANSPORT_VPN)) {
+ currentVpnNetworks[network] = nc
+ } else if (nc.hasTransport(TRANSPORT_WIFI)) {
+ currentWifiNetworks[network] = nc
+ } else if (nc.hasTransport(TRANSPORT_CELLULAR)) {
+ currentMobileDataNetworks[network] = nc
+ }
+ }
+ }
+
+ override fun onSetUp() {
+ assertNull(instance, "ConnectivityDiagnosticsCollectors were set up multiple times")
+ instance = this
+ TryTestConfig.swapDiagnosticsCollector { throwable ->
+ if (runOnFailure(throwable)) {
+ collectTestFailureDiagnostics(throwable)
+ }
+ }
+ }
+
+ override fun onCleanUp() {
+ instance = null
+ }
+
+ override fun onTestRunStart(runData: DataRecord?, description: Description?) {
+ runAsShell(NETWORK_SETTINGS) {
+ cbHelper.registerNetworkCallback(
+ NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addTransportType(TRANSPORT_WIFI)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .build(), networkCallback
+ )
+ }
+ }
+
+ override fun onTestRunEnd(runData: DataRecord?, result: Result?) {
+ // onTestRunEnd is called regardless of success/failure, and the Result contains summary of
+ // run/failed/ignored... tests.
+ cbHelper.unregisterAll()
+ }
+
+ override fun onTestFail(testData: DataRecord, description: Description, failure: Failure) {
+ // TODO: find a way to disable this behavior only on local runs, to avoid slowing them down
+ // when iterating on failing tests.
+ if (!runOnFailure(failure.exception)) return
+ if (outputFiles.size >= MAX_DUMPS) return
+ Log.i(TAG, "Collecting diagnostics for test failure. Disable by running tests with: " +
+ "atest MyModule -- " +
+ "--module-arg MyModule:instrumentation-arg:$ARG_RUN_ON_FAILURE:=false")
+ collectTestFailureDiagnostics(failure.exception)
+
+ val baseFilename = "${description.className}#${description.methodName}_failure"
+ flushBufferToFileMetric(testData, baseFilename)
+ }
+
+ override fun onTestEnd(testData: DataRecord, description: Description) {
+ // Tests may call methods like collectDumpsysConnectivity to collect diagnostics at any time
+ // during the run, for example to observe state at various points to investigate a flake
+ // and compare passing/failing cases.
+ // Flush the contents of the buffer to a file when the test ends, even when successful.
+ if (buffer.size() == 0) return
+ if (outputFiles.size >= MAX_DUMPS) return
+
+ // Flush any data that the test added to the buffer for dumping
+ val baseFilename = "${description.className}#${description.methodName}_testdump"
+ flushBufferToFileMetric(testData, baseFilename)
+ }
+
+ private fun runOnFailure(exception: Throwable): Boolean {
+ // Assumption failures (assumeTrue/assumeFalse) are not actual failures
+ if (exception is AssumptionViolatedException) return false
+
+ // Do not run on local builds (which have ro.build.version.incremental set to eng.username)
+ // to avoid slowing down local runs.
+ val enabledByDefault = !Build.VERSION.INCREMENTAL.startsWith("eng.")
+ return argsBundle.getString(ARG_RUN_ON_FAILURE)?.toBooleanStrictOrNull() ?: enabledByDefault
+ }
+
+ private fun flushBufferToFileMetric(testData: DataRecord, baseFilename: String) {
+ var filename = baseFilename
+ // In case a method was run multiple times (typically retries), append a number
+ var i = 2
+ while (outputFiles.contains(filename)) {
+ filename = baseFilename + "_$i"
+ i++
+ }
+ val outFile = File(collectorDir, filename + FILENAME_SUFFIX)
+ outputFiles.add(filename)
+ FileOutputStream(outFile).use { fos ->
+ failureHeader?.let {
+ fos.write(it.toByteArray())
+ fos.write("\n".toByteArray())
+ }
+ fos.write(buffer.toByteArray())
+ }
+ failureHeader = null
+ buffer.reset()
+ val fileKey = "${ConnectivityDiagnosticsCollector::class.qualifiedName}_$filename"
+ testData.addFileMetric(fileKey, outFile)
+ }
+
+ private fun maybeCollectFailureHeader() {
+ if (failureHeader != null) {
+ Log.i(TAG, "Connectivity diagnostics failure header already collected, skipping")
+ return
+ }
+
+ val instr = InstrumentationRegistry.getInstrumentation()
+ val ctx = instr.context
+ val pm = ctx.packageManager
+ val hasWifi = pm.hasSystemFeature(FEATURE_WIFI)
+ val hasMobileData = pm.hasSystemFeature(FEATURE_TELEPHONY)
+ val tm = if (hasMobileData) ctx.getSystemService(TelephonyManager::class.java) else null
+ // getAdoptedShellPermissions is S+. Optimistically assume that tests are not holding on
+ // shell permissions during failure/cleanup on R.
+ val canUseShell = !isAtLeastS() ||
+ instr.uiAutomation.getAdoptedShellPermissions().isNullOrEmpty()
+ val headerObj = JSONObject()
+ failureHeaderExtras.forEach { (k, v) -> headerObj.put(k, v) }
+ failureHeaderExtras.clear()
+ if (canUseShell) {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE, NETWORK_SETTINGS) {
+ headerObj.apply {
+ put("deviceSerial", Build.getSerial())
+ // The network callback filed on start cannot get the WifiInfo as it would need
+ // to keep NETWORK_SETTINGS permission throughout the test run. Try to
+ // obtain it while holding the permission at the end of the test.
+ val wifiInfo = networkCallback.currentWifiNetworks.keys.firstOrNull()?.let {
+ getWifiInfo(it)
+ }
+ put("ssid", wifiInfo?.ssid)
+ put("bssid", wifiInfo?.bssid)
+ put("simState", tm?.simState ?: SIM_STATE_UNKNOWN)
+ put("mccMnc", tm?.simOperator)
+ }
+ }
+ } else {
+ Log.w(TAG, "The test is still holding shell permissions, cannot collect privileged " +
+ "device info")
+ headerObj.put("shellPermissionsUnavailable", true)
+ }
+ failureHeader = headerObj.apply {
+ put("time", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()))
+ put(
+ "wifiEnabled",
+ hasWifi && ctx.getSystemService(WifiManager::class.java).isWifiEnabled
+ )
+ put("connectedWifiCount", networkCallback.currentWifiNetworks.size)
+ put("validatedWifiCount", networkCallback.currentWifiNetworks.filterValues {
+ it.hasCapability(NET_CAPABILITY_VALIDATED)
+ }.size)
+ put("mobileDataConnectivityPossible", tm?.isDataConnectivityPossible ?: false)
+ put("connectedMobileDataCount", networkCallback.currentMobileDataNetworks.size)
+ put("validatedMobileDataCount",
+ networkCallback.currentMobileDataNetworks.filterValues {
+ it.hasCapability(NET_CAPABILITY_VALIDATED)
+ }.size
+ )
+ }.toString()
+ }
+
+ private class WifiInfoCallback : NetworkCallback {
+ private val network: Network
+ val wifiInfoFuture = CompletableFuture<WifiInfo?>()
+ constructor(network: Network) : super() {
+ this.network = network
+ }
+ @RequiresApi(Build.VERSION_CODES.S)
+ constructor(network: Network, flags: Int) : super(flags) {
+ this.network = network
+ }
+ override fun onCapabilitiesChanged(net: Network, nc: NetworkCapabilities) {
+ if (network == net) {
+ wifiInfoFuture.complete(nc.transportInfo as? WifiInfo)
+ }
+ }
+ }
+
+ private fun getWifiInfo(network: Network): WifiInfo? {
+ // Get the SSID via network callbacks, as the Networks are obtained via callbacks, and
+ // synchronous calls (CM#getNetworkCapabilities) and callbacks should not be mixed.
+ // A new callback needs to be filed and received while holding NETWORK_SETTINGS permission.
+ val cb = if (isAtLeastS()) {
+ WifiInfoCallback(network, FLAG_INCLUDE_LOCATION_INFO)
+ } else {
+ WifiInfoCallback(network)
+ }
+ cbHelper.registerNetworkCallback(
+ NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
+ return try {
+ cb.wifiInfoFuture.get(1L, TimeUnit.SECONDS)
+ } catch (e: TimeoutException) {
+ null
+ } finally {
+ cbHelper.unregisterNetworkCallback(cb)
+ }
+ }
+
+ /**
+ * Add connectivity diagnostics to the test data dump.
+ *
+ * <p>This collects a set of diagnostics that are relevant to connectivity test failures.
+ * <p>The dump will be collected immediately, and exported to a test artifact file when the
+ * test ends.
+ * @param exceptionContext An exception to write a stacktrace to the dump for context.
+ */
+ fun collectTestFailureDiagnostics(exceptionContext: Throwable? = null) {
+ maybeCollectFailureHeader()
+ collectDumpsysConnectivity(exceptionContext)
+ }
+
+ /**
+ * Add dumpsys connectivity to the test data dump.
+ *
+ * <p>The dump will be collected immediately, and exported to a test artifact file when the
+ * test ends.
+ * @param exceptionContext An exception to write a stacktrace to the dump for context.
+ */
+ fun collectDumpsysConnectivity(exceptionContext: Throwable? = null) {
+ Log.i(TAG, "Collecting dumpsys connectivity for test artifacts")
+ PrintWriter(buffer).let {
+ it.println("--- Dumpsys connectivity at ${ZonedDateTime.now()} ---")
+ maybeWriteExceptionContext(it, exceptionContext)
+ it.flush()
+ }
+ ParcelFileDescriptor.AutoCloseInputStream(
+ InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(
+ "dumpsys connectivity --dump-priority HIGH")).use {
+ it.copyTo(buffer)
+ }
+ }
+
+ /**
+ * Add a key->value attribute to the failure data, to be written to the diagnostics file.
+ *
+ * <p>This is to be called by tests that know they will fail.
+ */
+ fun addFailureAttribute(key: String, value: Any) {
+ failureHeaderExtras[key] = value
+ }
+
+ private fun maybeWriteExceptionContext(writer: PrintWriter, exceptionContext: Throwable?) {
+ if (exceptionContext == null) return
+ writer.println("At: ")
+ exceptionContext.printStackTrace(writer)
+ }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
index 1b709b2..dd52d0b 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
@@ -34,7 +34,7 @@
class DefaultNetworkRestoreMonitor(
ctx: Context,
private val notifier: RunNotifier,
- private val timeoutMs: Long = 3000
+ private val timeoutMs: Long = 30_000
) {
var firstFailure: Exception? = null
var initialTransports = 0L
@@ -56,8 +56,8 @@
it.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
} catch (e: AssertionError) {
- firstFailure = IllegalStateException(desc.methodName +
- " does not restore the default network")
+ firstFailure = IllegalStateException(desc.methodName + " does not restore the" +
+ "default network, initialTransports = $initialTransports", e)
} finally {
cm.unregisterNetworkCallback(cb)
}
@@ -88,7 +88,7 @@
}
cm.registerDefaultNetworkCallback(cb)
try {
- val cap = capFuture.get(100, TimeUnit.MILLISECONDS)
+ val cap = capFuture.get(10_000, TimeUnit.MILLISECONDS)
initialTransports = BitUtils.packBits(cap.transportTypes)
} catch (e: Exception) {
firstFailure = IllegalStateException(
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
index 68248ca..044b410 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
@@ -18,6 +18,7 @@
import android.Manifest.permission.READ_DEVICE_CONFIG
import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
import android.provider.DeviceConfig
import android.util.Log
import com.android.modules.utils.build.SdkLevel
@@ -87,8 +88,9 @@
}
throw e
} cleanupStep {
- runAsShell(WRITE_DEVICE_CONFIG) {
+ runAsShell(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG) {
originalConfig.forEach { (key, value) ->
+ Log.i(TAG, "Resetting config \"${key.second}\" to \"$value\"")
DeviceConfig.setProperty(
key.first, key.second, value, false /* makeDefault */)
}
@@ -115,7 +117,8 @@
*/
fun setConfig(namespace: String, key: String, value: String?): String? {
Log.i(TAG, "Setting config \"$key\" to \"$value\"")
- val readWritePermissions = arrayOf(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+ val readWritePermissions =
+ arrayOf(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG)
val keyPair = Pair(namespace, key)
val existingValue = runAsShell(*readWritePermissions) {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
index ce55fdc..31879af 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
@@ -16,6 +16,10 @@
package com.android.testutils;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+
+import android.os.Build;
+import android.os.SystemProperties;
import android.os.VintfRuntimeInfo;
import android.text.TextUtils;
import android.util.Pair;
@@ -173,4 +177,14 @@
final KVersion from = DeviceInfoUtils.getMajorMinorSubminorVersion(version);
return current.isAtLeast(from);
}
+
+ /**
+ * Check if the current build is a debuggable build.
+ */
+ public static boolean isDebuggable() {
+ if (isAtLeastS()) {
+ return Build.isDebuggable();
+ }
+ return SystemProperties.getInt("ro.debuggable", 0) == 1;
+ }
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
new file mode 100644
index 0000000..8608344
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
@@ -0,0 +1,202 @@
+/*
+ * 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.testutils;
+
+import static android.net.DnsResolver.CLASS_IN;
+
+import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
+import static com.android.net.module.util.DnsPacketUtils.DnsRecordParser.domainNameToLabels;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+
+import static org.junit.Assert.fail;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import android.net.InetAddresses;
+
+import androidx.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DnsSvcbUtils {
+ private static final Pattern SVC_PARAM_PATTERN = Pattern.compile("([a-z0-9-]+)=?(.*)");
+
+ /**
+ * Returns a DNS SVCB response with given hostname `hostname` and given SVCB records
+ * `records`. Each record must contain the service priority, the target name, and the service
+ * parameters.
+ * E.g. "1 doh.google alpn=h2,h3 port=443 ipv4hint=192.0.2.1 dohpath=/dns-query{?dns}"
+ */
+ @NonNull
+ public static byte[] makeSvcbResponse(String hostname, String[] records) throws IOException {
+ if (records == null) throw new NullPointerException();
+ if (!hostname.startsWith("_dns.")) throw new UnsupportedOperationException();
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ // Write DNS header.
+ os.write(shortsToByteArray(
+ 0x1234, /* Transaction ID */
+ 0x8100, /* Flags */
+ 1, /* qdcount */
+ records.length, /* ancount */
+ 0, /* nscount */
+ 0 /* arcount */
+ ));
+ // Write Question.
+ // - domainNameToLabels() doesn't support the hostname starting with "_", so divide
+ // the writing into two steps.
+ os.write(new byte[] { 0x04, '_', 'd', 'n', 's' });
+ os.write(domainNameToLabels(hostname.substring(5)));
+ os.write(shortsToByteArray(TYPE_SVCB, CLASS_IN));
+ // Write Answer section.
+ for (String r : records) {
+ os.write(makeSvcbRecord(r));
+ }
+ return os.toByteArray();
+ }
+
+ @NonNull
+ private static byte[] makeSvcbRecord(String representation) throws IOException {
+ if (representation == null) return new byte[0];
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(shortsToByteArray(
+ 0xc00c, /* Pointer to qname in question section */
+ TYPE_SVCB,
+ CLASS_IN,
+ 0, 16, /* TTL = 16 */
+ 0 /* Data Length = 0 */
+
+ ));
+ final String[] strings = representation.split(" +");
+ // SvcPriority and TargetName are mandatory in the representation.
+ if (strings.length < 3) {
+ fail("Invalid SVCB representation: " + representation);
+ }
+ // Write SvcPriority, TargetName, and SvcParams.
+ os.write(shortsToByteArray(Short.parseShort(strings[0])));
+ os.write(domainNameToLabels(strings[1]));
+ for (int i = 2; i < strings.length; i++) {
+ try {
+ os.write(svcParamToByteArray(strings[i]));
+ } catch (UnsupportedEncodingException e) {
+ throw new IOException(e);
+ }
+ }
+ // Update rdata length.
+ final byte[] out = os.toByteArray();
+ ByteBuffer.wrap(out).putShort(10, (short) (out.length - 12));
+ return out;
+ }
+
+ @NonNull
+ private static byte[] svcParamToByteArray(String svcParam) throws IOException {
+ final Matcher matcher = SVC_PARAM_PATTERN.matcher(svcParam);
+ if (!matcher.matches() || matcher.groupCount() != 2) {
+ fail("Invalid SvcParam: " + svcParam);
+ }
+ final String svcParamkey = matcher.group(1);
+ final String svcParamValue = matcher.group(2);
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(svcParamKeyToBytes(svcParamkey));
+ switch (svcParamkey) {
+ case "mandatory":
+ final String[] keys = svcParamValue.split(",");
+ os.write(shortsToByteArray(keys.length));
+ for (String v : keys) {
+ os.write(svcParamKeyToBytes(v));
+ }
+ break;
+ case "alpn":
+ os.write(shortsToByteArray((svcParamValue.length() + 1)));
+ for (String v : svcParamValue.split(",")) {
+ os.write(v.length());
+ // TODO: support percent-encoding per RFC 7838.
+ os.write(v.getBytes(US_ASCII));
+ }
+ break;
+ case "no-default-alpn":
+ os.write(shortsToByteArray(0));
+ break;
+ case "port":
+ os.write(shortsToByteArray(2));
+ os.write(shortsToByteArray(Short.parseShort(svcParamValue)));
+ break;
+ case "ipv4hint":
+ final String[] v4Addrs = svcParamValue.split(",");
+ os.write(shortsToByteArray((v4Addrs.length * IPV4_ADDR_LEN)));
+ for (String v : v4Addrs) {
+ os.write(InetAddresses.parseNumericAddress(v).getAddress());
+ }
+ break;
+ case "ech":
+ os.write(shortsToByteArray(svcParamValue.length()));
+ os.write(svcParamValue.getBytes(US_ASCII)); // base64 encoded
+ break;
+ case "ipv6hint":
+ final String[] v6Addrs = svcParamValue.split(",");
+ os.write(shortsToByteArray((v6Addrs.length * IPV6_ADDR_LEN)));
+ for (String v : v6Addrs) {
+ os.write(InetAddresses.parseNumericAddress(v).getAddress());
+ }
+ break;
+ case "dohpath":
+ os.write(shortsToByteArray(svcParamValue.length()));
+ // TODO: support percent-encoding, since this is a URI template.
+ os.write(svcParamValue.getBytes(US_ASCII));
+ break;
+ default:
+ os.write(shortsToByteArray(svcParamValue.length()));
+ os.write(svcParamValue.getBytes(US_ASCII));
+ break;
+ }
+ return os.toByteArray();
+ }
+
+ @NonNull
+ private static byte[] svcParamKeyToBytes(String key) {
+ switch (key) {
+ case "mandatory": return shortsToByteArray(0);
+ case "alpn": return shortsToByteArray(1);
+ case "no-default-alpn": return shortsToByteArray(2);
+ case "port": return shortsToByteArray(3);
+ case "ipv4hint": return shortsToByteArray(4);
+ case "ech": return shortsToByteArray(5);
+ case "ipv6hint": return shortsToByteArray(6);
+ case "dohpath": return shortsToByteArray(7);
+ default:
+ if (!key.startsWith("key")) fail("Invalid SvcParamKey " + key);
+ return shortsToByteArray(Short.parseShort(key.substring(3)));
+ }
+ }
+
+ @NonNull
+ private static byte[] shortsToByteArray(int... values) {
+ final ByteBuffer out = ByteBuffer.allocate(values.length * 2);
+ for (int value: values) {
+ if (value < 0 || value > 0xffff) {
+ throw new AssertionError("not an unsigned short: " + value);
+ }
+ out.putShort((short) value);
+ }
+ return out.array();
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
index 1f82a35..e49c0c7 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
@@ -18,72 +18,56 @@
import android.net.DnsResolver
import android.net.InetAddresses
-import android.os.Looper
+import android.net.Network
import android.os.Handler
+import android.os.Looper
import com.android.internal.annotations.GuardedBy
-import java.net.InetAddress
-import java.util.concurrent.Executor
-import org.mockito.invocation.InvocationOnMock
+import com.android.net.module.util.DnsPacket
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doAnswer
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+import java.net.InetAddress
+import java.net.UnknownHostException
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
-const val TYPE_UNSPECIFIED = -1
-// TODO: Integrate with NetworkMonitorTest.
-class FakeDns(val mockResolver: DnsResolver) {
- class DnsEntry(val hostname: String, val type: Int, val addresses: List<InetAddress>) {
- fun match(host: String, type: Int) = hostname.equals(host) && type == type
- }
+// Nonexistent DNS query type to represent "A and/or AAAA queries".
+// TODO: deduplicate this with DnsUtils.TYPE_ADDRCONFIG.
+private const val TYPE_ADDRCONFIG = -1
- @GuardedBy("answers")
- val answers = ArrayList<DnsEntry>()
+class FakeDns(val network: Network, val dnsResolver: DnsResolver) {
+ private val HANDLER_TIMEOUT_MS = 1000
- fun getAnswer(hostname: String, type: Int): DnsEntry? = synchronized(answers) {
- return answers.firstOrNull { it.match(hostname, type) }
- }
-
- fun setAnswer(hostname: String, answer: Array<String>, type: Int) = synchronized(answers) {
- val ans = DnsEntry(hostname, type, generateAnswer(answer))
- // Replace or remove the existing one.
- when (val index = answers.indexOfFirst { it.match(hostname, type) }) {
- -1 -> answers.add(ans)
- else -> answers[index] = ans
+ /** Data class to record the Dns entry. */
+ class DnsEntry (val hostname: String, val type: Int, val answerSupplier: AnswerSupplier) {
+ // Full match or partial match that target host contains the entry hostname to support
+ // random private dns probe hostname.
+ fun matches(hostname: String, type: Int): Boolean {
+ return hostname.endsWith(this.hostname) && type == this.type
}
}
- private fun generateAnswer(answer: Array<String>) =
- answer.filterNotNull().map { InetAddresses.parseNumericAddress(it) }
+ /**
+ * Whether queries on [network] will be answered when private DNS is enabled. Queries that
+ * bypass private DNS by using [network.privateDnsBypassingCopy] are always answered.
+ */
+ var nonBypassPrivateDnsWorking: Boolean = true
- fun startMocking() {
- // Mock DnsResolver.query() w/o type
- doAnswer {
- mockAnswer(it, 1, -1, 3, 5)
- }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* flags */,
- any() /* executor */, any() /* cancellationSignal */, any() /*callback*/)
- // Mock DnsResolver.query() w/ type
- doAnswer {
- mockAnswer(it, 1, 2, 4, 6)
- }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* nsType */,
- anyInt() /* flags */, any() /* executor */, any() /* cancellationSignal */,
- any() /*callback*/)
+ @GuardedBy("answers")
+ private val answers = mutableListOf<DnsEntry>()
+
+ interface AnswerSupplier {
+ /** Supplies the answer to one DnsResolver query method call. */
+ @Throws(DnsResolver.DnsException::class)
+ fun get(): Array<String>?
}
- private fun mockAnswer(
- it: InvocationOnMock,
- posHos: Int,
- posType: Int,
- posExecutor: Int,
- posCallback: Int
- ) {
- val hostname = it.arguments[posHos] as String
- val executor = it.arguments[posExecutor] as Executor
- val callback = it.arguments[posCallback] as DnsResolver.Callback<List<InetAddress>>
- var type = if (posType != -1) it.arguments[posType] as Int else TYPE_UNSPECIFIED
- val answer = getAnswer(hostname, type)
-
- if (answer != null && !answer.addresses.isNullOrEmpty()) {
- Handler(Looper.getMainLooper()).post({ executor.execute({
- callback.onAnswer(answer.addresses, 0); }) })
+ private class InstantAnswerSupplier(val answers: Array<String>?) : AnswerSupplier {
+ override fun get(): Array<String>? {
+ return answers
}
}
@@ -91,4 +75,177 @@
fun clearAll() = synchronized(answers) {
answers.clear()
}
+
+ /** Returns the answer for a given name and type on the given mock network. */
+ private fun getAnswer(mockNetwork: Network, hostname: String, type: Int):
+ CompletableFuture<Array<String>?> {
+ if (!checkQueryNetwork(mockNetwork)) {
+ return CompletableFuture.completedFuture(null)
+ }
+ val answerSupplier: AnswerSupplier? = synchronized(answers) {
+ answers.firstOrNull({e: DnsEntry -> e.matches(hostname, type)})?.answerSupplier
+ }
+ if (answerSupplier == null) {
+ return CompletableFuture.completedFuture(null)
+ }
+ if (answerSupplier is InstantAnswerSupplier) {
+ // Save latency waiting for a query thread if the answer is hardcoded.
+ return CompletableFuture.completedFuture<Array<String>?>(answerSupplier.get())
+ }
+ val answerFuture = CompletableFuture<Array<String>?>()
+ // Don't worry about ThreadLeadMonitor: these threads terminate immediately, so they won't
+ // leak, and ThreadLeakMonitor won't monitor them anyway, since they have one-time names
+ // such as "Thread-42".
+ Thread {
+ try {
+ answerFuture.complete(answerSupplier.get())
+ } catch (e: DnsResolver.DnsException) {
+ answerFuture.completeExceptionally(e)
+ }
+ }.start()
+ return answerFuture
+ }
+
+ /** Sets the answer for a given name and type. */
+ fun setAnswer(hostname: String, answer: Array<String>?, type: Int) = setAnswer(
+ hostname, InstantAnswerSupplier(answer), type)
+
+ /** Sets the answer for a given name and type. */
+ fun setAnswer(
+ hostname: String, answerSupplier: AnswerSupplier, type: Int) = synchronized (answers) {
+ val ans = DnsEntry(hostname, type, answerSupplier)
+ // Replace or remove the existing one.
+ when (val index = answers.indexOfFirst { it.matches(hostname, type) }) {
+ -1 -> answers.add(ans)
+ else -> answers[index] = ans
+ }
+ }
+
+ private fun checkQueryNetwork(mockNetwork: Network): Boolean {
+ // Queries on the wrong network do not work.
+ // Queries that bypass private DNS work.
+ // Queries that do not bypass private DNS work only if nonBypassPrivateDnsWorking is true.
+ return mockNetwork == network.privateDnsBypassingCopy ||
+ mockNetwork == network && nonBypassPrivateDnsWorking
+ }
+
+ /** Simulates a getAllByName call for the specified name on the specified mock network. */
+ private fun getAllByName(mockNetwork: Network, hostname: String): Array<InetAddress>? {
+ val answer = stringsToInetAddresses(queryAllTypes(mockNetwork, hostname)
+ .get(HANDLER_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS))
+ if (answer == null || answer.size == 0) {
+ throw UnknownHostException(hostname)
+ }
+ return answer.toTypedArray()
+ }
+
+ // Regardless of the type, depends on what the responses contained in the network.
+ private fun queryAllTypes(
+ mockNetwork: Network, hostname: String
+ ): CompletableFuture<Array<String>?> {
+ val aFuture = getAnswer(mockNetwork, hostname, DnsResolver.TYPE_A)
+ .exceptionally { emptyArray() }
+ val aaaaFuture = getAnswer(mockNetwork, hostname, DnsResolver.TYPE_AAAA)
+ .exceptionally { emptyArray() }
+ val combinedFuture = CompletableFuture<Array<String>?>()
+ aFuture.thenAcceptBoth(aaaaFuture) { res1: Array<String>?, res2: Array<String>? ->
+ var answer: Array<String> = arrayOf()
+ if (res1 != null) answer += res1
+ if (res2 != null) answer += res2
+ combinedFuture.complete(answer)
+ }
+ return combinedFuture
+ }
+
+ /** Starts mocking DNS queries. */
+ fun startMocking() {
+ // Queries on mNetwork using getAllByName.
+ doAnswer {
+ getAllByName(it.mock as Network, it.getArgument(0))
+ }.`when`(network).getAllByName(any())
+
+ // Queries on mCleartextDnsNetwork using DnsResolver#query.
+ doAnswer {
+ mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 3, posCallback = 5,
+ posType = -1)
+ }.`when`(dnsResolver).query(any(), any(), anyInt(), any(), any(), any())
+
+ // Queries on mCleartextDnsNetwork using DnsResolver#query with QueryType.
+ doAnswer {
+ mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 4, posCallback = 6,
+ posType = 2)
+ }.`when`(dnsResolver).query(any(), any(), anyInt(), anyInt(), any(), any(), any())
+
+ // Queries using rawQuery. Currently, mockQuery only supports TYPE_SVCB.
+ doAnswer {
+ mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 5, posCallback = 7,
+ posType = 3)
+ }.`when`(dnsResolver).rawQuery(any(), any(), anyInt(), anyInt(), anyInt(), any(), any(),
+ any())
+ }
+
+ private fun stringsToInetAddresses(addrs: Array<String>?): List<InetAddress>? {
+ if (addrs == null) return null
+ val out: MutableList<InetAddress> = ArrayList()
+ for (addr in addrs) {
+ out.add(InetAddresses.parseNumericAddress(addr))
+ }
+ return out
+ }
+
+ // Mocks all the DnsResolver query methods used in this test.
+ private fun mockQuery(
+ invocation: InvocationOnMock, posNetwork: Int, posHostname: Int,
+ posExecutor: Int, posCallback: Int, posType: Int
+ ): Answer<*>? {
+ val hostname = invocation.getArgument<String>(posHostname)
+ val executor = invocation.getArgument<Executor>(posExecutor)
+ val network = invocation.getArgument<Network>(posNetwork)
+ val qtype = if (posType != -1) invocation.getArgument(posType) else TYPE_ADDRCONFIG
+ val answerFuture: CompletableFuture<Array<String>?> = if (posType != -1) getAnswer(
+ network,
+ hostname,
+ invocation.getArgument(posType)
+ ) else queryAllTypes(network, hostname)
+
+ // Discriminate between different callback types to avoid unchecked cast warnings when
+ // calling the onAnswer methods.
+ val inetAddressCallback: DnsResolver.Callback<List<InetAddress>> =
+ invocation.getArgument(posCallback)
+ val byteArrayCallback: DnsResolver.Callback<ByteArray> =
+ invocation.getArgument(posCallback)
+ val callback: DnsResolver.Callback<*> = invocation.getArgument(posCallback)
+
+ answerFuture.whenComplete { answer: Array<String>?, exception: Throwable? ->
+ // Use getMainLooper() because that's what android.net.DnsResolver currently uses.
+ Handler(Looper.getMainLooper()).post {
+ executor.execute {
+ if (exception != null) {
+ if (exception !is DnsResolver.DnsException) {
+ throw java.lang.AssertionError(
+ "Test error building DNS response",
+ exception
+ )
+ }
+ callback.onError((exception as DnsResolver.DnsException?)!!)
+ return@execute
+ }
+ if (answer != null && answer.size > 0) {
+ when (qtype) {
+ DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA, TYPE_ADDRCONFIG ->
+ inetAddressCallback.onAnswer(stringsToInetAddresses(answer)!!, 0)
+ DnsPacket.TYPE_SVCB ->
+ byteArrayCallback.onAnswer(
+ DnsSvcbUtils.makeSvcbResponse(hostname, answer), 0)
+ else -> throw UnsupportedOperationException(
+ "Unsupported qtype $qtype, update this fake"
+ )
+ }
+ }
+ }
+ }
+ }
+ // If the future does not complete or has no answer do nothing. The timeout should fire.
+ return null
+ }
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
index 8b88224..5729452 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
@@ -28,8 +28,6 @@
import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
import com.android.net.module.util.TrackRecord
-import com.android.testutils.IPv6UdpFilter
-import com.android.testutils.TapPacketReader
import java.net.Inet6Address
import java.net.InetAddress
import kotlin.test.assertEquals
@@ -246,7 +244,7 @@
as Inet6Address
}
-fun TapPacketReader.pollForMdnsPacket(
+fun PollPacketReader.pollForMdnsPacket(
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS,
predicate: (TestDnsPacket) -> Boolean
): TestDnsPacket? {
@@ -264,7 +262,7 @@
}
}
-fun TapPacketReader.pollForProbe(
+fun PollPacketReader.pollForProbe(
serviceName: String,
serviceType: String,
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
@@ -272,7 +270,7 @@
it.isProbeFor("$serviceName.$serviceType.local")
}
-fun TapPacketReader.pollForAdvertisement(
+fun PollPacketReader.pollForAdvertisement(
serviceName: String,
serviceType: String,
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
@@ -280,19 +278,19 @@
it.isReplyFor("$serviceName.$serviceType.local")
}
-fun TapPacketReader.pollForQuery(
+fun PollPacketReader.pollForQuery(
recordName: String,
vararg requiredTypes: Int,
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isQueryFor(recordName, *requiredTypes) }
-fun TapPacketReader.pollForReply(
+fun PollPacketReader.pollForReply(
recordName: String,
type: Int,
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isReplyFor(recordName, type) }
-fun TapPacketReader.pollForReply(
+fun PollPacketReader.pollForReply(
serviceName: String,
serviceType: String,
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
index 8324b25..26bdb49 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
@@ -16,8 +16,21 @@
package com.android.testutils
+import android.app.usage.NetworkStatsManager
+import android.content.Context
+import android.net.INetworkStatsService
+import android.net.INetworkStatsSession
import android.net.NetworkStats
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_MOBILE
+import android.text.TextUtils
+import com.android.modules.utils.build.SdkLevel
import kotlin.test.assertTrue
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito
@JvmOverloads
fun orderInsensitiveEquals(
@@ -26,7 +39,7 @@
compareTime: Boolean = false
): Boolean {
if (leftStats == rightStats) return true
- if (compareTime && leftStats.getElapsedRealtime() != rightStats.getElapsedRealtime()) {
+ if (compareTime && leftStats.elapsedRealtime != rightStats.elapsedRealtime) {
return false
}
@@ -47,12 +60,41 @@
left.metered, left.roaming, left.defaultNetwork, i)
if (j == -1) return false
rightTrimmedEmpty.getValues(j, right)
- if (left != right) return false
+ if (SdkLevel.isAtLeastT()) {
+ if (left != right) return false
+ } else {
+ if (!checkEntryEquals(left, right)) return false
+ }
}
return true
}
/**
+ * Assert that the two {@link NetworkStats.Entry} are equals.
+ */
+fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
+ assertTrue(checkEntryEquals(left, right))
+}
+
+// TODO: Make all callers use NetworkStats.Entry#equals once S- downstreams
+// are no longer supported. Because NetworkStats is mainlined on T+ and
+// NetworkStats.Entry#equals in S- does not support null iface.
+fun checkEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry): Boolean {
+ return TextUtils.equals(left.iface, right.iface) &&
+ left.uid == right.uid &&
+ left.set == right.set &&
+ left.tag == right.tag &&
+ left.metered == right.metered &&
+ left.roaming == right.roaming &&
+ left.defaultNetwork == right.defaultNetwork &&
+ left.rxBytes == right.rxBytes &&
+ left.rxPackets == right.rxPackets &&
+ left.txBytes == right.txBytes &&
+ left.txPackets == right.txPackets &&
+ left.operations == right.operations
+}
+
+/**
* Assert that two {@link NetworkStats} are equals, assuming the order of the records are not
* necessarily the same.
*
@@ -66,7 +108,7 @@
compareTime: Boolean = false
) {
assertTrue(orderInsensitiveEquals(expected, actual, compareTime),
- "expected: " + expected + " but was: " + actual)
+ "expected: $expected but was: $actual")
}
/**
@@ -74,5 +116,34 @@
* object.
*/
fun assertParcelingIsLossless(stats: NetworkStats) {
- assertParcelingIsLossless(stats, { a, b -> orderInsensitiveEquals(a, b) })
+ assertParcelingIsLossless(stats) { a, b -> orderInsensitiveEquals(a, b) }
+}
+
+/**
+ * Make a {@link android.app.usage.NetworkStats} instance from
+ * a {@link android.net.NetworkStats} instance.
+ */
+// It's not possible to directly create a mocked `NetworkStats` instance
+// because of limitations with `NetworkStats#getNextBucket`.
+// As a workaround for testing, create a mock by controlling the return values
+// from the mocked service that provides the `NetworkStats` data.
+// Notes:
+// 1. The order of records in the final `NetworkStats` object might change or
+// some records might be merged if there are items with duplicate keys.
+// 2. The interface and operations fields will be empty since there is
+// no such field in the {@link android.app.usage.NetworkStats}.
+fun makePublicStatsFromAndroidNetStats(androidNetStats: NetworkStats):
+ android.app.usage.NetworkStats {
+ val mockService = Mockito.mock(INetworkStatsService::class.java)
+ val manager = NetworkStatsManager(Mockito.mock(Context::class.java), mockService)
+ val mockStatsSession = Mockito.mock(INetworkStatsSession::class.java)
+
+ Mockito.doReturn(mockStatsSession).`when`(mockService)
+ .openSessionForUsageStats(anyInt(), any())
+ Mockito.doReturn(androidNetStats).`when`(mockStatsSession).getSummaryForAllUid(
+ any(NetworkTemplate::class.java), anyLong(), anyLong(), anyBoolean())
+ return manager.querySummary(
+ NetworkTemplate.Builder(MATCH_MOBILE).build(),
+ Long.MIN_VALUE, Long.MAX_VALUE
+ )
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
index 964c6c6..62d0e82 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
@@ -21,24 +21,24 @@
private const val POLL_FREQUENCY_MS = 1000L
/**
- * A class that can be used to reply to packets from a [TapPacketReader].
+ * A class that can be used to reply to packets from a [PollPacketReader].
*
* A reply thread will be created to reply to incoming packets asynchronously.
- * The receiver creates a new read head on the [TapPacketReader], to read packets, so it does not
- * affect packets obtained through [TapPacketReader.popPacket].
+ * The receiver creates a new read head on the [PollPacketReader], to read packets, so it does not
+ * affect packets obtained through [PollPacketReader.popPacket].
*
- * @param reader a [TapPacketReader] to obtain incoming packets and reply to them.
+ * @param reader a [PollPacketReader] to obtain incoming packets and reply to them.
* @param packetFilter A filter to apply to incoming packets.
* @param name Name to use for the internal responder thread.
*/
abstract class PacketResponder(
- private val reader: TapPacketReader,
- private val packetFilter: Predicate<ByteArray>,
- name: String
+ private val reader: PollPacketReader,
+ private val packetFilter: Predicate<ByteArray>,
+ name: String
) {
private val replyThread = ReplyThread(name)
- protected abstract fun replyToPacket(packet: ByteArray, reader: TapPacketReader)
+ protected abstract fun replyToPacket(packet: ByteArray, reader: PollPacketReader)
/**
* Start the [PacketResponder].
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReader.java b/staticlibs/testutils/devicetests/com/android/testutils/PollPacketReader.java
similarity index 91%
rename from staticlibs/testutils/devicetests/com/android/testutils/TapPacketReader.java
rename to staticlibs/testutils/devicetests/com/android/testutils/PollPacketReader.java
index b25b9f2..dbc7eb0 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReader.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PollPacketReader.java
@@ -35,19 +35,19 @@
import kotlin.LazyKt;
/**
- * A packet reader that runs on a TAP interface.
+ * A packet reader that can poll for received packets and send responses on a fd.
*
* It also implements facilities to reply to received packets.
*/
-public class TapPacketReader extends PacketReader {
- private final FileDescriptor mTapFd;
+public class PollPacketReader extends PacketReader {
+ private final FileDescriptor mFd;
private final ArrayTrackRecord<byte[]> mReceivedPackets = new ArrayTrackRecord<>();
private final Lazy<ArrayTrackRecord<byte[]>.ReadHead> mReadHead =
LazyKt.lazy(mReceivedPackets::newReadHead);
- public TapPacketReader(Handler h, FileDescriptor tapFd, int maxPacketSize) {
+ public PollPacketReader(Handler h, FileDescriptor fd, int maxPacketSize) {
super(h, maxPacketSize);
- mTapFd = tapFd;
+ mFd = fd;
}
@@ -63,7 +63,7 @@
@Override
protected FileDescriptor createFd() {
- return mTapFd;
+ return mFd;
}
@Override
@@ -119,7 +119,7 @@
}
/*
- * Send a response on the TAP interface.
+ * Send a response on the fd.
*
* The passed ByteBuffer is flipped after use.
*
@@ -127,7 +127,7 @@
* @throws IOException if the interface can't be written to.
*/
public void sendResponse(final ByteBuffer packet) throws IOException {
- try (FileOutputStream out = new FileOutputStream(mTapFd)) {
+ try (FileOutputStream out = new FileOutputStream(mFd)) {
byte[] packetBytes = new byte[packet.limit()];
packet.get(packetBytes);
packet.flip(); // So we can reuse it in the future.
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java b/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java
index 51d57bc..6709555 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java
@@ -62,18 +62,18 @@
private static final String TAG = "RouterAdvertisementResponder";
private static final Inet6Address DNS_SERVER =
(Inet6Address) InetAddresses.parseNumericAddress("2001:4860:4860::64");
- private final TapPacketReader mPacketReader;
+ private final PollPacketReader mPacketReader;
// Maps IPv6 address to MacAddress and isRouter boolean.
private final Map<Inet6Address, Pair<MacAddress, Boolean>> mNeighborMap = new ArrayMap<>();
private final IpPrefix mPrefix;
- public RouterAdvertisementResponder(TapPacketReader packetReader, IpPrefix prefix) {
+ public RouterAdvertisementResponder(PollPacketReader packetReader, IpPrefix prefix) {
super(packetReader, RouterAdvertisementResponder::isRsOrNs, TAG);
mPacketReader = packetReader;
mPrefix = Objects.requireNonNull(prefix);
}
- public RouterAdvertisementResponder(TapPacketReader packetReader) {
+ public RouterAdvertisementResponder(PollPacketReader packetReader) {
this(packetReader, makeRandomPrefix());
}
@@ -148,7 +148,7 @@
buildSllaOption(srcMac));
}
- private static void sendResponse(TapPacketReader reader, ByteBuffer buffer) {
+ private static void sendResponse(PollPacketReader reader, ByteBuffer buffer) {
try {
reader.sendResponse(buffer);
} catch (IOException e) {
@@ -158,7 +158,7 @@
}
}
- private void replyToRouterSolicitation(TapPacketReader reader, MacAddress dstMac) {
+ private void replyToRouterSolicitation(PollPacketReader reader, MacAddress dstMac) {
for (Map.Entry<Inet6Address, Pair<MacAddress, Boolean>> it : mNeighborMap.entrySet()) {
final boolean isRouter = it.getValue().second;
if (!isRouter) {
@@ -169,7 +169,7 @@
}
}
- private void replyToNeighborSolicitation(TapPacketReader reader, MacAddress dstMac,
+ private void replyToNeighborSolicitation(PollPacketReader reader, MacAddress dstMac,
Inet6Address dstIp, Inet6Address targetIp) {
final Pair<MacAddress, Boolean> neighbor = mNeighborMap.get(targetIp);
if (neighbor == null) {
@@ -190,7 +190,7 @@
}
@Override
- protected void replyToPacket(byte[] packet, TapPacketReader reader) {
+ protected void replyToPacket(byte[] packet, PollPacketReader reader) {
final ByteBuffer buf = ByteBuffer.wrap(packet);
// Messages are filtered by parent class, so it is safe to assume that packet is either an
// RS or NS.
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
index d5e91c2..7b970d3 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
@@ -57,6 +57,7 @@
* @param enabled The desired state (true for enabled, false for disabled) of the feature flag.
*/
@Target(AnnotationTarget.FUNCTION)
+ @Repeatable
@Retention(AnnotationRetention.RUNTIME)
annotation class FeatureFlag(val name: String, val enabled: Boolean = true)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt
index 701666c..adf7619 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt
@@ -31,9 +31,9 @@
private const val HANDLER_TIMEOUT_MS = 10_000L
/**
- * A [TestRule] that sets up a [TapPacketReader] on a [TestNetworkInterface] for use in the test.
+ * A [TestRule] that sets up a [PollPacketReader] on a [TestNetworkInterface] for use in the test.
*
- * @param maxPacketSize Maximum size of packets read in the [TapPacketReader] buffer.
+ * @param maxPacketSize Maximum size of packets read in the [PollPacketReader] buffer.
* @param autoStart Whether to initialize the interface and start the reader automatically for every
* test. If false, each test must either call start() and stop(), or be annotated
* with TapPacketReaderTest before using the reader or interface.
@@ -50,21 +50,21 @@
// referenced before they could be initialized (typically if autoStart is false and the test
// does not call start or use @TapPacketReaderTest).
lateinit var iface: TestNetworkInterface
- lateinit var reader: TapPacketReader
+ lateinit var reader: PollPacketReader
@Volatile
private var readerRunning = false
/**
* Indicates that the [TapPacketReaderRule] should initialize its [TestNetworkInterface] and
- * start the [TapPacketReader] before the test, and tear them down afterwards.
+ * start the [PollPacketReader] before the test, and tear them down afterwards.
*
* For use when [TapPacketReaderRule] is created with autoStart = false.
*/
annotation class TapPacketReaderTest
/**
- * Initialize the tap interface and start the [TapPacketReader].
+ * Initialize the tap interface and start the [PollPacketReader].
*
* Tests using this method must also call [stop] before exiting.
* @param handler Handler to run the reader on. Callers are responsible for safely terminating
@@ -85,13 +85,13 @@
}
val usedHandler = handler ?: HandlerThread(
TapPacketReaderRule::class.java.simpleName).apply { start() }.threadHandler
- reader = TapPacketReader(usedHandler, iface.fileDescriptor.fileDescriptor, maxPacketSize)
+ reader = PollPacketReader(usedHandler, iface.fileDescriptor.fileDescriptor, maxPacketSize)
reader.startAsyncForTest()
readerRunning = true
}
/**
- * Stop the [TapPacketReader].
+ * Stop the [PollPacketReader].
*
* Tests calling [start] must call this method before exiting. If a handler was specified in
* [start], all messages on that handler must also be processed after calling this method and
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
index 70f20d6..58e6622 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
@@ -65,10 +65,11 @@
@Override
public void insertEntry(K key, V value) throws ErrnoException,
- IllegalArgumentException {
- // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry.
+ IllegalStateException {
+ // The entry is created if and only if it doesn't exist.
+ // And throws exception if it exists. See BpfMap#insertEntry.
if (mMap.get(key) != null) {
- throw new IllegalArgumentException(key + " already exist");
+ throw new IllegalStateException(key + " already exist");
}
mMap.put(key, value);
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
index 84fb47b..341d55f 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
@@ -29,7 +29,6 @@
import android.os.Binder
import android.os.Build
import androidx.annotation.RequiresApi
-import com.android.modules.utils.build.SdkLevel.isAtLeastR
import com.android.modules.utils.build.SdkLevel.isAtLeastS
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@@ -137,7 +136,6 @@
network = try {
if (lp != null) {
- assertTrue(isAtLeastR(), "Cannot specify TestNetwork LinkProperties before R")
tnm.setupTestNetwork(lp, true /* isMetered */, binder)
} else {
tnm.setupTestNetwork(iface.interfaceName, binder)
diff --git a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
index 435fdd8..f6168af 100644
--- a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -28,6 +28,7 @@
private const val CONNECTIVITY_CHECKER_APK = "ConnectivityTestPreparer.apk"
private const val CONNECTIVITY_PKG_NAME = "com.android.testutils.connectivitypreparer"
private const val CONNECTIVITY_CHECK_CLASS = "$CONNECTIVITY_PKG_NAME.ConnectivityCheckTest"
+private const val CARRIER_CONFIG_SETUP_CLASS = "$CONNECTIVITY_PKG_NAME.CarrierConfigSetupTest"
// As per the <instrumentation> defined in the checker manifest
private const val CONNECTIVITY_CHECK_RUNNER_NAME = "androidx.test.runner.AndroidJUnitRunner"
@@ -84,27 +85,28 @@
installer.setShouldGrantPermission(true)
installer.setUp(testInfo)
- val testMethods = mutableListOf<String>()
+ val testMethods = mutableListOf<Pair<String, String>>()
if (!ignoreWifiCheck) {
- testMethods.add("testCheckWifiSetup")
+ testMethods.add(CONNECTIVITY_CHECK_CLASS to "testCheckWifiSetup")
}
if (!ignoreMobileDataCheck) {
- testMethods.add("testCheckTelephonySetup")
+ testMethods.add(CARRIER_CONFIG_SETUP_CLASS to "testSetCarrierConfig")
+ testMethods.add(CONNECTIVITY_CHECK_CLASS to "testCheckTelephonySetup")
}
testMethods.forEach {
- runTestMethod(testInfo, it)
+ runTestMethod(testInfo, it.first, it.second)
}
}
- private fun runTestMethod(testInfo: TestInformation, method: String) {
+ private fun runTestMethod(testInfo: TestInformation, clazz: String, method: String) {
val runner = DefaultRemoteAndroidTestRunner(
CONNECTIVITY_PKG_NAME,
CONNECTIVITY_CHECK_RUNNER_NAME,
testInfo.device.iDevice
)
runner.runOptions = "--no-hidden-api-checks"
- runner.setMethodName(CONNECTIVITY_CHECK_CLASS, method)
+ runner.setMethodName(clazz, method)
val receiver = CollectingTestListener()
if (!testInfo.device.runInstrumentationTests(runner, receiver)) {
@@ -187,6 +189,9 @@
override fun tearDown(testInfo: TestInformation, e: Throwable?) {
if (isTearDownDisabled) return
+ if (!ignoreMobileDataCheck) {
+ runTestMethod(testInfo, CARRIER_CONFIG_SETUP_CLASS, "testClearCarrierConfig")
+ }
installer.tearDown(testInfo, e)
setUpdaterNetworkingEnabled(
testInfo,
diff --git a/staticlibs/testutils/host/python/apf_test_base.py b/staticlibs/testutils/host/python/apf_test_base.py
new file mode 100644
index 0000000..2552aa3
--- /dev/null
+++ b/staticlibs/testutils/host/python/apf_test_base.py
@@ -0,0 +1,115 @@
+# 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.
+
+from mobly import asserts
+from net_tests_utils.host.python import adb_utils, apf_utils, assert_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python.tether_utils import UpstreamType
+import time
+
+class ApfTestBase(multi_devices_test_base.MultiDevicesTestBase):
+
+ def setup_class(self):
+ super().setup_class()
+
+ # Check test preconditions.
+ asserts.abort_class_if(
+ not self.client.isAtLeastV(),
+ "Do not enforce the test until V+ since chipset potential bugs are"
+ " expected to be fixed on V+ releases.",
+ )
+ tether_utils.assume_hotspot_test_preconditions(
+ self.serverDevice, self.clientDevice, UpstreamType.NONE
+ )
+ asserts.abort_class_if(
+ not apf_utils.is_send_raw_packet_downstream_supported(
+ self.serverDevice
+ ),
+ "NetworkStack is too old to support send raw packet, skip test.",
+ )
+
+ # Fetch device properties and storing them locally for later use.
+ # TODO: refactor to separate instances to store client and server device
+ self.server_iface_name, client_network = (
+ tether_utils.setup_hotspot_and_client_for_upstream_type(
+ self.serverDevice, self.clientDevice, UpstreamType.NONE
+ )
+ )
+ self.client_iface_name = self.client.getInterfaceNameFromNetworkHandle(
+ client_network
+ )
+ self.server_mac_address = apf_utils.get_hardware_address(
+ self.serverDevice, self.server_iface_name
+ )
+ self.client_mac_address = apf_utils.get_hardware_address(
+ self.clientDevice, self.client_iface_name
+ )
+ self.server_ipv4_addresses = apf_utils.get_ipv4_addresses(
+ self.serverDevice, self.server_iface_name
+ )
+ self.client_ipv4_addresses = apf_utils.get_ipv4_addresses(
+ self.clientDevice, self.client_iface_name
+ )
+ self.server_ipv6_addresses = apf_utils.get_ipv6_addresses(
+ self.serverDevice, self.server_iface_name
+ )
+ self.client_ipv6_addresses = apf_utils.get_ipv6_addresses(
+ self.clientDevice, self.client_iface_name
+ )
+
+ # Enable doze mode to activate APF.
+ adb_utils.set_doze_mode(self.clientDevice, True)
+
+ def teardown_class(self):
+ adb_utils.set_doze_mode(self.clientDevice, False)
+ tether_utils.cleanup_tethering_for_upstream_type(
+ self.serverDevice, UpstreamType.NONE
+ )
+
+ def send_packet_and_expect_counter_increased(
+ self, packet: str, counter_name: str
+ ) -> None:
+ count_before_test = apf_utils.get_apf_counter(
+ self.clientDevice,
+ self.client_iface_name,
+ counter_name,
+ )
+ apf_utils.send_raw_packet_downstream(
+ self.serverDevice, self.server_iface_name, packet
+ )
+
+ assert_utils.expect_with_retry(
+ lambda: apf_utils.get_apf_counter(
+ self.clientDevice,
+ self.client_iface_name,
+ counter_name,
+ )
+ > count_before_test
+ )
+
+ def send_packet_and_expect_reply_received(
+ self, send_packet: str, counter_name: str, receive_packet: str
+ ) -> None:
+ try:
+ apf_utils.start_capture_packets(self.serverDevice, self.server_iface_name)
+
+ self.send_packet_and_expect_counter_increased(send_packet, counter_name)
+
+ assert_utils.expect_with_retry(
+ lambda: apf_utils.get_matched_packet_counts(
+ self.serverDevice, self.server_iface_name, receive_packet
+ )
+ == 1
+ )
+ finally:
+ apf_utils.stop_capture_packets(self.serverDevice, self.server_iface_name)
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index 415799c..55ac860 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -20,11 +20,6 @@
from net_tests_utils.host.python import adb_utils, assert_utils
-# Constants.
-ETHER_BROADCAST = "FFFFFFFFFFFF"
-ETH_P_ETHERCAT = "88A4"
-
-
class PatternNotFoundException(Exception):
"""Raised when the given pattern cannot be found."""
@@ -88,6 +83,69 @@
ad.log.debug("Getting apf counters: " + str(result))
return result
+def get_ipv4_addresses(
+ ad: android_device.AndroidDevice, iface_name: str
+) -> list[str]:
+ """Retrieves the IPv4 addresses of a given interface on an Android device.
+
+ This function executes an ADB shell command (`ip -4 address show`) to get the
+ network interface information and extracts the IPv4 address from the output.
+ If devices have no IPv4 address, raise PatternNotFoundException.
+
+ Args:
+ ad: The Android device object.
+ iface_name: The name of the network interface (e.g., "wlan0").
+
+ Returns:
+ The IPv4 addresses of the interface as a list of string.
+ Return empty list if no IPv4 address.
+ """
+ # output format:
+ # 54: wlan2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
+ # inet 192.168.195.162/24 brd 192.168.195.255 scope global wlan2
+ # valid_lft forever preferred_lft forever
+ # inet 192.168.1.1/24 brd 192.168.1.255 scope global wlan2
+ # valid_lft forever preferred_lft forever
+ output = adb_utils.adb_shell(ad, f"ip -4 address show {iface_name}")
+ pattern = r"inet\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/\d+"
+ matches = re.findall(pattern, output)
+
+ if matches:
+ return matches
+ else:
+ return []
+
+def get_ipv6_addresses(
+ ad: android_device.AndroidDevice, iface_name: str
+) -> list[str]:
+ """Retrieves the IPv6 addresses of a given interface on an Android device.
+
+ This function executes an ADB shell command (`ip -6 address show`) to get the
+ network interface information and extracts the IPv6 address from the output.
+ If devices have no IPv6 address, raise PatternNotFoundException.
+
+ Args:
+ ad: The Android device object.
+ iface_name: The name of the network interface (e.g., "wlan0").
+
+ Returns:
+ The IPv6 addresses of the interface as a list of string.
+ Return empty list if no IPv6 address.
+ """
+ # output format
+ # 54: wlan2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
+ # inet6 fe80::10a3:5dff:fe52:de32/64 scope link
+ # valid_lft forever preferred_lft forever
+ # inet6 fe80::1233:aadb:3d32:1234/64 scope link
+ # valid_lft forever preferred_lft forever
+ output = adb_utils.adb_shell(ad, f"ip -6 address show {iface_name}")
+ pattern = r"inet6\s+([0-9a-fA-F:]+)\/\d+"
+ matches = re.findall(pattern, output)
+
+ if matches:
+ return matches
+ else:
+ return []
def get_hardware_address(
ad: android_device.AndroidDevice, iface_name: str
@@ -120,26 +178,42 @@
"Cannot get hardware address for " + iface_name
)
+def is_packet_capture_supported(
+ ad: android_device.AndroidDevice,
+) -> bool:
-def send_broadcast_empty_ethercat_packet(
- ad: android_device.AndroidDevice, iface_name: str
-) -> None:
- """Transmits a broadcast empty EtherCat packet on the specified interface."""
+ try:
+ # Invoke the shell command with empty argument and see how NetworkStack respond.
+ # If supported, an IllegalArgumentException with help page will be printed.
+ assert_utils.expect_throws(
+ lambda: start_capture_packets(ad, ""),
+ assert_utils.UnexpectedBehaviorError
+ )
+ assert_utils.expect_throws(
+ lambda: stop_capture_packets(ad, ""),
+ assert_utils.UnexpectedBehaviorError
+ )
+ assert_utils.expect_throws(
+ lambda: get_matched_packet_counts(ad, "", ""),
+ assert_utils.UnexpectedBehaviorError
+ )
+ except assert_utils.UnexpectedExceptionError:
+ return False
- # Get the interface's MAC address.
- mac_address = get_hardware_address(ad, iface_name)
+ # If no UnsupportOperationException is thrown, regard it as supported
+ return True
- # TODO: Build packet by using scapy library.
- # Ethernet header (14 bytes).
- packet = ETHER_BROADCAST # Destination MAC (broadcast)
- packet += mac_address.replace(":", "") # Source MAC
- packet += ETH_P_ETHERCAT # EtherType (EtherCAT)
-
- # EtherCAT header (2 bytes) + 44 bytes of zero padding.
- packet += "00" * 46
-
- # Send the packet using a raw socket.
- send_raw_packet_downstream(ad, iface_name, packet)
+def is_send_raw_packet_downstream_supported(
+ ad: android_device.AndroidDevice,
+) -> bool:
+ try:
+ # Invoke the shell command with empty argument and see how NetworkStack respond.
+ # If supported, an IllegalArgumentException with help page will be printed.
+ send_raw_packet_downstream(ad, "", "")
+ except assert_utils.UnexpectedBehaviorError:
+ return True
+ except UnsupportedOperationException:
+ return False
def send_raw_packet_downstream(
@@ -174,25 +248,92 @@
representation of a packet starting from L2 header.
"""
- cmd = (
- "cmd network_stack send-raw-packet-downstream"
- f" {iface_name} {packet_in_hex}"
- )
+ cmd = f"cmd network_stack send-raw-packet-downstream {iface_name} {packet_in_hex}"
# Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
- try:
- output = adb_utils.adb_shell(ad, cmd)
- except AdbError as e:
- output = str(e.stdout)
- if output:
- if "Unknown command" in output:
- raise UnsupportedOperationException(
- "send-raw-packet-downstream command is not supported."
- )
+ adb_output = AdbOutputHandler(ad, cmd).get_output()
+ if adb_output:
raise assert_utils.UnexpectedBehaviorError(
- f"Got unexpected output: {output} for command: {cmd}."
+ f"Got unexpected output: {adb_output} for command: {cmd}."
)
+def start_capture_packets(
+ ad: android_device.AndroidDevice,
+ iface_name: str
+) -> None:
+ """Starts packet capturing on a specified network interface.
+
+ This function initiates packet capture on the given network interface of an
+ Android device using an ADB shell command. It handles potential errors
+ related to unsupported commands or unexpected output.
+ This command only supports downstream tethering interface.
+
+ Args:
+ ad: The Android device object.
+ iface_name: The name of the network interface (e.g., "wlan0").
+ """
+ cmd = f"cmd network_stack capture start {iface_name}"
+
+ # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+ adb_output = AdbOutputHandler(ad, cmd).get_output()
+ if adb_output != "success":
+ raise assert_utils.UnexpectedBehaviorError(
+ f"Got unexpected output: {adb_output} for command: {cmd}."
+ )
+
+def stop_capture_packets(
+ ad: android_device.AndroidDevice,
+ iface_name: str
+) -> None:
+ """Stops packet capturing on a specified network interface.
+
+ This function terminates packet capture on the given network interface of an
+ Android device using an ADB shell command. It handles potential errors
+ related to unsupported commands or unexpected output.
+
+ Args:
+ ad: The Android device object.
+ iface_name: The name of the network interface (e.g., "wlan0").
+ """
+ cmd = f"cmd network_stack capture stop {iface_name}"
+
+ # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+ adb_output = AdbOutputHandler(ad, cmd).get_output()
+ if adb_output != "success":
+ raise assert_utils.UnexpectedBehaviorError(
+ f"Got unexpected output: {adb_output} for command: {cmd}."
+ )
+
+def get_matched_packet_counts(
+ ad: android_device.AndroidDevice,
+ iface_name: str,
+ packet_in_hex: str
+) -> int:
+ """Gets the number of captured packets matching a specific hexadecimal pattern.
+
+ This function retrieves the count of captured packets on the specified
+ network interface that match a given hexadecimal pattern. It uses an ADB
+ shell command and handles potential errors related to unsupported commands,
+ unexpected output, or invalid output format.
+
+ Args:
+ ad: The Android device object.
+ iface_name: The name of the network interface (e.g., "wlan0").
+ packet_in_hex: The hexadecimal string representing the packet pattern.
+
+ Returns:
+ The number of matched packets as an integer.
+ """
+ cmd = f"cmd network_stack capture matched-packet-counts {iface_name} {packet_in_hex}"
+
+ # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+ adb_output = AdbOutputHandler(ad, cmd).get_output()
+ try:
+ return int(adb_output)
+ except ValueError as e:
+ raise assert_utils.UnexpectedBehaviorError(
+ f"Got unexpected exception: {e} for command: {cmd}."
+ )
@dataclass
class ApfCapabilities:
@@ -249,8 +390,24 @@
ad: android_device.AndroidDevice, iface_name: str, expected_version: int
) -> None:
caps = get_apf_capabilities(ad, iface_name)
- asserts.skip_if(
+ asserts.abort_class_if(
caps.apf_version_supported < expected_version,
f"Supported apf version {caps.apf_version_supported} < expected version"
f" {expected_version}",
)
+
+class AdbOutputHandler:
+ def __init__(self, ad, cmd):
+ self._ad = ad
+ self._cmd = cmd
+
+ def get_output(self) -> str:
+ try:
+ return adb_utils.adb_shell(self._ad, self._cmd)
+ except AdbError as e:
+ output = str(e.stdout)
+ if "Unknown command" in output:
+ raise UnsupportedOperationException(
+ f"{self._cmd} is not supported."
+ )
+ return output
\ No newline at end of file
diff --git a/staticlibs/testutils/host/python/assert_utils.py b/staticlibs/testutils/host/python/assert_utils.py
index da1bb9e..40094a2 100644
--- a/staticlibs/testutils/host/python/assert_utils.py
+++ b/staticlibs/testutils/host/python/assert_utils.py
@@ -19,6 +19,8 @@
class UnexpectedBehaviorError(Exception):
"""Raised when there is an unexpected behavior during applying a procedure."""
+class UnexpectedExceptionError(Exception):
+ """Raised when there is an unexpected exception throws during applying a procedure"""
def expect_with_retry(
predicate: Callable[[], bool],
@@ -41,3 +43,17 @@
raise UnexpectedBehaviorError(
"Predicate didn't become true after " + str(max_retries) + " retries."
)
+
+def expect_throws(runnable: callable, exception_class) -> None:
+ try:
+ runnable()
+ raise UnexpectedBehaviorError("Expected an exception, but none was thrown")
+ except exception_class:
+ pass
+ except UnexpectedBehaviorError as e:
+ raise e
+ except Exception as e:
+ raise UnexpectedExceptionError(
+ f"Expected exception of type {exception_class.__name__}, "
+ f"but got {type(e).__name__}: {e}"
+ )
\ No newline at end of file
diff --git a/staticlibs/testutils/host/python/multi_devices_test_base.py b/staticlibs/testutils/host/python/multi_devices_test_base.py
new file mode 100644
index 0000000..677329a
--- /dev/null
+++ b/staticlibs/testutils/host/python/multi_devices_test_base.py
@@ -0,0 +1,55 @@
+# 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.
+#
+# 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.
+
+from mobly import base_test
+from mobly import utils
+from mobly.controllers import android_device
+
+CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+
+
+class MultiDevicesTestBase(base_test.BaseTestClass):
+
+ def setup_class(self):
+ # Declare that two Android devices are needed.
+ self.clientDevice, self.serverDevice = self.register_controller(
+ android_device, min_number=2
+ )
+
+ def setup_device(device):
+ device.load_snippet(
+ "connectivity_multi_devices_snippet",
+ CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
+ )
+
+ # Set up devices in parallel to save time.
+ utils.concurrent_exec(
+ setup_device,
+ ((self.clientDevice,), (self.serverDevice,)),
+ max_workers=2,
+ raise_on_exception=True,
+ )
+ self.client = self.clientDevice.connectivity_multi_devices_snippet
diff --git a/staticlibs/testutils/host/python/packet_utils.py b/staticlibs/testutils/host/python/packet_utils.py
new file mode 100644
index 0000000..b613f03
--- /dev/null
+++ b/staticlibs/testutils/host/python/packet_utils.py
@@ -0,0 +1,70 @@
+# 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.
+from ipaddress import IPv4Address
+from socket import inet_aton
+
+ETHER_BROADCAST_MAC_ADDRESS = "FF:FF:FF:FF:FF:FF"
+ARP_REQUEST_OP = 1
+ARP_REPLY_OP = 2
+
+"""
+This variable defines a template for constructing ARP packets in hexadecimal format.
+It's used to provide the common fields for ARP packet, and replaced needed fields when constructing
+"""
+ARP_TEMPLATE = (
+ # Ether Header (14 bytes)
+ "{dst_mac}" + # DA
+ "{src_mac}" + # SA
+ "0806" + # ARP
+ # ARP Header (28 bytes)
+ "0001" + # Hardware type (Ethernet)
+ "0800" + # Protocol type (IPv4)
+ "06" + # hardware address length
+ "04" + # protocol address length
+ "{opcode}" + # opcode
+ "{sender_mac}" + # sender MAC
+ "{sender_ip}" + # sender IP
+ "{target_mac}" + # target MAC
+ "{target_ip}" # target IP
+)
+
+def construct_arp_packet(src_mac, dst_mac, src_ip, dst_ip, op) -> str:
+ """Constructs an ARP packet as a hexadecimal string.
+
+ This function creates an ARP packet by filling in the required fields
+ in a predefined ARP packet template.
+
+ Args:
+ src_mac: The MAC address of the sender. (e.g. "11:22:33:44:55:66")
+ dst_mac: The MAC address of the recipient. (e.g. "aa:bb:cc:dd:ee:ff")
+ src_ip: The IP address of the sender. (e.g. "1.1.1.1")
+ dst_ip: The IP address of the target machine. (e.g. "2.2.2.2")
+ op: The op code of the ARP packet, refer to ARP_*_OP
+
+ Returns:
+ A string representing the ARP packet in hexadecimal format.
+ """
+ # Replace the needed fields from packet template
+ arp_pkt = ARP_TEMPLATE.format(
+ dst_mac=dst_mac.replace(":",""),
+ src_mac=src_mac.replace(":",""),
+ opcode=str(op).rjust(4, "0"),
+ sender_mac=src_mac.replace(":",""),
+ sender_ip=inet_aton(src_ip).hex(),
+ target_mac=("000000000000" if op == ARP_REQUEST_OP else dst_mac.replace(":", "")),
+ target_ip=inet_aton(dst_ip).hex()
+ )
+
+ # always convert to upper case hex string
+ return arp_pkt.upper()
\ No newline at end of file
diff --git a/staticlibs/testutils/host/python/wifip2p_utils.py b/staticlibs/testutils/host/python/wifip2p_utils.py
index 8b4ffa5..ef6af75 100644
--- a/staticlibs/testutils/host/python/wifip2p_utils.py
+++ b/staticlibs/testutils/host/python/wifip2p_utils.py
@@ -14,11 +14,13 @@
from mobly import asserts
from mobly.controllers import android_device
+from net_tests_utils.host.python import tether_utils
def assume_wifi_p2p_test_preconditions(
server_device: android_device, client_device: android_device
) -> None:
+ """Preconditions check for running Wi-Fi P2P test."""
server = server_device.connectivity_multi_devices_snippet
client = client_device.connectivity_multi_devices_snippet
@@ -36,10 +38,51 @@
def setup_wifi_p2p_server_and_client(
server_device: android_device, client_device: android_device
) -> None:
- """Set up the Wi-Fi P2P server and client."""
+ """Set up the Wi-Fi P2P server and client, then connect them to establish a Wi-Fi P2P connection."""
+ server = server_device.connectivity_multi_devices_snippet
+ client = client_device.connectivity_multi_devices_snippet
+
# Start Wi-Fi P2P on both server and client.
- server_device.connectivity_multi_devices_snippet.startWifiP2p()
- client_device.connectivity_multi_devices_snippet.startWifiP2p()
+ server.startWifiP2p()
+ client.startWifiP2p()
+
+ # Get the current device name
+ server_name = server.getDeviceName()
+ client_name = client.getDeviceName()
+
+ # Generate Wi-Fi P2P group passphrase with random characters.
+ group_name = "DIRECT-" + tether_utils.generate_uuid32_base64()
+ group_passphrase = tether_utils.generate_uuid32_base64()
+
+ # Server creates a Wi-Fi P2P group
+ server.createGroup(group_name, group_passphrase)
+
+ # Start Wi-Fi P2p peers discovery on both devices
+ server.startPeersDiscovery()
+ client.startPeersDiscovery()
+
+ # Ensure the target device has been discovered
+ server_address = client.ensureDeviceDiscovered(server_name)
+ client_address = server.ensureDeviceDiscovered(client_name)
+
+ # Server invites the device to the group
+ server.inviteDeviceToGroup(group_name, group_passphrase, client_address)
+
+ # Wait for a p2p connection changed intent to ensure the invitation has been
+ # received.
+ client.waitForP2pConnectionChanged(True, group_name)
+ # Accept the group invitation
+ client.acceptGroupInvitation(server_address)
+
+ # Server waits for connection request from client and accept joining
+ server.waitForPeerConnectionRequestAndAcceptJoining(client_address)
+
+ # Wait for a p2p connection changed intent to ensure joining the group
+ client.waitForP2pConnectionChanged(False, group_name)
+
+ # Ensure Wi-Fi P2P connected on both devices
+ client.ensureDeviceConnected(server_name)
+ server.ensureDeviceConnected(client_name)
def cleanup_wifi_p2p(
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
index 9f28234..45c69c9 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
@@ -20,6 +20,7 @@
import com.android.testutils.FunctionalUtils.ThrowingRunnable
import com.android.testutils.FunctionalUtils.ThrowingSupplier
+import java.util.function.Consumer
import javax.annotation.CheckReturnValue
/**
@@ -73,18 +74,34 @@
* });
*/
+object TryTestConfig {
+ private var diagnosticsCollector: Consumer<Throwable>? = null
+
+ /**
+ * Set the diagnostics collector to be used in case of failure in [tryTest].
+ *
+ * @return The previous collector.
+ */
+ fun swapDiagnosticsCollector(collector: Consumer<Throwable>?): Consumer<Throwable>? {
+ val oldCollector = diagnosticsCollector
+ diagnosticsCollector = collector
+ return oldCollector
+ }
+
+ fun reportError(e: Throwable) {
+ diagnosticsCollector?.accept(e)
+ }
+}
+
@CheckReturnValue
fun <T> tryTest(block: () -> T) = TryExpr(
try {
Result.success(block())
} catch (e: Throwable) {
Result.failure(e)
- })
+ }, skipErrorReporting = false)
-// Some downstream branches have an older kotlin that doesn't know about value classes.
-// TODO : Change this to "value class" when aosp no longer merges into such branches.
-@Suppress("INLINE_CLASS_DEPRECATED")
-inline class TryExpr<T>(val result: Result<T>) {
+class TryExpr<T>(val result: Result<T>, val skipErrorReporting: Boolean) {
inline infix fun <reified E : Throwable> catch(block: (E) -> T): TryExpr<T> {
val originalException = result.exceptionOrNull()
if (originalException !is E) return this
@@ -92,23 +109,32 @@
Result.success(block(originalException))
} catch (e: Throwable) {
Result.failure(e)
- })
+ }, this.skipErrorReporting)
}
@CheckReturnValue
inline infix fun cleanupStep(block: () -> Unit): TryExpr<T> {
+ // Report errors before the cleanup step, but after catch blocks that may suppress it
+ val originalException = result.exceptionOrNull()
+ var nextSkipErrorReporting = skipErrorReporting
+ if (!skipErrorReporting && originalException != null) {
+ TryTestConfig.reportError(originalException)
+ nextSkipErrorReporting = true
+ }
try {
block()
} catch (e: Throwable) {
- val originalException = result.exceptionOrNull()
- return TryExpr(if (null == originalException) {
- Result.failure(e)
+ return if (null == originalException) {
+ if (!skipErrorReporting) {
+ TryTestConfig.reportError(e)
+ }
+ TryExpr(Result.failure(e), skipErrorReporting = true)
} else {
originalException.addSuppressed(e)
- Result.failure(originalException)
- })
+ TryExpr(Result.failure(originalException), true)
+ }
}
- return this
+ return TryExpr(result, nextSkipErrorReporting)
}
inline infix fun cleanup(block: () -> Unit): T = cleanupStep(block).result.getOrThrow()
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
index 1883387..176546a 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
@@ -20,11 +20,14 @@
import com.android.testutils.FunctionalUtils.ThrowingRunnable
import java.lang.reflect.Modifier
+import java.util.concurrent.TimeUnit
+import java.util.function.BooleanSupplier
import kotlin.system.measureTimeMillis
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue
+import kotlin.test.fail
private const val TAG = "Connectivity unit test"
@@ -118,4 +121,25 @@
val actualSet: HashSet<T> = HashSet(actual)
assertEquals(actualSet.size, actual.size, "actual list contains duplicates")
assertEquals(expectedSet, actualSet)
+}
+
+@JvmOverloads
+fun assertEventuallyTrue(
+ descr: String,
+ timeoutMs: Long,
+ pollIntervalMs: Long = 10L,
+ fn: BooleanSupplier
+) {
+ // This should use SystemClock.elapsedRealtime() since nanoTime does not include time in deep
+ // sleep, but this is a host-device library and SystemClock is Android-specific (not available
+ // on host). When waiting for a condition during tests the device would generally not go into
+ // deep sleep, and the polling sleep would go over the timeout anyway in that case, so this is
+ // fine.
+ val limit = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs)
+ while (!fn.asBoolean) {
+ if (System.nanoTime() > limit) {
+ fail(descr)
+ }
+ Thread.sleep(pollIntervalMs)
+ }
}
\ No newline at end of file
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index e95a81a..60a02fb 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -61,7 +61,7 @@
// Combine Connectivity, NetworkStack and Tethering jarjar rules for coverage target.
// The jarjar files are simply concatenated in the order specified in srcs.
// jarjar stops at the first matching rule, so order of concatenation affects the output.
-genrule {
+java_genrule {
name: "ConnectivityCoverageJarJarRules",
defaults: ["jarjar-rules-combine-defaults"],
srcs: [
@@ -78,7 +78,7 @@
name: "ConnectivityCoverageTestsLib",
min_sdk_version: "30",
static_libs: [
- "FrameworksNetTestsLib",
+ "ConnectivityUnitTestsLib",
"NetdStaticLibTestsLib",
"NetworkStaticLibTestsLib",
"NetworkStackTestsLib",
@@ -147,6 +147,7 @@
// meaning @hide APIs in framework-connectivity are resolved before @SystemApi
// stubs in framework
"framework-connectivity.impl",
+ "framework-connectivity-b.impl",
"framework-connectivity-t.impl",
"framework-tethering.impl",
"framework",
diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
index 8cef6aa..17f5e96 100644
--- a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
@@ -28,18 +28,26 @@
import android.net.NetworkStats.SET_DEFAULT
import android.net.NetworkStats.SET_FOREGROUND
import android.net.NetworkStats.TAG_NONE
+import android.os.Build
import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.assertNetworkStatsEquals
import com.android.testutils.assertParcelingIsLossless
import kotlin.test.assertEquals
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-@RunWith(JUnit4::class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
class NetworkStatsApiTest {
+ @get:Rule
+ val ignoreRule = DevSdkIgnoreRule()
private val testStatsEmpty = NetworkStats(0L, 0)
// Note that these variables need to be initialized outside of constructor, initialize
@@ -49,6 +57,7 @@
// be merged if performing add on these 2 stats.
private lateinit var testStats1: NetworkStats
private lateinit var testStats2: NetworkStats
+ private lateinit var expectedEntriesInStats2: List<Entry>
// This is a result of adding stats1 and stats2, while the merging of common key items is
// subject to test later, this should not be initialized with for a loop to add stats1
@@ -84,19 +93,23 @@
METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 1, 6, 2, 0))
assertEquals(8, testStats1.size())
- testStats2 = NetworkStats(0L, 0)
- // Entries which are common for set1 and set2.
- .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1))
- .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45))
- .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7))
- .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0))
- // Entry which only appears in set2.
- .addEntry(Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+ expectedEntriesInStats2 = listOf(
+ // Entries which are common for set1 and set2.
+ Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1),
+ Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45),
+ Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7),
+ Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0),
+ // Entry which only appears in set2.
+ Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+ testStats2 = NetworkStats(0L, 5)
+ for (entry in expectedEntriesInStats2) {
+ testStats2 = testStats2.addEntry(entry)
+ }
assertEquals(5, testStats2.size())
testStats3 = NetworkStats(0L, 9)
@@ -125,18 +138,6 @@
@Test
fun testAddEntry() {
- val expectedEntriesInStats2 = arrayOf(
- Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1),
- Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45),
- Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7),
- Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0),
- Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
-
// While testStats* are already initialized with addEntry, verify content added
// matches expectation.
for (i in expectedEntriesInStats2.indices) {
@@ -150,6 +151,27 @@
assertEquals(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16, -2, 9, 1, 9),
stats.getValues(3, null))
+
+ // Verify the original ststs object is not altered.
+ for (i in expectedEntriesInStats2.indices) {
+ val entry = testStats2.getValues(i, null)
+ assertEquals(expectedEntriesInStats2[i], entry)
+ }
+ }
+
+ @ConnectivityModuleTest
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2) // Mainlined NetworkStats only runs on T+
+ @Test
+ fun testAddEntries() {
+ val baseStats = NetworkStats(0L, 1)
+ .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9))
+ val statsUnderTest = baseStats.addEntries(expectedEntriesInStats2)
+ // Assume the correctness of addEntry is verified in other tests.
+ val expectedStats = testStats2
+ .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9))
+ assertNetworkStatsEquals(expectedStats, statsUnderTest)
}
@Test
diff --git a/tests/cts/hostside-network-policy/Android.bp b/tests/cts/hostside-network-policy/Android.bp
deleted file mode 100644
index c3ce0b9..0000000
--- a/tests/cts/hostside-network-policy/Android.bp
+++ /dev/null
@@ -1,46 +0,0 @@
-// 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_framework_backstage_power",
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_test_host {
- name: "CtsHostsideNetworkPolicyTests",
- defaults: ["cts_defaults"],
- // Only compile source java files in this apk.
- srcs: [
- "src/**/*.java",
- ":ArgumentConstants",
- ],
- libs: [
- "cts-tradefed",
- "tradefed",
- ],
- static_libs: [
- "modules-utils-build-testing",
- ],
- // Tag this module as a cts test artifact
- test_suites: [
- "cts",
- "general-tests",
- "sts",
- ],
- data: [
- ":CtsHostsideNetworkPolicyTestsApp",
- ":CtsHostsideNetworkPolicyTestsApp2",
- ],
- per_testcase_directory: true,
-}
diff --git a/tests/cts/hostside-network-policy/AndroidTest.xml b/tests/cts/hostside-network-policy/AndroidTest.xml
deleted file mode 100644
index 44f77f8..0000000
--- a/tests/cts/hostside-network-policy/AndroidTest.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<?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 CTS network policy host test cases">
- <option name="test-suite-tag" value="cts" />
- <option name="config-descriptor:metadata" key="component" value="networking" />
- <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
- <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
- <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
- <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
-
- <target_preparer class="com.android.compatibility.common.tradefed.targetprep.LocationCheck" />
- <target_preparer class="com.android.cts.netpolicy.NetworkPolicyTestsPreparer" />
-
- <!-- Enabling change id ALLOW_TEST_API_ACCESS allows that package to access @TestApi methods -->
- <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
- <option name="run-command" value="am compat enable ALLOW_TEST_API_ACCESS com.android.cts.netpolicy.hostside.app2" />
- <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS com.android.cts.netpolicy.hostside.app2" />
- <option name="teardown-command" value="cmd power set-mode 0" />
- <option name="teardown-command" value="cmd battery reset" />
- <option name="teardown-command" value="cmd netpolicy stop-watching" />
- </target_preparer>
-
- <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
- <option name="force-skip-system-props" value="true" />
- <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
- <option name="set-global-setting" key="low_power_standby_enabled" value="0" />
- </target_preparer>
-
- <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
- <option name="jar" value="CtsHostsideNetworkPolicyTests.jar" />
- <option name="runtime-hint" value="3m56s" />
- </test>
-
- <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
- <option name="directory-keys" value="/sdcard/CtsHostsideNetworkPolicyTests" />
- <option name="collect-on-run-ended-only" value="true" />
- </metrics_collector>
-</configuration>
diff --git a/tests/cts/hostside-network-policy/OWNERS b/tests/cts/hostside-network-policy/OWNERS
deleted file mode 100644
index ea83e61..0000000
--- a/tests/cts/hostside-network-policy/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-# Bug component: 61373
-# Inherits parent owners
-include platform/frameworks/base:/services/core/java/com/android/server/net/OWNERS
diff --git a/tests/cts/hostside-network-policy/TEST_MAPPING b/tests/cts/hostside-network-policy/TEST_MAPPING
deleted file mode 100644
index 57ac4f7..0000000
--- a/tests/cts/hostside-network-policy/TEST_MAPPING
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "presubmit-large": [
- {
- "name": "CtsHostsideNetworkPolicyTests",
- "options": [
- {
- "exclude-annotation": "android.platform.test.annotations.FlakyTest"
- },
- {
- "exclude-annotation": "android.platform.test.annotations.RequiresDevice"
- }
- ]
- }
- ],
- "postsubmit": [
- {
- // Postsubmit on virtual devices to monitor flakiness of all tests that don't require a
- // physical device
- "name": "CtsHostsideNetworkPolicyTests",
- "options": [
- {
- "exclude-annotation": "androidx.test.filters.RequiresDevice"
- }
- ]
- }
- ]
-}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl
deleted file mode 100644
index 068d9d8..0000000
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/IMyService.aidl
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import android.app.job.JobInfo;
-
-import com.android.cts.netpolicy.hostside.INetworkCallback;
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-
-interface IMyService {
- void registerBroadcastReceiver();
- int getCounters(String receiverName, String action);
- NetworkCheckResult checkNetworkStatus(String customUrl);
- String getRestrictBackgroundStatus();
- void sendNotification(int notificationId, String notificationType);
- void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
- void unregisterNetworkCallback();
- int scheduleJob(in JobInfo jobInfo);
-}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl
deleted file mode 100644
index 38efc7b..0000000
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkCallback.aidl
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import android.net.Network;
-import android.net.NetworkCapabilities;
-
-interface INetworkCallback {
- void onBlockedStatusChanged(in Network network, boolean blocked);
- void onAvailable(in Network network);
- void onLost(in Network network);
- void onCapabilitiesChanged(in Network network, in NetworkCapabilities cap);
-}
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl b/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl
deleted file mode 100644
index c6b7a1c..0000000
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/INetworkStateObserver.aidl
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import android.net.NetworkInfo;
-
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-
-interface INetworkStateObserver {
- void onNetworkStateChecked(int resultCode, in NetworkCheckResult networkCheckResult);
-
- const int RESULT_SUCCESS_NETWORK_STATE_CHECKED = 0;
- const int RESULT_ERROR_UNEXPECTED_PROC_STATE = 1;
- const int RESULT_ERROR_UNEXPECTED_CAPABILITIES = 2;
- const int RESULT_ERROR_OTHER = 3;
-}
\ No newline at end of file
diff --git a/tests/cts/hostside-network-policy/app/Android.bp b/tests/cts/hostside-network-policy/app/Android.bp
deleted file mode 100644
index a31c843..0000000
--- a/tests/cts/hostside-network-policy/app/Android.bp
+++ /dev/null
@@ -1,57 +0,0 @@
-//
-// 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_framework_backstage_power",
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_defaults {
- name: "CtsHostsideNetworkPolicyTestsAppDefaults",
- platform_apis: true,
- static_libs: [
- "CtsHostsideNetworkPolicyTestsAidl",
- "androidx.test.ext.junit",
- "androidx.test.rules",
- "androidx.test.uiautomator_uiautomator",
- "compatibility-device-util-axt",
- "cts-net-utils",
- "ctstestrunner-axt",
- "modules-utils-build",
- ],
- libs: [
- "android.test.runner",
- "android.test.base",
- ],
- srcs: [
- "src/**/*.java",
- ":ArgumentConstants",
- ],
- // Tag this module as a cts test artifact
- test_suites: [
- "general-tests",
- "sts",
- ],
-}
-
-android_test_helper_app {
- name: "CtsHostsideNetworkPolicyTestsApp",
- defaults: [
- "cts_support_defaults",
- "framework-connectivity-test-defaults",
- "CtsHostsideNetworkPolicyTestsAppDefaults",
- ],
-}
diff --git a/tests/cts/hostside-network-policy/app/AndroidManifest.xml b/tests/cts/hostside-network-policy/app/AndroidManifest.xml
deleted file mode 100644
index f19e35f..0000000
--- a/tests/cts/hostside-network-policy/app/AndroidManifest.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-<?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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.cts.netpolicy.hostside">
-
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
- <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
- <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
- <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
- <uses-permission android:name="android.permission.INTERNET"/>
- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
- <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
- <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
- <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
- <uses-permission android:name="android.permission.WAKE_LOCK" />
-
- <application android:requestLegacyExternalStorage="true">
- <uses-library android:name="android.test.runner"/>
- <service android:name=".MyNotificationListenerService"
- android:label="MyNotificationListenerService"
- android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
- android:exported="true">
- <intent-filter>
- <action android:name="android.service.notification.NotificationListenerService"/>
- </intent-filter>
- </service>
- </application>
-
- <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.cts.netpolicy.hostside"/>
-
-</manifest>
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java
deleted file mode 100644
index 19e4364..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractAppIdleTestCase.java
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-
-import static org.junit.Assert.assertEquals;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered tests on idle apps.
- */
-@RequiredProperties({APP_STANDBY_MODE})
-abstract class AbstractAppIdleTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
- @Before
- public final void setUp() throws Exception {
- super.setUp();
-
- // Set initial state.
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- setAppIdle(false);
- turnBatteryOn();
-
- registerBroadcastReceiver();
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
-
- resetBatteryState();
- setAppIdle(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_enabled() throws Exception {
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- // Make sure foreground app doesn't lose access upon enabling it.
- setAppIdle(true);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- finishActivity();
- assertAppIdle(false); // verify - not idle anymore, since activity was launched...
- assertBackgroundNetworkAccess(true);
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- // Same for foreground service.
- setAppIdle(true);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- stopForegroundService();
- assertAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- // Set Idle after foreground service start.
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- setAppIdle(true);
- addPowerSaveModeWhitelist(TEST_PKG);
- removePowerSaveModeWhitelist(TEST_PKG);
- assertForegroundServiceNetworkAccess();
- stopForegroundService();
- assertAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- }
-
- @Test
- public void testBackgroundNetworkAccess_whitelisted() throws Exception {
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertAppIdle(false); // verify - not idle anymore, since whitelisted
- assertBackgroundNetworkAccess(true);
-
- setAppIdleNoAssert(true);
- assertAppIdle(false); // app is still whitelisted
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertAppIdle(true); // verify - idle again, once whitelisted was removed
- assertBackgroundNetworkAccess(false);
-
- setAppIdle(true);
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertAppIdle(false); // verify - not idle anymore, since whitelisted
- assertBackgroundNetworkAccess(true);
-
- setAppIdleNoAssert(true);
- assertAppIdle(false); // app is still whitelisted
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertAppIdle(true); // verify - idle again, once whitelisted was removed
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
-
- // verify - no whitelist, no access!
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_tempWhitelisted() throws Exception {
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(true);
- // Wait until the whitelist duration is expired.
- SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_disabled() throws Exception {
- assertBackgroundNetworkAccess(true);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- }
-
- @RequiredProperties({BATTERY_SAVER_MODE})
- @Test
- public void testAppIdleNetworkAccess_whenCharging() throws Exception {
- // Check that app is paroled when charging
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
- turnBatteryOff();
- assertBackgroundNetworkAccess(true);
- turnBatteryOn();
- assertBackgroundNetworkAccess(false);
-
- // Check that app is restricted when not idle but power-save is on
- setAppIdle(false);
- assertBackgroundNetworkAccess(true);
- setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
- // Use setBatterySaverMode API to leave power-save mode instead of plugging in charger
- setBatterySaverMode(false);
- turnBatteryOff();
- assertBackgroundNetworkAccess(true);
-
- // And when no longer charging, it still has network access, since it's not idle
- turnBatteryOn();
- assertBackgroundNetworkAccess(true);
- }
-
- @Test
- public void testAppIdleNetworkAccess_idleWhitelisted() throws Exception {
- setAppIdle(true);
- assertAppIdle(true);
- assertBackgroundNetworkAccess(false);
-
- addAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(true);
-
- removeAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
-
- // Make sure whitelisting a random app doesn't affect the tested app.
- addAppIdleWhitelist(mUid + 1);
- assertBackgroundNetworkAccess(false);
- removeAppIdleWhitelist(mUid + 1);
- }
-
- @Test
- public void testAppIdle_toast() throws Exception {
- setAppIdle(true);
- assertAppIdle(true);
- assertEquals("Shown", showToast());
- assertAppIdle(true);
- // Wait for a couple of seconds for the toast to actually be shown
- SystemClock.sleep(2000);
- assertAppIdle(true);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java
deleted file mode 100644
index ae226e2..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractBatterySaverModeTestCase.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered Battery Saver Mode tests.
- */
-@RequiredProperties({BATTERY_SAVER_MODE})
-abstract class AbstractBatterySaverModeTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
- @Before
- public final void setUp() throws Exception {
- super.setUp();
-
- // Set initial state.
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- setBatterySaverMode(false);
-
- registerBroadcastReceiver();
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
-
- setBatterySaverMode(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_enabled() throws Exception {
- setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
-
- // Make sure foreground app doesn't lose access upon Battery Saver.
- setBatterySaverMode(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- setBatterySaverMode(true);
- assertTopNetworkAccess(true);
-
- // Although it should not have access while the screen is off.
- turnScreenOff();
- assertBackgroundNetworkAccess(false);
- turnScreenOn();
- assertTopNetworkAccess(true);
-
- // Goes back to background state.
- finishActivity();
- assertBackgroundNetworkAccess(false);
-
- // Make sure foreground service doesn't lose access upon enabling Battery Saver.
- setBatterySaverMode(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- setBatterySaverMode(true);
- assertForegroundServiceNetworkAccess();
- stopForegroundService();
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_whitelisted() throws Exception {
- setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_disabled() throws Exception {
- assertBackgroundNetworkAccess(true);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
deleted file mode 100644
index 00f67f4..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDefaultRestrictionsTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for default, always-on network restrictions.
- */
-abstract class AbstractDefaultRestrictionsTest extends AbstractRestrictBackgroundNetworkTestCase {
-
- @Before
- public final void setUp() throws Exception {
- super.setUp();
-
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-
- registerBroadcastReceiver();
- assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
-
- stopApp();
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- }
-
- @Test
- public void testFgsNetworkAccess() throws Exception {
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- SystemClock.sleep(mProcessStateTransitionShortDelayMs);
- assertNetworkAccess(false, null);
-
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- }
-
- @Test
- public void testActivityNetworkAccess() throws Exception {
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- SystemClock.sleep(mProcessStateTransitionShortDelayMs);
- assertNetworkAccess(false, null);
-
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- }
-
- @Test
- public void testBackgroundNetworkAccess_inFullAllowlist() throws Exception {
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- SystemClock.sleep(mProcessStateTransitionShortDelayMs);
- assertNetworkAccess(false, null);
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- assertNetworkAccess(true, null);
- }
-
- @Test
- public void testBackgroundNetworkAccess_inExceptIdleAllowlist() throws Exception {
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- SystemClock.sleep(mProcessStateTransitionShortDelayMs);
- assertNetworkAccess(false, null);
-
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- assertNetworkAccess(true, null);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java
deleted file mode 100644
index 0c8cb70..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractDozeModeTestCase.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.NOT_LOW_RAM_DEVICE;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered Doze Mode tests.
- */
-@RequiredProperties({DOZE_MODE})
-abstract class AbstractDozeModeTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
- @Before
- public final void setUp() throws Exception {
- super.setUp();
-
- // Set initial state.
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- setDozeMode(false);
-
- registerBroadcastReceiver();
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
-
- setDozeMode(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_enabled() throws Exception {
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
-
- // Make sure foreground service doesn't lose network access upon enabling doze.
- setDozeMode(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- setDozeMode(true);
- assertForegroundServiceNetworkAccess();
- stopForegroundService();
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_whitelisted() throws Exception {
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testBackgroundNetworkAccess_disabled() throws Exception {
- assertBackgroundNetworkAccess(true);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- }
-
- @RequiredProperties({NOT_LOW_RAM_DEVICE})
- @Test
- public void testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction()
- throws Exception {
- setPendingIntentAllowlistDuration(NETWORK_TIMEOUT_MS);
- try {
- registerNotificationListenerService();
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
-
- testNotification(4, NOTIFICATION_TYPE_CONTENT);
- testNotification(8, NOTIFICATION_TYPE_DELETE);
- testNotification(15, NOTIFICATION_TYPE_FULL_SCREEN);
- testNotification(16, NOTIFICATION_TYPE_BUNDLE);
- testNotification(23, NOTIFICATION_TYPE_ACTION);
- testNotification(42, NOTIFICATION_TYPE_ACTION_BUNDLE);
- testNotification(108, NOTIFICATION_TYPE_ACTION_REMOTE_INPUT);
- } finally {
- resetDeviceIdleSettings();
- }
- }
-
- private void testNotification(int id, String type) throws Exception {
- sendNotification(id, type);
- assertBackgroundNetworkAccess(true);
- if (type.equals(NOTIFICATION_TYPE_ACTION)) {
- // Make sure access is disabled after it expires. Since this check considerably slows
- // downs the CTS tests, do it just once.
- SystemClock.sleep(NETWORK_TIMEOUT_MS);
- assertBackgroundNetworkAccess(false);
- }
- }
-
- // Must override so it only tests foreground service - once an app goes to foreground, device
- // leaves Doze Mode.
- @Override
- protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception {
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- stopForegroundService();
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java
deleted file mode 100644
index 5435920..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractExpeditedJobTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class AbstractExpeditedJobTest extends AbstractRestrictBackgroundNetworkTestCase {
- @Before
- public final void setUp() throws Exception {
- super.setUp();
- resetDeviceState();
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
- resetDeviceState();
- }
-
- private void resetDeviceState() throws Exception {
- resetBatteryState();
- setBatterySaverMode(false);
- setRestrictBackground(false);
- setAppIdle(false);
- setDozeMode(false);
- }
-
- @Test
- @RequiredProperties({BATTERY_SAVER_MODE})
- public void testNetworkAccess_batterySaverMode() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNetworkAccess();
- }
-
- @Test
- @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
- public void testNetworkAccess_dataSaverMode() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setRestrictBackground(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNoNetworkAccess();
- }
-
- @Test
- @RequiredProperties({APP_STANDBY_MODE})
- public void testNetworkAccess_appIdleState() throws Exception {
- turnBatteryOn();
- setAppIdle(false);
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setAppIdle(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNetworkAccess();
- }
-
- @Test
- @RequiredProperties({DOZE_MODE})
- public void testNetworkAccess_dozeMode() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNetworkAccess();
- }
-
- @Test
- @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK})
- public void testNetworkAccess_dataAndBatterySaverMode() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setRestrictBackground(true);
- setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNoNetworkAccess();
- }
-
- @Test
- @RequiredProperties({DOZE_MODE, DATA_SAVER_MODE, METERED_NETWORK})
- public void testNetworkAccess_dozeAndDataSaverMode() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setRestrictBackground(true);
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNoNetworkAccess();
- }
-
- @Test
- @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK, DOZE_MODE,
- APP_STANDBY_MODE})
- public void testNetworkAccess_allRestrictionsEnabled() throws Exception {
- assertBackgroundNetworkAccess(true);
- assertExpeditedJobHasNetworkAccess();
-
- setRestrictBackground(true);
- setBatterySaverMode(true);
- setAppIdle(true);
- setDozeMode(true);
- assertBackgroundNetworkAccess(false);
- assertExpeditedJobHasNoNetworkAccess();
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
deleted file mode 100644
index 0f5f58c..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ /dev/null
@@ -1,1165 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_TOP;
-import static android.app.job.JobScheduler.RESULT_SUCCESS;
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-import static android.os.BatteryManager.BATTERY_PLUGGED_ANY;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.executeShellCommand;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.forceRunJob;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getConnectivityManager;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getContext;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getInstrumentation;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackgroundInternal;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.annotation.NonNull;
-import android.app.Instrumentation;
-import android.app.NotificationManager;
-import android.app.job.JobInfo;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.NetworkInfo.DetailedState;
-import android.net.NetworkInfo.State;
-import android.net.NetworkRequest;
-import android.os.BatteryManager;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.PowerManager;
-import android.os.RemoteCallback;
-import android.os.SystemClock;
-import android.provider.DeviceConfig;
-import android.service.notification.NotificationListenerService;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.Nullable;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.compatibility.common.util.AmUtils;
-import com.android.compatibility.common.util.BatteryUtils;
-import com.android.compatibility.common.util.DeviceConfigStateHelper;
-import com.android.compatibility.common.util.ThrowingRunnable;
-import com.android.modules.utils.build.SdkLevel;
-
-import org.junit.Rule;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
-
-/**
- * Superclass for tests related to background network restrictions.
- */
-@RunWith(NetworkPolicyTestRunner.class)
-public abstract class AbstractRestrictBackgroundNetworkTestCase {
- public static final String TAG = "RestrictBackgroundNetworkTests";
-
- protected static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
- protected static final String TEST_APP2_PKG = "com.android.cts.netpolicy.hostside.app2";
- // TODO(b/321797685): Configure it via device-config once it is available.
- protected final long mProcessStateTransitionLongDelayMs =
- useDifferentDelaysForBackgroundChain() ? TimeUnit.SECONDS.toMillis(20)
- : TimeUnit.SECONDS.toMillis(5);
- protected final long mProcessStateTransitionShortDelayMs =
- useDifferentDelaysForBackgroundChain() ? TimeUnit.SECONDS.toMillis(2)
- : TimeUnit.SECONDS.toMillis(5);
-
- private static final String TEST_APP2_ACTIVITY_CLASS = TEST_APP2_PKG + ".MyActivity";
- private static final String TEST_APP2_SERVICE_CLASS = TEST_APP2_PKG + ".MyForegroundService";
- private static final String TEST_APP2_JOB_SERVICE_CLASS = TEST_APP2_PKG + ".MyJobService";
-
- private static final ComponentName TEST_JOB_COMPONENT = new ComponentName(
- TEST_APP2_PKG, TEST_APP2_JOB_SERVICE_CLASS);
- private static final int TEST_JOB_ID = 7357437;
-
- private static final int SLEEP_TIME_SEC = 1;
-
- // Constants below must match values defined on app2's Common.java
- private static final String MANIFEST_RECEIVER = "ManifestReceiver";
- private static final String DYNAMIC_RECEIVER = "DynamicReceiver";
- private static final String ACTION_FINISH_ACTIVITY =
- "com.android.cts.netpolicy.hostside.app2.action.FINISH_ACTIVITY";
- private static final String ACTION_FINISH_JOB =
- "com.android.cts.netpolicy.hostside.app2.action.FINISH_JOB";
- // Copied from com.android.server.net.NetworkPolicyManagerService class
- private static final String ACTION_SNOOZE_WARNING =
- "com.android.server.net.action.SNOOZE_WARNING";
-
- private static final String ACTION_RECEIVER_READY =
- "com.android.cts.netpolicy.hostside.app2.action.RECEIVER_READY";
- static final String ACTION_SHOW_TOAST =
- "com.android.cts.netpolicy.hostside.app2.action.SHOW_TOAST";
-
- protected static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
- protected static final String NOTIFICATION_TYPE_DELETE = "DELETE";
- protected static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
- protected static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE";
- protected static final String NOTIFICATION_TYPE_ACTION = "ACTION";
- protected static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
- protected static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
-
- private static final String NETWORK_STATUS_SEPARATOR = "\\|";
- private static final int SECOND_IN_MS = 1000;
- static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
-
- private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
- private static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
- private static final String KEY_CUSTOM_URL = TEST_PKG + ".custom_url";
-
- private static final String EMPTY_STRING = "";
-
- protected static final int TYPE_COMPONENT_ACTIVTIY = 0;
- protected static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
- protected static final int TYPE_EXPEDITED_JOB = 2;
-
- private static final int BATTERY_STATE_TIMEOUT_MS = 5000;
- private static final int BATTERY_STATE_CHECK_INTERVAL_MS = 500;
-
- private static final int ACTIVITY_NETWORK_STATE_TIMEOUT_MS = 10_000;
- private static final int JOB_NETWORK_STATE_TIMEOUT_MS = 10_000;
- private static final int LAUNCH_ACTIVITY_TIMEOUT_MS = 10_000;
-
- // Must be higher than NETWORK_TIMEOUT_MS
- private static final int ORDERED_BROADCAST_TIMEOUT_MS = NETWORK_TIMEOUT_MS * 4;
-
- private static final IntentFilter BATTERY_CHANGED_FILTER =
- new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
-
- protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec
-
- private static final long BROADCAST_TIMEOUT_MS = 5_000;
-
- protected Context mContext;
- protected Instrumentation mInstrumentation;
- protected ConnectivityManager mCm;
- protected int mUid;
- private int mMyUid;
- private @Nullable String mCustomUrl;
- private MyServiceClient mServiceClient;
- private DeviceConfigStateHelper mDeviceIdleDeviceConfigStateHelper;
- private PowerManager mPowerManager;
- private PowerManager.WakeLock mLock;
-
- @Rule
- public final RuleChain mRuleChain = RuleChain.outerRule(new RequiredPropertiesRule())
- .around(new MeterednessConfigurationRule());
-
- protected void setUp() throws Exception {
- mInstrumentation = getInstrumentation();
- mContext = getContext();
- mCm = getConnectivityManager();
- mDeviceIdleDeviceConfigStateHelper =
- new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_DEVICE_IDLE);
- mUid = getUid(TEST_APP2_PKG);
- mMyUid = getUid(mContext.getPackageName());
- mServiceClient = new MyServiceClient(mContext);
-
- final Bundle args = InstrumentationRegistry.getArguments();
- mCustomUrl = args.getString(ARG_CONNECTION_CHECK_CUSTOM_URL);
- if (mCustomUrl != null) {
- Log.d(TAG, "Using custom URL " + mCustomUrl + " for network checks");
- }
-
- final int bindPriorityFlags;
- if (Boolean.valueOf(args.getString(ARG_WAIVE_BIND_PRIORITY, "false"))) {
- bindPriorityFlags = Context.BIND_WAIVE_PRIORITY;
- } else {
- bindPriorityFlags = Context.BIND_NOT_FOREGROUND;
- }
- mServiceClient.bind(bindPriorityFlags);
-
- mPowerManager = mContext.getSystemService(PowerManager.class);
- executeShellCommand("cmd netpolicy start-watching " + mUid);
- // Some of the test cases assume that Data saver mode is initially disabled, which might not
- // always be the case. Therefore, explicitly disable it before running the tests.
- // Invoke setRestrictBackgroundInternal() directly instead of going through
- // setRestrictBackground(), as some devices do not fully support the Data saver mode but
- // still have certain parts of it enabled by default.
- setRestrictBackgroundInternal(false);
- setAppIdle(false);
- mLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
-
- Log.i(TAG, "Apps status:\n"
- + "\ttest app: uid=" + mMyUid + ", state=" + getProcessStateByUid(mMyUid) + "\n"
- + "\tapp2: uid=" + mUid + ", state=" + getProcessStateByUid(mUid));
- }
-
- protected void tearDown() throws Exception {
- executeShellCommand("cmd netpolicy stop-watching");
- mServiceClient.unbind();
- final PowerManager.WakeLock lock = mLock;
- if (null != lock && lock.isHeld()) lock.release();
- }
-
- /**
- * Check if the feature blocking network for top_sleeping and lower priority proc-states is
- * enabled. This is a manual check because the feature flag infrastructure may not be available
- * in all the branches that will get this code.
- * TODO: b/322115994 - Use @RequiresFlagsEnabled with
- * Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE once the tests are moved to cts.
- */
- protected boolean isNetworkBlockedForTopSleepingAndAbove() {
- if (!SdkLevel.isAtLeastV()) {
- return false;
- }
- final String output = executeShellCommand("device_config get backstage_power"
- + " com.android.server.net.network_blocked_for_top_sleeping_and_above");
- return Boolean.parseBoolean(output);
- }
-
- /**
- * Check if the flag to use different delays for sensitive proc-states is enabled.
- * This is a manual check because the feature flag infrastructure may not be available
- * in all the branches that will get this code.
- * TODO: b/322115994 - Use @RequiresFlagsEnabled with
- * Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN once the tests are moved to cts.
- */
- private boolean useDifferentDelaysForBackgroundChain() {
- if (!SdkLevel.isAtLeastV()) {
- return false;
- }
- final String output = executeShellCommand("device_config get backstage_power"
- + " com.android.server.net.use_different_delays_for_background_chain");
- return Boolean.parseBoolean(output);
- }
-
- protected int getUid(String packageName) throws Exception {
- return mContext.getPackageManager().getPackageUid(packageName, 0);
- }
-
- protected void assertRestrictBackgroundChangedReceived(int expectedCount) throws Exception {
- assertRestrictBackgroundChangedReceived(DYNAMIC_RECEIVER, expectedCount);
- assertRestrictBackgroundChangedReceived(MANIFEST_RECEIVER, 0);
- }
-
- protected void assertRestrictBackgroundChangedReceived(String receiverName, int expectedCount)
- throws Exception {
- int attempts = 0;
- int count = 0;
- final int maxAttempts = 5;
- do {
- attempts++;
- count = getNumberBroadcastsReceived(receiverName, ACTION_RESTRICT_BACKGROUND_CHANGED);
- assertFalse("Expected count " + expectedCount + " but actual is " + count,
- count > expectedCount);
- if (count == expectedCount) {
- break;
- }
- Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
- + attempts + " attempts; sleeping "
- + SLEEP_TIME_SEC + " seconds before trying again");
- // No sleep after the last turn
- if (attempts <= maxAttempts) {
- SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
- }
- } while (attempts <= maxAttempts);
- assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
- + maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count);
- }
-
- protected void assertSnoozeWarningNotReceived() throws Exception {
- // Wait for a while to take broadcast queue delays into account
- SystemClock.sleep(BROADCAST_TIMEOUT_MS);
- assertEquals(0, getNumberBroadcastsReceived(DYNAMIC_RECEIVER, ACTION_SNOOZE_WARNING));
- }
-
- protected String sendOrderedBroadcast(Intent intent) throws Exception {
- return sendOrderedBroadcast(intent, ORDERED_BROADCAST_TIMEOUT_MS);
- }
-
- protected String sendOrderedBroadcast(Intent intent, int timeoutMs) throws Exception {
- final LinkedBlockingQueue<String> result = new LinkedBlockingQueue<>(1);
- Log.d(TAG, "Sending ordered broadcast: " + intent);
- mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- final String resultData = getResultData();
- if (resultData == null) {
- Log.e(TAG, "Received null data from ordered intent");
- // Offer an empty string so that the code waiting for the result can return.
- result.offer(EMPTY_STRING);
- return;
- }
- result.offer(resultData);
- }
- }, null, 0, null, null);
-
- final String resultData = result.poll(timeoutMs, TimeUnit.MILLISECONDS);
- Log.d(TAG, "Ordered broadcast response after " + timeoutMs + "ms: " + resultData );
- return resultData;
- }
-
- protected int getNumberBroadcastsReceived(String receiverName, String action) throws Exception {
- return mServiceClient.getCounters(receiverName, action);
- }
-
- protected void assertRestrictBackgroundStatus(int expectedStatus) throws Exception {
- final String status = mServiceClient.getRestrictBackgroundStatus();
- assertNotNull("didn't get API status from app2", status);
- assertEquals(restrictBackgroundValueToString(expectedStatus),
- restrictBackgroundValueToString(Integer.parseInt(status)));
- }
-
- /**
- * @deprecated The definition of "background" can be ambiguous. Use separate calls to
- * {@link #assertProcessStateBelow(int)} with
- * {@link #assertNetworkAccess(boolean, boolean, String)} to be explicit, instead.
- */
- @Deprecated
- protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- assertNetworkAccess(expectAllowed, false, null);
- }
-
- protected void assertTopNetworkAccess(boolean expectAllowed) throws Exception {
- assertTopState();
- assertNetworkAccess(expectAllowed, true /* needScreenOn */);
- }
-
- protected void assertForegroundServiceNetworkAccess() throws Exception {
- assertForegroundServiceState();
- assertNetworkAccess(true /* expectAvailable */, false /* needScreenOn */);
- }
-
- /**
- * Asserts that an app always have access while on foreground or running a foreground service.
- *
- * <p>This method will launch an activity, a foreground service to make
- * the assertion, but will finish the activity / stop the service afterwards.
- */
- protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception{
- // Checks foreground first.
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- finishActivity();
-
- // Then foreground service
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- stopForegroundService();
- }
-
- protected void assertExpeditedJobHasNetworkAccess() throws Exception {
- launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB);
- finishExpeditedJob();
- }
-
- protected void assertExpeditedJobHasNoNetworkAccess() throws Exception {
- launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB, false);
- finishExpeditedJob();
- }
-
- /**
- * Asserts that the process state of the test app is below, in priority, to the given
- * {@link android.app.ActivityManager.ProcessState}.
- */
- protected final void assertProcessStateBelow(int processState) throws Exception {
- assertProcessState(ps -> ps.state > processState, null);
- }
-
- protected final void assertTopState() throws Exception {
- assertProcessState(ps -> ps.state == PROCESS_STATE_TOP, () -> turnScreenOn());
- }
-
- protected final void assertForegroundServiceState() throws Exception {
- assertProcessState(ps -> ps.state == PROCESS_STATE_FOREGROUND_SERVICE, null);
- }
-
- private void assertProcessState(Predicate<ProcessState> statePredicate,
- ThrowingRunnable onRetry) throws Exception {
- final int maxTries = 30;
- ProcessState state = null;
- for (int i = 1; i <= maxTries; i++) {
- if (onRetry != null) {
- onRetry.run();
- }
- state = getProcessStateByUid(mUid);
- Log.v(TAG, "assertProcessState(): status for app2 (" + mUid + ") on attempt #" + i
- + ": " + state);
- if (statePredicate.test(state)) {
- return;
- }
- Log.i(TAG, "App not in desired process state on attempt #" + i
- + "; sleeping 1s before trying again");
- if (i < maxTries) {
- SystemClock.sleep(SECOND_IN_MS);
- }
- }
- fail("App2 (" + mUid + ") is not in the desired process state after " + maxTries
- + " attempts: " + state);
- }
-
- /**
- * Asserts whether the active network is available or not. If the network is unavailable, also
- * checks whether it is blocked by the expected error.
- *
- * @param expectAllowed expect background network access to be allowed or not.
- * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
- * meaningful only when the {@code expectAllowed} is 'false'.
- * Throws an IllegalArgumentException when {@code expectAllowed}
- * is true and this parameter is not null. When the
- * {@code expectAllowed} is 'false' and this parameter is null,
- * this function does not compare error type of the networking
- * access failure.
- */
- protected void assertNetworkAccess(boolean expectAllowed, String expectedUnavailableError)
- throws Exception {
- if (expectAllowed && expectedUnavailableError != null) {
- throw new IllegalArgumentException("expectedUnavailableError is not null");
- }
- assertNetworkAccess(expectAllowed, false, expectedUnavailableError);
- }
-
- /**
- * Asserts whether the active network is available or not.
- */
- private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
- throws Exception {
- assertNetworkAccess(expectAvailable, needScreenOn, null);
- }
-
- private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn,
- @Nullable final String expectedUnavailableError) throws Exception {
- final int maxTries = 5;
- String error = null;
- int timeoutMs = 500;
-
- for (int i = 1; i <= maxTries; i++) {
- error = checkNetworkAccess(expectAvailable, expectedUnavailableError);
-
- if (error == null) return;
-
- // TODO: ideally, it should retry only when it cannot connect to an external site,
- // or no retry at all! But, currently, the initial change fails almost always on
- // battery saver tests because the netd changes are made asynchronously.
- // Once b/27803922 is fixed, this retry mechanism should be revisited.
-
- Log.w(TAG, "Network status didn't match for expectAvailable=" + expectAvailable
- + " on attempt #" + i + ": " + error + "\n"
- + "Sleeping " + timeoutMs + "ms before trying again");
- if (needScreenOn) {
- turnScreenOn();
- }
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(timeoutMs);
- }
- // Exponential back-off.
- timeoutMs = Math.min(timeoutMs*2, NETWORK_TIMEOUT_MS);
- }
- fail("Invalid state for " + mUid + "; expectAvailable=" + expectAvailable + " after "
- + maxTries + " attempts.\nLast error: " + error);
- }
-
- /**
- * Asserts whether the network is blocked by accessing bpf maps if command-line tool supports.
- */
- void assertNetworkAccessBlockedByBpf(boolean expectBlocked, int uid, boolean metered) {
- final String result;
- try {
- result = executeShellCommand(
- "cmd network_stack is-uid-networking-blocked " + uid + " " + metered);
- } catch (AssertionError e) {
- // If NetworkStack is too old to support this command, ignore and continue
- // this test to verify other parts.
- if (e.getMessage().contains("No shell command implementation.")) {
- return;
- }
- throw e;
- }
-
- // Tethering module is too old.
- if (result.contains("API is unsupported")) {
- return;
- }
-
- assertEquals(expectBlocked, parseBooleanOrThrow(result.trim()));
- }
-
- /**
- * Similar to {@link Boolean#parseBoolean} but throws when the input
- * is unexpected instead of returning false.
- */
- private static boolean parseBooleanOrThrow(@NonNull String s) {
- // Don't use Boolean.parseBoolean
- if ("true".equalsIgnoreCase(s)) return true;
- if ("false".equalsIgnoreCase(s)) return false;
- throw new IllegalArgumentException("Unexpected: " + s);
- }
-
- /**
- * Checks whether the network is available as expected.
- *
- * @return error message with the mismatch (or empty if assertion passed).
- */
- private String checkNetworkAccess(boolean expectAvailable,
- @Nullable final String expectedUnavailableError) throws Exception {
- final NetworkCheckResult checkResult = mServiceClient.checkNetworkStatus(mCustomUrl);
- return checkForAvailabilityInNetworkCheckResult(checkResult, expectAvailable,
- expectedUnavailableError);
- }
-
- private String checkForAvailabilityInNetworkCheckResult(NetworkCheckResult networkCheckResult,
- boolean expectAvailable, @Nullable final String expectedUnavailableError) {
- assertNotNull("NetworkCheckResult from app2 is null", networkCheckResult);
-
- final NetworkInfo networkInfo = networkCheckResult.networkInfo;
- assertNotNull("NetworkInfo from app2 is null", networkInfo);
-
- final State state = networkInfo.getState();
- final DetailedState detailedState = networkInfo.getDetailedState();
-
- final boolean connected = networkCheckResult.connected;
- final String connectionCheckDetails = networkCheckResult.details;
-
- final StringBuilder errors = new StringBuilder();
- final State expectedState;
- final DetailedState expectedDetailedState;
- if (expectAvailable) {
- expectedState = State.CONNECTED;
- expectedDetailedState = DetailedState.CONNECTED;
- } else {
- expectedState = State.DISCONNECTED;
- expectedDetailedState = DetailedState.BLOCKED;
- }
-
- if (expectAvailable != connected) {
- errors.append(String.format("External site connection failed: expected %s, got %s\n",
- expectAvailable, connected));
- }
- if (expectedState != state || expectedDetailedState != detailedState) {
- errors.append(String.format("Connection state mismatch: expected %s/%s, got %s/%s\n",
- expectedState, expectedDetailedState, state, detailedState));
- } else if (!expectAvailable && (expectedUnavailableError != null)
- && !connectionCheckDetails.contains(expectedUnavailableError)) {
- errors.append("Connection unavailable reason mismatch: expected "
- + expectedUnavailableError + "\n");
- }
-
- if (errors.length() > 0) {
- errors.append("\tnetworkInfo: " + networkInfo + "\n");
- errors.append("\tconnectionCheckDetails: " + connectionCheckDetails + "\n");
- }
- return errors.length() == 0 ? null : errors.toString();
- }
-
- /**
- * Runs a Shell command which is not expected to generate output.
- */
- protected void executeSilentShellCommand(String command) {
- final String result = executeShellCommand(command);
- assertTrue("Command '" + command + "' failed: " + result, result.trim().isEmpty());
- }
-
- /**
- * Asserts the result of a command, wait and re-running it a couple times if necessary.
- */
- protected void assertDelayedShellCommand(String command, final String expectedResult)
- throws Exception {
- assertDelayedShellCommand(command, 5, 1, expectedResult);
- }
-
- protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
- final String expectedResult) throws Exception {
- assertDelayedShellCommand(command, maxTries, napTimeSeconds, new ExpectResultChecker() {
-
- @Override
- public boolean isExpected(String result) {
- return expectedResult.equals(result);
- }
-
- @Override
- public String getExpected() {
- return expectedResult;
- }
- });
- }
-
- protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
- ExpectResultChecker checker) throws Exception {
- String result = "";
- for (int i = 1; i <= maxTries; i++) {
- result = executeShellCommand(command).trim();
- if (checker.isExpected(result)) return;
- Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
- + checker.getExpected() + "' on attempt #" + i
- + "; sleeping " + napTimeSeconds + "s before trying again");
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
- }
- }
- fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after "
- + maxTries
- + " attempts. Last result: '" + result + "'");
- }
-
- protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy add restrict-background-whitelist " + uid);
- assertRestrictBackgroundWhitelist(uid, true);
- // UID policies live by the Highlander rule: "There can be only one".
- // Hence, if app is whitelisted, it should not be blacklisted.
- assertRestrictBackgroundBlacklist(uid, false);
- }
-
- protected void removeRestrictBackgroundWhitelist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy remove restrict-background-whitelist " + uid);
- assertRestrictBackgroundWhitelist(uid, false);
- }
-
- protected void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
- assertRestrictBackground("restrict-background-whitelist", uid, expected);
- }
-
- protected void addRestrictBackgroundBlacklist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy add restrict-background-blacklist " + uid);
- assertRestrictBackgroundBlacklist(uid, true);
- // UID policies live by the Highlander rule: "There can be only one".
- // Hence, if app is blacklisted, it should not be whitelisted.
- assertRestrictBackgroundWhitelist(uid, false);
- }
-
- protected void removeRestrictBackgroundBlacklist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy remove restrict-background-blacklist " + uid);
- assertRestrictBackgroundBlacklist(uid, false);
- }
-
- protected void assertRestrictBackgroundBlacklist(int uid, boolean expected) throws Exception {
- assertRestrictBackground("restrict-background-blacklist", uid, expected);
- }
-
- protected void addAppIdleWhitelist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy add app-idle-whitelist " + uid);
- assertAppIdleWhitelist(uid, true);
- }
-
- protected void removeAppIdleWhitelist(int uid) throws Exception {
- executeShellCommand("cmd netpolicy remove app-idle-whitelist " + uid);
- assertAppIdleWhitelist(uid, false);
- }
-
- protected void assertAppIdleWhitelist(int uid, boolean expected) throws Exception {
- assertRestrictBackground("app-idle-whitelist", uid, expected);
- }
-
- private void assertRestrictBackground(String list, int uid, boolean expected) throws Exception {
- final int maxTries = 5;
- boolean actual = false;
- final String expectedUid = Integer.toString(uid);
- String uids = "";
- for (int i = 1; i <= maxTries; i++) {
- final String output =
- executeShellCommand("cmd netpolicy list " + list);
- uids = output.split(":")[1];
- for (String candidate : uids.split(" ")) {
- actual = candidate.trim().equals(expectedUid);
- if (expected == actual) {
- return;
- }
- }
- Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected "
- + expected + ", got " + actual + "); sleeping 1s before polling again");
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(SECOND_IN_MS);
- }
- }
- fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual
- + ". Full list: " + uids);
- }
-
- protected void addTempPowerSaveModeWhitelist(String packageName, long duration)
- throws Exception {
- Log.i(TAG, "Adding pkg " + packageName + " to temp-power-save-mode whitelist");
- executeShellCommand("dumpsys deviceidle tempwhitelist -d " + duration + " " + packageName);
- }
-
- protected void assertPowerSaveModeWhitelist(String packageName, boolean expected)
- throws Exception {
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- assertDelayedShellCommand("dumpsys deviceidle whitelist =" + packageName,
- Boolean.toString(expected));
- }
-
- protected void addPowerSaveModeWhitelist(String packageName) throws Exception {
- Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist");
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- executeShellCommand("dumpsys deviceidle whitelist +" + packageName);
- assertPowerSaveModeWhitelist(packageName, true);
- }
-
- protected void removePowerSaveModeWhitelist(String packageName) throws Exception {
- Log.i(TAG, "Removing package " + packageName + " from power-save-mode whitelist");
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- executeShellCommand("dumpsys deviceidle whitelist -" + packageName);
- assertPowerSaveModeWhitelist(packageName, false);
- }
-
- protected void assertPowerSaveModeExceptIdleWhitelist(String packageName, boolean expected)
- throws Exception {
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- assertDelayedShellCommand("dumpsys deviceidle except-idle-whitelist =" + packageName,
- Boolean.toString(expected));
- }
-
- protected void addPowerSaveModeExceptIdleWhitelist(String packageName) throws Exception {
- Log.i(TAG, "Adding package " + packageName + " to power-save-mode-except-idle whitelist");
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- executeShellCommand("dumpsys deviceidle except-idle-whitelist +" + packageName);
- assertPowerSaveModeExceptIdleWhitelist(packageName, true);
- }
-
- protected void removePowerSaveModeExceptIdleWhitelist(String packageName) throws Exception {
- Log.i(TAG, "Removing package " + packageName
- + " from power-save-mode-except-idle whitelist");
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- executeShellCommand("dumpsys deviceidle except-idle-whitelist reset");
- assertPowerSaveModeExceptIdleWhitelist(packageName, false);
- }
-
- protected void turnBatteryOn() throws Exception {
- executeSilentShellCommand("cmd battery unplug");
- executeSilentShellCommand("cmd battery set status "
- + BatteryManager.BATTERY_STATUS_DISCHARGING);
- assertBatteryState(false);
- }
-
- protected void turnBatteryOff() throws Exception {
- executeSilentShellCommand("cmd battery set ac " + BATTERY_PLUGGED_ANY);
- executeSilentShellCommand("cmd battery set level 100");
- executeSilentShellCommand("cmd battery set status "
- + BatteryManager.BATTERY_STATUS_CHARGING);
- assertBatteryState(true);
- }
-
- protected void resetBatteryState() {
- BatteryUtils.runDumpsysBatteryReset();
- }
-
- private void assertBatteryState(boolean pluggedIn) throws Exception {
- final long endTime = SystemClock.elapsedRealtime() + BATTERY_STATE_TIMEOUT_MS;
- while (isDevicePluggedIn() != pluggedIn && SystemClock.elapsedRealtime() <= endTime) {
- Thread.sleep(BATTERY_STATE_CHECK_INTERVAL_MS);
- }
- if (isDevicePluggedIn() != pluggedIn) {
- fail("Timed out waiting for the plugged-in state to change,"
- + " expected pluggedIn: " + pluggedIn);
- }
- }
-
- private boolean isDevicePluggedIn() {
- final Intent batteryIntent = mContext.registerReceiver(null, BATTERY_CHANGED_FILTER);
- return batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) > 0;
- }
-
- protected void turnScreenOff() throws Exception {
- if (!mLock.isHeld()) mLock.acquire();
- executeSilentShellCommand("input keyevent KEYCODE_SLEEP");
- }
-
- protected void turnScreenOn() throws Exception {
- executeSilentShellCommand("input keyevent KEYCODE_WAKEUP");
- if (mLock.isHeld()) mLock.release();
- executeSilentShellCommand("wm dismiss-keyguard");
- }
-
- protected void setBatterySaverMode(boolean enabled) throws Exception {
- if (!isBatterySaverSupported()) {
- return;
- }
- Log.i(TAG, "Setting Battery Saver Mode to " + enabled);
- if (enabled) {
- turnBatteryOn();
- AmUtils.waitForBroadcastBarrier();
- executeSilentShellCommand("cmd power set-mode 1");
- } else {
- executeSilentShellCommand("cmd power set-mode 0");
- turnBatteryOff();
- AmUtils.waitForBroadcastBarrier();
- }
- }
-
- protected void setDozeMode(boolean enabled) throws Exception {
- if (!isDozeModeSupported()) {
- return;
- }
-
- Log.i(TAG, "Setting Doze Mode to " + enabled);
- if (enabled) {
- turnBatteryOn();
- turnScreenOff();
- executeShellCommand("dumpsys deviceidle force-idle deep");
- } else {
- turnScreenOn();
- turnBatteryOff();
- executeShellCommand("dumpsys deviceidle unforce");
- }
- assertDozeMode(enabled);
- }
-
- protected void assertDozeMode(boolean enabled) throws Exception {
- assertDelayedShellCommand("dumpsys deviceidle get deep", enabled ? "IDLE" : "ACTIVE");
- }
-
- protected void stopApp() {
- executeSilentShellCommand("am stop-app " + TEST_APP2_PKG);
- }
-
- protected void setAppIdle(boolean isIdle) throws Exception {
- setAppIdleNoAssert(isIdle);
- assertAppIdle(isIdle);
- }
-
- protected void setAppIdleNoAssert(boolean isIdle) throws Exception {
- if (!isAppStandbySupported()) {
- return;
- }
- Log.i(TAG, "Setting app idle to " + isIdle);
- final String bucketName = isIdle ? "rare" : "active";
- executeSilentShellCommand("am set-standby-bucket " + TEST_APP2_PKG + " " + bucketName);
- }
-
- protected void assertAppIdle(boolean isIdle) throws Exception {
- try {
- assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
- 30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + isIdle);
- } catch (Throwable e) {
- throw e;
- }
- }
-
- /**
- * Starts a service that will register a broadcast receiver to receive
- * {@code RESTRICT_BACKGROUND_CHANGE} intents.
- * <p>
- * The service must run in a separate app because otherwise it would be killed every time
- * {@link #runDeviceTests(String, String)} is executed.
- */
- protected void registerBroadcastReceiver() throws Exception {
- mServiceClient.registerBroadcastReceiver();
-
- final Intent intent = new Intent(ACTION_RECEIVER_READY)
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- // Wait until receiver is ready.
- final int maxTries = 10;
- for (int i = 1; i <= maxTries; i++) {
- final String message = sendOrderedBroadcast(intent, SECOND_IN_MS * 4);
- Log.d(TAG, "app2 receiver acked: " + message);
- if (message != null) {
- return;
- }
- Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(SECOND_IN_MS);
- }
- }
- fail("app2 receiver is not ready in " + mUid);
- }
-
- protected void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
- throws Exception {
- Log.i(TAG, "Registering network callback for request: " + request);
- mServiceClient.registerNetworkCallback(request, cb);
- }
-
- protected void unregisterNetworkCallback() throws Exception {
- mServiceClient.unregisterNetworkCallback();
- }
-
- /**
- * Registers a {@link NotificationListenerService} implementation that will execute the
- * notification actions right after the notification is sent.
- */
- protected void registerNotificationListenerService() throws Exception {
- executeShellCommand("cmd notification allow_listener "
- + MyNotificationListenerService.getId());
- final NotificationManager nm = mContext.getSystemService(NotificationManager.class);
- final ComponentName listenerComponent = MyNotificationListenerService.getComponentName();
- assertTrue(listenerComponent + " has not been granted access",
- nm.isNotificationListenerAccessGranted(listenerComponent));
- }
-
- protected void setPendingIntentAllowlistDuration(long durationMs) {
- mDeviceIdleDeviceConfigStateHelper.set("notification_allowlist_duration_ms",
- String.valueOf(durationMs));
- }
-
- protected void resetDeviceIdleSettings() {
- mDeviceIdleDeviceConfigStateHelper.restoreOriginalValues();
- }
-
- protected void launchActivity() throws Exception {
- turnScreenOn();
- final CountDownLatch latch = new CountDownLatch(1);
- final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_ACTIVTIY);
- final RemoteCallback callback = new RemoteCallback(result -> latch.countDown());
- launchIntent.putExtra(Intent.EXTRA_REMOTE_CALLBACK, callback);
- mContext.startActivity(launchIntent);
- // There might be a race when app2 is launched but ACTION_FINISH_ACTIVITY has not registered
- // before test calls finishActivity(). When the issue is happened, there is no way to fix
- // it, so have a callback design to make sure that the app is launched completely and
- // ACTION_FINISH_ACTIVITY will be registered before leaving this method.
- if (!latch.await(LAUNCH_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- fail("Timed out waiting for launching activity");
- }
- }
-
- protected void launchComponentAndAssertNetworkAccess(int type) throws Exception {
- launchComponentAndAssertNetworkAccess(type, true);
- }
-
- protected void launchComponentAndAssertNetworkAccess(int type, boolean expectAvailable)
- throws Exception {
- if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
- startForegroundService();
- assertForegroundServiceNetworkAccess();
- } else if (type == TYPE_COMPONENT_ACTIVTIY) {
- turnScreenOn();
- final CountDownLatch latch = new CountDownLatch(1);
- final Intent launchIntent = getIntentForComponent(type);
- final Bundle extras = new Bundle();
- final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
- new AtomicReference<>();
- extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
- extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
- extras.putString(KEY_CUSTOM_URL, mCustomUrl);
- launchIntent.putExtras(extras);
- mContext.startActivity(launchIntent);
- if (latch.await(ACTIVITY_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- final int resultCode = result.get().first;
- final NetworkCheckResult networkCheckResult = result.get().second;
- if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
- final String error = checkForAvailabilityInNetworkCheckResult(
- networkCheckResult, expectAvailable,
- null /* expectedUnavailableError */);
- if (error != null) {
- fail("Network is not available for activity in app2 (" + mUid + "): "
- + error);
- }
- } else if (resultCode == INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE) {
- Log.d(TAG, networkCheckResult.details);
- // App didn't come to foreground when the activity is started, so try again.
- assertTopNetworkAccess(true);
- } else {
- fail("Unexpected resultCode=" + resultCode
- + "; networkCheckResult=[" + networkCheckResult + "]");
- }
- } else {
- fail("Timed out waiting for network availability status from app2's activity ("
- + mUid + ")");
- }
- } else if (type == TYPE_EXPEDITED_JOB) {
- final Bundle extras = new Bundle();
- final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
- new AtomicReference<>();
- final CountDownLatch latch = new CountDownLatch(1);
- extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
- extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
- extras.putString(KEY_CUSTOM_URL, mCustomUrl);
- final JobInfo jobInfo = new JobInfo.Builder(TEST_JOB_ID, TEST_JOB_COMPONENT)
- .setExpedited(true)
- .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
- .setTransientExtras(extras)
- .build();
- assertEquals("Error scheduling " + jobInfo,
- RESULT_SUCCESS, mServiceClient.scheduleJob(jobInfo));
- forceRunJob(TEST_APP2_PKG, TEST_JOB_ID);
- if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- final int resultCode = result.get().first;
- final NetworkCheckResult networkCheckResult = result.get().second;
- if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
- final String error = checkForAvailabilityInNetworkCheckResult(
- networkCheckResult, expectAvailable,
- null /* expectedUnavailableError */);
- if (error != null) {
- Log.d(TAG, "Network state is unexpected, checking again. " + error);
- // Right now we could end up in an unexpected state if expedited job
- // doesn't have network access immediately after starting, so check again.
- assertNetworkAccess(expectAvailable, false /* needScreenOn */);
- }
- } else {
- fail("Unexpected resultCode=" + resultCode
- + "; networkCheckResult=[" + networkCheckResult + "]");
- }
- } else {
- fail("Timed out waiting for network availability status from app2's expedited job ("
- + mUid + ")");
- }
- } else {
- throw new IllegalArgumentException("Unknown type: " + type);
- }
- }
-
- protected void startActivity() throws Exception {
- final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_ACTIVTIY);
- mContext.startActivity(launchIntent);
- }
-
- private void startForegroundService() throws Exception {
- final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_FOREGROUND_SERVICE);
- mContext.startForegroundService(launchIntent);
- assertForegroundServiceState();
- }
-
- private Intent getIntentForComponent(int type) {
- final Intent intent = new Intent();
- if (type == TYPE_COMPONENT_ACTIVTIY) {
- intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_ACTIVITY_CLASS))
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
- } else if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
- intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS))
- .setFlags(1);
- } else {
- fail("Unknown type: " + type);
- }
- return intent;
- }
-
- protected void stopForegroundService() throws Exception {
- executeShellCommand(String.format("am startservice -f 2 %s/%s",
- TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS));
- // NOTE: cannot assert state because it depends on whether activity was on top before.
- }
-
- private Binder getNewNetworkStateObserver(final CountDownLatch latch,
- final AtomicReference<Pair<Integer, NetworkCheckResult>> result) {
- return new INetworkStateObserver.Stub() {
- @Override
- public void onNetworkStateChecked(int resultCode,
- NetworkCheckResult networkCheckResult) {
- result.set(Pair.create(resultCode, networkCheckResult));
- latch.countDown();
- }
- };
- }
-
- /**
- * Finishes an activity on app2 so its process is demoted from foreground status.
- */
- protected void finishActivity() throws Exception {
- final Intent intent = new Intent(ACTION_FINISH_ACTIVITY)
- .setPackage(TEST_APP2_PKG)
- .setFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- sendOrderedBroadcast(intent);
- }
-
- /**
- * Finishes the expedited job on app2 so its process is demoted from foreground status.
- */
- private void finishExpeditedJob() throws Exception {
- final Intent intent = new Intent(ACTION_FINISH_JOB)
- .setPackage(TEST_APP2_PKG)
- .setFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- sendOrderedBroadcast(intent);
- }
-
- protected void sendNotification(int notificationId, String notificationType) throws Exception {
- Log.d(TAG, "Sending notification broadcast (id=" + notificationId
- + ", type=" + notificationType);
- mServiceClient.sendNotification(notificationId, notificationType);
- }
-
- protected String showToast() {
- final Intent intent = new Intent(ACTION_SHOW_TOAST);
- intent.setPackage(TEST_APP2_PKG);
- Log.d(TAG, "Sending request to show toast");
- try {
- return sendOrderedBroadcast(intent, 3 * SECOND_IN_MS);
- } catch (Exception e) {
- return "";
- }
- }
-
- private ProcessState getProcessStateByUid(int uid) throws Exception {
- return new ProcessState(executeShellCommand("cmd activity get-uid-state " + uid));
- }
-
- private static class ProcessState {
- private final String fullState;
- final int state;
-
- ProcessState(String fullState) {
- this.fullState = fullState;
- try {
- this.state = Integer.parseInt(fullState.split(" ")[0]);
- } catch (Exception e) {
- throw new IllegalArgumentException("Could not parse " + fullState);
- }
- }
-
- @Override
- public String toString() {
- return fullState;
- }
- }
-
- /**
- * Helper class used to assert the result of a Shell command.
- */
- protected static interface ExpectResultChecker {
- /**
- * Checkes whether the result of the command matched the expectation.
- */
- boolean isExpected(String result);
- /**
- * Gets the expected result so it's displayed on log and failure messages.
- */
- String getExpected();
- }
-
- protected void setRestrictedNetworkingMode(boolean enabled) throws Exception {
- executeSilentShellCommand(
- "settings put global restricted_networking_mode " + (enabled ? 1 : 0));
- assertRestrictedNetworkingModeState(enabled);
- }
-
- protected void assertRestrictedNetworkingModeState(boolean enabled) throws Exception {
- assertDelayedShellCommand("cmd netpolicy get restricted-mode",
- "Restricted mode status: " + (enabled ? "enabled" : "disabled"));
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java
deleted file mode 100644
index 6b802f6..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class AppIdleMeteredTest extends AbstractAppIdleTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java
deleted file mode 100644
index 2e725ae..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/AppIdleNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class AppIdleNonMeteredTest extends AbstractAppIdleTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java
deleted file mode 100644
index 2e421f6..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class BatterySaverModeMeteredTest extends AbstractBatterySaverModeTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java
deleted file mode 100644
index 0be5644..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/BatterySaverModeNonMeteredTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class BatterySaverModeNonMeteredTest extends AbstractBatterySaverModeTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
deleted file mode 100644
index bfccce9..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ConnOnActivityStartTest.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2022 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.cts.netpolicy.hostside;
-
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getUiDevice;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class ConnOnActivityStartTest extends AbstractRestrictBackgroundNetworkTestCase {
- private static final int TEST_ITERATION_COUNT = 5;
-
- @Before
- public final void setUp() throws Exception {
- super.setUp();
- resetDeviceState();
- }
-
- @After
- public final void tearDown() throws Exception {
- super.tearDown();
- stopApp();
- resetDeviceState();
- }
-
- private void resetDeviceState() throws Exception {
- resetBatteryState();
- setBatterySaverMode(false);
- setRestrictBackground(false);
- setAppIdle(false);
- setDozeMode(false);
- }
-
-
- @Test
- @RequiredProperties({BATTERY_SAVER_MODE})
- public void testStartActivity_batterySaver() throws Exception {
- setBatterySaverMode(true);
- assertNetworkAccess(false, null);
- assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver", null);
- }
-
- @Test
- @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
- public void testStartActivity_dataSaver() throws Exception {
- setRestrictBackground(true);
- assertNetworkAccess(false, null);
- assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver", null);
- }
-
- @Test
- @RequiredProperties({DOZE_MODE})
- public void testStartActivity_doze() throws Exception {
- setDozeMode(true);
- assertNetworkAccess(false, null);
- // TODO (235284115): We need to turn on Doze every time before starting
- // the activity.
- assertLaunchedActivityHasNetworkAccess("testStartActivity_doze", null);
- }
-
- @Test
- @RequiredProperties({APP_STANDBY_MODE})
- public void testStartActivity_appStandby() throws Exception {
- turnBatteryOn();
- setAppIdle(true);
- assertNetworkAccess(false, null);
- // TODO (235284115): We need to put the app into app standby mode every
- // time before starting the activity.
- assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby", null);
- }
-
- @Test
- public void testStartActivity_default() throws Exception {
- assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
- assertLaunchedActivityHasNetworkAccess("testStartActivity_default", () -> {
- assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
- SystemClock.sleep(mProcessStateTransitionLongDelayMs);
- assertNetworkAccess(false, null);
- });
- }
-
- private void assertLaunchedActivityHasNetworkAccess(String testName,
- ThrowingRunnable onBeginIteration) throws Exception {
- for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
- if (onBeginIteration != null) {
- onBeginIteration.run();
- }
- Log.i(TAG, testName + " start #" + i);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- getUiDevice().pressHome();
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- Log.i(TAG, testName + " end #" + i);
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java
deleted file mode 100644
index 66e0d00..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataSaverModeTest.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
-
-import static com.android.compatibility.common.util.FeatureUtil.isTV;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NO_DATA_SAVER_MODE;
-
-import static org.junit.Assert.fail;
-
-import androidx.test.filters.LargeTest;
-
-import com.android.compatibility.common.util.CddTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
-@LargeTest
-public class DataSaverModeTest extends AbstractRestrictBackgroundNetworkTestCase {
-
- private static final String[] REQUIRED_WHITELISTED_PACKAGES = {
- "com.android.providers.downloads"
- };
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- // Set initial state.
- setRestrictBackground(false);
- removeRestrictBackgroundWhitelist(mUid);
- removeRestrictBackgroundBlacklist(mUid);
-
- registerBroadcastReceiver();
- assertRestrictBackgroundChangedReceived(0);
- }
-
- @After
- public void tearDown() throws Exception {
- super.tearDown();
-
- setRestrictBackground(false);
- }
-
- @Test
- public void testGetRestrictBackgroundStatus_disabled() throws Exception {
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-
- // Verify status is always disabled, never whitelisted
- addRestrictBackgroundWhitelist(mUid);
- assertRestrictBackgroundChangedReceived(0);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
- }
-
- @Test
- public void testGetRestrictBackgroundStatus_whitelisted() throws Exception {
- setRestrictBackground(true);
- assertRestrictBackgroundChangedReceived(1);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- addRestrictBackgroundWhitelist(mUid);
- assertRestrictBackgroundChangedReceived(2);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED);
-
- removeRestrictBackgroundWhitelist(mUid);
- assertRestrictBackgroundChangedReceived(3);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
- }
-
- @Test
- public void testGetRestrictBackgroundStatus_enabled() throws Exception {
- setRestrictBackground(true);
- assertRestrictBackgroundChangedReceived(1);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- // Make sure foreground app doesn't lose access upon enabling Data Saver.
- setRestrictBackground(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- setRestrictBackground(true);
- assertTopNetworkAccess(true);
-
- // Although it should not have access while the screen is off.
- turnScreenOff();
- assertBackgroundNetworkAccess(false);
- turnScreenOn();
- // On some TVs, it is possible that the activity on top may change after the screen is
- // turned off and on again, so relaunch the activity in the test app again.
- if (isTV()) {
- startActivity();
- }
- assertTopNetworkAccess(true);
-
- // Goes back to background state.
- finishActivity();
- assertBackgroundNetworkAccess(false);
-
- // Make sure foreground service doesn't lose access upon enabling Data Saver.
- setRestrictBackground(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
- setRestrictBackground(true);
- assertForegroundServiceNetworkAccess();
- stopForegroundService();
- assertBackgroundNetworkAccess(false);
- }
-
- @Test
- public void testGetRestrictBackgroundStatus_blacklisted() throws Exception {
- addRestrictBackgroundBlacklist(mUid);
- assertRestrictBackgroundChangedReceived(1);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- assertsForegroundAlwaysHasNetworkAccess();
- assertRestrictBackgroundChangedReceived(1);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
- // UID policies live by the Highlander rule: "There can be only one".
- // Hence, if app is whitelisted, it should not be blacklisted anymore.
- setRestrictBackground(true);
- assertRestrictBackgroundChangedReceived(2);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
- addRestrictBackgroundWhitelist(mUid);
- assertRestrictBackgroundChangedReceived(3);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED);
-
- // Check status after removing blacklist.
- // ...re-enables first
- addRestrictBackgroundBlacklist(mUid);
- assertRestrictBackgroundChangedReceived(4);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
- assertsForegroundAlwaysHasNetworkAccess();
- // ... remove blacklist - access's still rejected because Data Saver is on
- removeRestrictBackgroundBlacklist(mUid);
- assertRestrictBackgroundChangedReceived(4);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
- assertsForegroundAlwaysHasNetworkAccess();
- // ... finally, disable Data Saver
- setRestrictBackground(false);
- assertRestrictBackgroundChangedReceived(5);
- assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
- assertsForegroundAlwaysHasNetworkAccess();
- }
-
- @Test
- public void testGetRestrictBackgroundStatus_requiredWhitelistedPackages() throws Exception {
- final StringBuilder error = new StringBuilder();
- for (String packageName : REQUIRED_WHITELISTED_PACKAGES) {
- int uid = -1;
- try {
- uid = getUid(packageName);
- assertRestrictBackgroundWhitelist(uid, true);
- } catch (Throwable t) {
- error.append("\nFailed for '").append(packageName).append("'");
- if (uid > 0) {
- error.append(" (uid ").append(uid).append(")");
- }
- error.append(": ").append(t).append("\n");
- }
- }
- if (error.length() > 0) {
- fail(error.toString());
- }
- }
-
- @RequiredProperties({NO_DATA_SAVER_MODE})
- @CddTest(requirement="7.4.7/C-2-2")
- @Test
- public void testBroadcastNotSentOnUnsupportedDevices() throws Exception {
- setRestrictBackground(true);
- assertRestrictBackgroundChangedReceived(0);
-
- setRestrictBackground(false);
- assertRestrictBackgroundChangedReceived(0);
-
- setRestrictBackground(true);
- assertRestrictBackgroundChangedReceived(0);
- }
-
- private void assertDataSaverStatusOnBackground(int expectedStatus) throws Exception {
- assertRestrictBackgroundStatus(expectedStatus);
- assertBackgroundNetworkAccess(expectedStatus != RESTRICT_BACKGROUND_STATUS_ENABLED);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java
deleted file mode 100644
index 69ca206..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DataWarningReceiverTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.clearSnoozeTimestamps;
-
-import android.content.pm.PackageManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.SubscriptionPlan;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.Direction;
-import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject2;
-import androidx.test.uiautomator.Until;
-
-import com.android.compatibility.common.util.SystemUtil;
-import com.android.compatibility.common.util.UiAutomatorUtils2;
-
-import org.junit.After;
-import org.junit.Assume;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.time.Period;
-import java.time.ZonedDateTime;
-import java.util.Arrays;
-import java.util.List;
-
-public class DataWarningReceiverTest extends AbstractRestrictBackgroundNetworkTestCase {
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- clearSnoozeTimestamps();
- registerBroadcastReceiver();
- turnScreenOn();
- }
-
- @After
- public void tearDown() throws Exception {
- super.tearDown();
- }
-
- @Test
- public void testSnoozeWarningNotReceived() throws Exception {
- Assume.assumeTrue("Feature not supported: " + PackageManager.FEATURE_TELEPHONY,
- mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY));
- final SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
- final int subId = SubscriptionManager.getDefaultDataSubscriptionId();
- Assume.assumeTrue("Valid subId not found",
- subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID);
-
- setSubPlanOwner(subId, TEST_PKG);
- final List<SubscriptionPlan> originalPlans = sm.getSubscriptionPlans(subId);
- try {
- // In NetworkPolicyManagerService class, we set the data warning bytes to 90% of
- // data limit bytes. So, create the subscription plan in such a way this data warning
- // threshold is already reached.
- final SubscriptionPlan plan = SubscriptionPlan.Builder
- .createRecurring(ZonedDateTime.parse("2007-03-14T00:00:00.000Z"),
- Period.ofMonths(1))
- .setTitle("CTS")
- .setDataLimit(1_000_000_000, SubscriptionPlan.LIMIT_BEHAVIOR_THROTTLED)
- .setDataUsage(999_000_000, System.currentTimeMillis())
- .build();
- sm.setSubscriptionPlans(subId, Arrays.asList(plan));
- final UiDevice uiDevice = UiDevice.getInstance(mInstrumentation);
- uiDevice.openNotification();
- try {
- final UiObject2 uiObject = UiAutomatorUtils2.waitFindObject(
- By.text("Data warning"));
- Assume.assumeNotNull(uiObject);
- uiObject.wait(Until.clickable(true), 10_000L);
- uiObject.getParent().swipe(Direction.RIGHT, 1.0f);
- } catch (Throwable t) {
- Assume.assumeNoException(
- "Error occurred while finding and swiping the notification", t);
- }
- assertSnoozeWarningNotReceived();
- uiDevice.pressHome();
- } finally {
- sm.setSubscriptionPlans(subId, originalPlans);
- setSubPlanOwner(subId, "");
- }
- }
-
- private static void setSubPlanOwner(int subId, String packageName) throws Exception {
- SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
- "cmd netpolicy set sub-plan-owner " + subId + " " + packageName);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java
deleted file mode 100644
index 810fd19..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class DefaultRestrictionsMeteredTest extends AbstractDefaultRestrictionsTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java
deleted file mode 100644
index fef546c..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DefaultRestrictionsNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class DefaultRestrictionsNonMeteredTest extends AbstractDefaultRestrictionsTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java
deleted file mode 100644
index 741dd7e..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class DozeModeMeteredTest extends AbstractDozeModeTestCase {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java
deleted file mode 100644
index 2dc6cc4..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DumpOnFailureRule.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_APP2_PKG;
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_PKG;
-
-import android.os.Environment;
-import android.os.FileUtils;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.UiDevice;
-
-import com.android.compatibility.common.util.OnFailureRule;
-
-import org.junit.AssumptionViolatedException;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-
-public class DumpOnFailureRule extends OnFailureRule {
- private File mDumpDir = new File(Environment.getExternalStorageDirectory(),
- "CtsHostsideNetworkPolicyTests");
-
- @Override
- public void onTestFailure(Statement base, Description description, Throwable throwable) {
- if (throwable instanceof AssumptionViolatedException) {
- final String testName = description.getClassName() + "_" + description.getMethodName();
- Log.d(TAG, "Skipping test " + testName + ": " + throwable);
- return;
- }
-
- prepareDumpRootDir();
- final String shortenedTestName = getShortenedTestName(description);
- final File dumpFile = new File(mDumpDir, "dump-" + shortenedTestName);
- Log.i(TAG, "Dumping debug info for " + description + ": " + dumpFile.getPath());
- try (FileOutputStream out = new FileOutputStream(dumpFile)) {
- for (String cmd : new String[] {
- "dumpsys netpolicy",
- "dumpsys network_management",
- "dumpsys usagestats " + TEST_PKG + " " + TEST_APP2_PKG,
- "dumpsys usagestats appstandby",
- "dumpsys connectivity trafficcontroller",
- "dumpsys netd trafficcontroller",
- "dumpsys platform_compat", // TODO (b/279829773): Remove this dump
- "dumpsys jobscheduler " + TEST_APP2_PKG, // TODO (b/288220398): Remove this dump
- }) {
- dumpCommandOutput(out, cmd);
- }
- } catch (FileNotFoundException e) {
- Log.e(TAG, "Error opening file: " + dumpFile, e);
- } catch (IOException e) {
- Log.e(TAG, "Error closing file: " + dumpFile, e);
- }
- final UiDevice uiDevice = UiDevice.getInstance(
- InstrumentationRegistry.getInstrumentation());
- final File screenshotFile = new File(mDumpDir, "sc-" + shortenedTestName + ".png");
- uiDevice.takeScreenshot(screenshotFile);
- final File windowHierarchyFile = new File(mDumpDir, "wh-" + shortenedTestName + ".xml");
- try {
- uiDevice.dumpWindowHierarchy(windowHierarchyFile);
- } catch (IOException e) {
- Log.e(TAG, "Error dumping window hierarchy", e);
- }
- }
-
- private String getShortenedTestName(Description description) {
- final String qualifiedClassName = description.getClassName();
- final String className = qualifiedClassName.substring(
- qualifiedClassName.lastIndexOf(".") + 1);
- final String shortenedClassName = className.chars()
- .filter(Character::isUpperCase)
- .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
- .toString();
- return shortenedClassName + "_" + description.getMethodName();
- }
-
- void dumpCommandOutput(FileOutputStream out, String cmd) {
- final ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation()
- .getUiAutomation().executeShellCommand(cmd);
- try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
- out.write(("Output of '" + cmd + "':\n").getBytes(StandardCharsets.UTF_8));
- FileUtils.copy(in, out);
- out.write("\n\n=================================================================\n\n"
- .getBytes(StandardCharsets.UTF_8));
- } catch (IOException e) {
- Log.e(TAG, "Error dumping '" + cmd + "'", e);
- }
- }
-
- void prepareDumpRootDir() {
- if (!mDumpDir.exists() && !mDumpDir.mkdir()) {
- Log.e(TAG, "Error creating " + mDumpDir);
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java
deleted file mode 100644
index d56a50b..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class ExpeditedJobMeteredTest extends AbstractExpeditedJobTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java
deleted file mode 100644
index 0a776ee..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/ExpeditedJobNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class ExpeditedJobNonMeteredTest extends AbstractExpeditedJobTest {
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java
deleted file mode 100644
index 4f4e68e..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MeterednessConfigurationRule.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setupActiveNetworkMeteredness;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-import android.util.ArraySet;
-
-import com.android.compatibility.common.util.BeforeAfterRule;
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-public class MeterednessConfigurationRule extends BeforeAfterRule {
- private ThrowingRunnable mMeterednessResetter;
-
- @Override
- public void onBefore(Statement base, Description description) throws Throwable {
- final ArraySet<Property> requiredProperties
- = RequiredPropertiesRule.getRequiredProperties();
- if (requiredProperties.contains(METERED_NETWORK)) {
- configureNetworkMeteredness(true);
- } else if (requiredProperties.contains(NON_METERED_NETWORK)) {
- configureNetworkMeteredness(false);
- }
- }
-
- @Override
- public void onAfter(Statement base, Description description) throws Throwable {
- resetNetworkMeteredness();
- }
-
- public void configureNetworkMeteredness(boolean metered) throws Exception {
- mMeterednessResetter = setupActiveNetworkMeteredness(metered);
- }
-
- public void resetNetworkMeteredness() throws Exception {
- if (mMeterednessResetter != null) {
- mMeterednessResetter.run();
- mMeterednessResetter = null;
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java
deleted file mode 100644
index b0fa106..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MixedModesTest.java
+++ /dev/null
@@ -1,370 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DOZE_MODE;
-import static com.android.cts.netpolicy.hostside.Property.METERED_NETWORK;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Test cases for the more complex scenarios where multiple restrictions (like Battery Saver Mode
- * and Data Saver Mode) are applied simultaneously.
- * <p>
- * <strong>NOTE: </strong>it might sound like the test methods on this class are testing too much,
- * which would make it harder to diagnose individual failures, but the assumption is that such
- * failure most likely will happen when the restriction is tested individually as well.
- */
-public class MixedModesTest extends AbstractRestrictBackgroundNetworkTestCase {
- private static final String TAG = "MixedModesTest";
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- // Set initial state.
- removeRestrictBackgroundWhitelist(mUid);
- removeRestrictBackgroundBlacklist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-
- registerBroadcastReceiver();
- }
-
- @After
- public void tearDown() throws Exception {
- super.tearDown();
-
- try {
- setRestrictBackground(false);
- } finally {
- setBatterySaverMode(false);
- }
- }
-
- /**
- * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on metered networks.
- */
- @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK})
- @Test
- public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
- final MeterednessConfigurationRule meterednessConfiguration
- = new MeterednessConfigurationRule();
- meterednessConfiguration.configureNetworkMeteredness(true);
- try {
- setRestrictBackground(true);
- setBatterySaverMode(true);
-
- Log.v(TAG, "Not whitelisted for any.");
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
-
- Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver.");
- addRestrictBackgroundWhitelist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundWhitelist(mUid);
-
- Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver.");
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- removeRestrictBackgroundWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
-
- Log.v(TAG, "Whitelisted for both.");
- addRestrictBackgroundWhitelist(mUid);
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundWhitelist(mUid);
-
- Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver.");
- addRestrictBackgroundBlacklist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundBlacklist(mUid);
-
- Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver.");
- addRestrictBackgroundBlacklist(mUid);
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundBlacklist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- } finally {
- meterednessConfiguration.resetNetworkMeteredness();
- }
- }
-
- /**
- * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on non-metered
- * networks.
- */
- @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, NON_METERED_NETWORK})
- @Test
- public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
- final MeterednessConfigurationRule meterednessConfiguration
- = new MeterednessConfigurationRule();
- meterednessConfiguration.configureNetworkMeteredness(false);
- try {
- setRestrictBackground(true);
- setBatterySaverMode(true);
-
- Log.v(TAG, "Not whitelisted for any.");
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
-
- Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver.");
- addRestrictBackgroundWhitelist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundWhitelist(mUid);
-
- Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver.");
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- removeRestrictBackgroundWhitelist(mUid);
- assertBackgroundNetworkAccess(true);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
-
- Log.v(TAG, "Whitelisted for both.");
- addRestrictBackgroundWhitelist(mUid);
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundWhitelist(mUid);
-
- Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver.");
- addRestrictBackgroundBlacklist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(false);
- removeRestrictBackgroundBlacklist(mUid);
-
- Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver.");
- addRestrictBackgroundBlacklist(mUid);
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
- assertsForegroundAlwaysHasNetworkAccess();
- assertBackgroundNetworkAccess(true);
- removeRestrictBackgroundBlacklist(mUid);
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- } finally {
- meterednessConfiguration.resetNetworkMeteredness();
- }
- }
-
- /**
- * Tests that powersave whitelists works as expected when doze and battery saver modes
- * are enabled.
- */
- @RequiredProperties({DOZE_MODE, BATTERY_SAVER_MODE})
- @Test
- public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
- setBatterySaverMode(true);
- setDozeMode(true);
-
- try {
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- } finally {
- setBatterySaverMode(false);
- setDozeMode(false);
- }
- }
-
- /**
- * Tests that powersave whitelists works as expected when doze and appIdle modes
- * are enabled.
- */
- @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE})
- @Test
- public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
- setDozeMode(true);
- setAppIdle(true);
-
- try {
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
-
- removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setDozeMode(false);
- }
- }
-
- @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
- @Test
- public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
- setDozeMode(true);
- setAppIdle(true);
-
- try {
- assertBackgroundNetworkAccess(false);
-
- addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(true);
-
- // Wait until the whitelist duration is expired.
- SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setDozeMode(false);
- }
- }
-
- @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
- @Test
- public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
- setBatterySaverMode(true);
- setAppIdle(true);
-
- try {
- assertBackgroundNetworkAccess(false);
-
- addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(true);
-
- // Wait until the whitelist duration is expired.
- SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setBatterySaverMode(false);
- }
- }
-
- /**
- * Tests that the app idle whitelist works as expected when doze and appIdle mode are enabled.
- */
- @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE})
- @Test
- public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
- setDozeMode(true);
- setAppIdle(true);
-
- try {
- assertBackgroundNetworkAccess(false);
-
- // UID still shouldn't have access because of Doze.
- addAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
-
- removeAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setDozeMode(false);
- }
- }
-
- @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
- @Test
- public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
- setDozeMode(true);
- setAppIdle(true);
-
- try {
- assertBackgroundNetworkAccess(false);
-
- addAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
-
- addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(true);
-
- // Wait until the whitelist duration is expired.
- SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setDozeMode(false);
- removeAppIdleWhitelist(mUid);
- }
- }
-
- @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
- @Test
- public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
- setBatterySaverMode(true);
- setAppIdle(true);
-
- try {
- assertBackgroundNetworkAccess(false);
-
- addAppIdleWhitelist(mUid);
- assertBackgroundNetworkAccess(false);
-
- addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(true);
-
- // Wait until the whitelist duration is expired.
- SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
- assertBackgroundNetworkAccess(false);
- } finally {
- setAppIdle(false);
- setBatterySaverMode(false);
- removeAppIdleWhitelist(mUid);
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java
deleted file mode 100644
index 6dc9921..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyNotificationListenerService.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.PendingIntent.CanceledException;
-import android.app.RemoteInput;
-import android.content.ComponentName;
-import android.os.Bundle;
-import android.service.notification.NotificationListenerService;
-import android.service.notification.StatusBarNotification;
-import android.util.Log;
-
-/**
- * NotificationListenerService implementation that executes the notification actions once they're
- * created.
- */
-public class MyNotificationListenerService extends NotificationListenerService {
- private static final String TAG = "MyNotificationListenerService";
-
- @Override
- public void onListenerConnected() {
- Log.d(TAG, "onListenerConnected()");
- }
-
- @Override
- public void onNotificationPosted(StatusBarNotification sbn) {
- Log.d(TAG, "onNotificationPosted(): " + sbn);
- if (!sbn.getPackageName().startsWith(getPackageName())) {
- Log.v(TAG, "ignoring notification from a different package");
- return;
- }
- final PendingIntentSender sender = new PendingIntentSender();
- final Notification notification = sbn.getNotification();
- if (notification.contentIntent != null) {
- sender.send("content", notification.contentIntent);
- }
- if (notification.deleteIntent != null) {
- sender.send("delete", notification.deleteIntent);
- }
- if (notification.fullScreenIntent != null) {
- sender.send("full screen", notification.fullScreenIntent);
- }
- if (notification.actions != null) {
- for (Notification.Action action : notification.actions) {
- sender.send("action", action.actionIntent);
- sender.send("action extras", action.getExtras());
- final RemoteInput[] remoteInputs = action.getRemoteInputs();
- if (remoteInputs != null && remoteInputs.length > 0) {
- for (RemoteInput remoteInput : remoteInputs) {
- sender.send("remote input extras", remoteInput.getExtras());
- }
- }
- }
- }
- sender.send("notification extras", notification.extras);
- }
-
- static String getId() {
- return String.format("%s/%s", MyNotificationListenerService.class.getPackage().getName(),
- MyNotificationListenerService.class.getName());
- }
-
- static ComponentName getComponentName() {
- return new ComponentName(MyNotificationListenerService.class.getPackage().getName(),
- MyNotificationListenerService.class.getName());
- }
-
- private static final class PendingIntentSender {
- private PendingIntent mSentIntent = null;
- private String mReason = null;
-
- private void send(String reason, PendingIntent pendingIntent) {
- if (pendingIntent == null) {
- // Could happen on action that only has extras
- Log.v(TAG, "Not sending null pending intent for " + reason);
- return;
- }
- if (mSentIntent != null || mReason != null) {
- // Sanity check: make sure test case set up just one pending intent in the
- // notification, otherwise it could pass because another pending intent caused the
- // whitelisting.
- throw new IllegalStateException("Already sent a PendingIntent (" + mSentIntent
- + ") for reason '" + mReason + "' when requested another for '" + reason
- + "' (" + pendingIntent + ")");
- }
- Log.i(TAG, "Sending pending intent for " + reason + ":" + pendingIntent);
- try {
- pendingIntent.send();
- mSentIntent = pendingIntent;
- mReason = reason;
- } catch (CanceledException e) {
- Log.w(TAG, "Pending intent " + pendingIntent + " canceled");
- }
- }
-
- private void send(String reason, Bundle extras) {
- if (extras != null) {
- for (String key : extras.keySet()) {
- Object value = extras.get(key);
- if (value instanceof PendingIntent) {
- send(reason + " with key '" + key + "'", (PendingIntent) value);
- }
- }
- }
- }
-
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java
deleted file mode 100644
index 71b28f6..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/MyServiceClient.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside;
-
-import android.app.job.JobInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.net.NetworkRequest;
-import android.os.ConditionVariable;
-import android.os.IBinder;
-import android.os.RemoteException;
-
-public class MyServiceClient {
- private static final int TIMEOUT_MS = 20_000;
- private static final String PACKAGE = MyServiceClient.class.getPackage().getName();
- private static final String APP2_PACKAGE = PACKAGE + ".app2";
- private static final String SERVICE_NAME = APP2_PACKAGE + ".MyService";
-
- private Context mContext;
- private ServiceConnection mServiceConnection;
- private volatile IMyService mService;
- private final ConditionVariable mServiceCondition = new ConditionVariable();
-
- public MyServiceClient(Context context) {
- mContext = context;
- }
-
- /**
- * Binds to a service in the test app to communicate state.
- * @param bindPriorityFlags Flags to influence the process-state of the bound app.
- */
- public void bind(int bindPriorityFlags) {
- if (mService != null) {
- throw new IllegalStateException("Already bound");
- }
- mServiceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mService = IMyService.Stub.asInterface(service);
- mServiceCondition.open();
- }
- @Override
- public void onServiceDisconnected(ComponentName name) {
- mServiceCondition.close();
- mService = null;
- }
- };
-
- final Intent intent = new Intent();
- intent.setComponent(new ComponentName(APP2_PACKAGE, SERVICE_NAME));
- // Needs to use BIND_NOT_FOREGROUND so app2 does not run in
- // the same process state as app
- mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE
- | bindPriorityFlags);
- ensureServiceConnection();
- }
-
- public void unbind() {
- if (mService != null) {
- mContext.unbindService(mServiceConnection);
- }
- }
-
- private void ensureServiceConnection() {
- if (mService != null) {
- return;
- }
- mServiceCondition.block(TIMEOUT_MS);
- if (mService == null) {
- throw new IllegalStateException(
- "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
- }
- }
-
- public void registerBroadcastReceiver() throws RemoteException {
- ensureServiceConnection();
- mService.registerBroadcastReceiver();
- }
-
- public int getCounters(String receiverName, String action) throws RemoteException {
- ensureServiceConnection();
- return mService.getCounters(receiverName, action);
- }
-
- /** Retrieves the network state as observed from the bound test app */
- public NetworkCheckResult checkNetworkStatus(String address) throws RemoteException {
- ensureServiceConnection();
- return mService.checkNetworkStatus(address);
- }
-
- public String getRestrictBackgroundStatus() throws RemoteException {
- ensureServiceConnection();
- return mService.getRestrictBackgroundStatus();
- }
-
- public void sendNotification(int notificationId, String notificationType)
- throws RemoteException {
- ensureServiceConnection();
- mService.sendNotification(notificationId, notificationType);
- }
-
- public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
- throws RemoteException {
- ensureServiceConnection();
- mService.registerNetworkCallback(request, cb);
- }
-
- public void unregisterNetworkCallback() throws RemoteException {
- ensureServiceConnection();
- mService.unregisterNetworkCallback();
- }
-
- public int scheduleJob(JobInfo jobInfo) throws RemoteException {
- ensureServiceConnection();
- return mService.scheduleJob(jobInfo);
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
deleted file mode 100644
index 3934cfa..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkCallbackTest.java
+++ /dev/null
@@ -1,428 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
-import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.getActiveNetworkCapabilities;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
-
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.net.cts.util.CtsNetUtils;
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.modules.utils.build.SdkLevel;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.Objects;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
- private Network mNetwork;
- private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
- private CtsNetUtils mCtsNetUtils;
- private static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
-
- @Rule
- public final MeterednessConfigurationRule mMeterednessConfiguration
- = new MeterednessConfigurationRule();
-
- enum CallbackState {
- NONE,
- AVAILABLE,
- LOST,
- BLOCKED_STATUS,
- CAPABILITIES
- }
-
- private static class CallbackInfo {
- public final CallbackState state;
- public final Network network;
- public final Object arg;
-
- CallbackInfo(CallbackState s, Network n, Object o) {
- state = s; network = n; arg = o;
- }
-
- public String toString() {
- return String.format("%s (%s) (%s)", state, network, arg);
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof CallbackInfo)) return false;
- // Ignore timeMs, since it's unpredictable.
- final CallbackInfo other = (CallbackInfo) o;
- return (state == other.state) && Objects.equals(network, other.network)
- && Objects.equals(arg, other.arg);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(state, network, arg);
- }
- }
-
- private class TestNetworkCallback extends INetworkCallback.Stub {
- private static final int TEST_CONNECT_TIMEOUT_MS = 30_000;
- private static final int TEST_CALLBACK_TIMEOUT_MS = 5_000;
-
- private final LinkedBlockingQueue<CallbackInfo> mCallbacks = new LinkedBlockingQueue<>();
-
- protected void setLastCallback(CallbackState state, Network network, Object o) {
- mCallbacks.offer(new CallbackInfo(state, network, o));
- }
-
- CallbackInfo nextCallback(int timeoutMs) {
- CallbackInfo cb = null;
- try {
- cb = mCallbacks.poll(timeoutMs, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- }
- if (cb == null) {
- fail("Did not receive callback after " + timeoutMs + "ms");
- }
- return cb;
- }
-
- CallbackInfo expectCallback(CallbackState state, Network expectedNetwork, Object o) {
- final CallbackInfo expected = new CallbackInfo(state, expectedNetwork, o);
- final CallbackInfo actual = nextCallback(TEST_CALLBACK_TIMEOUT_MS);
- assertEquals("Unexpected callback:", expected, actual);
- return actual;
- }
-
- @Override
- public void onAvailable(Network network) {
- setLastCallback(CallbackState.AVAILABLE, network, null);
- }
-
- @Override
- public void onLost(Network network) {
- setLastCallback(CallbackState.LOST, network, null);
- }
-
- @Override
- public void onBlockedStatusChanged(Network network, boolean blocked) {
- setLastCallback(CallbackState.BLOCKED_STATUS, network, blocked);
- }
-
- @Override
- public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
- setLastCallback(CallbackState.CAPABILITIES, network, cap);
- }
-
- public Network expectAvailableCallbackAndGetNetwork() {
- final CallbackInfo cb = nextCallback(TEST_CONNECT_TIMEOUT_MS);
- if (cb.state != CallbackState.AVAILABLE) {
- fail("Network is not available. Instead obtained the following callback :" + cb);
- }
- return cb.network;
- }
-
- public void drainAndWaitForIdle() {
- try {
- do {
- mCallbacks.drainTo(new ArrayList<>());
- } while (mCallbacks.poll(TEST_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS) != null);
- } catch (InterruptedException ie) {
- Log.e(TAG, "Interrupted while draining callback queue", ie);
- Thread.currentThread().interrupt();
- }
- }
-
- public void expectBlockedStatusCallback(Network expectedNetwork, boolean expectBlocked) {
- expectCallback(CallbackState.BLOCKED_STATUS, expectedNetwork, expectBlocked);
- }
-
- public void expectBlockedStatusCallbackEventually(Network expectedNetwork,
- boolean expectBlocked) {
- final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
- do {
- final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
- if (cb.state == CallbackState.BLOCKED_STATUS
- && cb.network.equals(expectedNetwork)) {
- assertEquals(expectBlocked, cb.arg);
- return;
- }
- } while (System.currentTimeMillis() <= deadline);
- fail("Didn't receive onBlockedStatusChanged()");
- }
-
- public void expectCapabilitiesCallbackEventually(Network expectedNetwork, boolean hasCap,
- int cap) {
- final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
- do {
- final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
- if (cb.state != CallbackState.CAPABILITIES
- || !expectedNetwork.equals(cb.network)
- || (hasCap != ((NetworkCapabilities) cb.arg).hasCapability(cap))) {
- Log.i("NetworkCallbackTest#expectCapabilitiesCallback",
- "Ignoring non-matching callback : " + cb);
- continue;
- }
- // Found a match, return
- return;
- } while (System.currentTimeMillis() <= deadline);
- fail("Didn't receive the expected callback to onCapabilitiesChanged(). Check the "
- + "log for a list of received callbacks, if any.");
- }
- }
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- assumeTrue(canChangeActiveNetworkMeteredness());
-
- registerBroadcastReceiver();
-
- removeRestrictBackgroundWhitelist(mUid);
- removeRestrictBackgroundBlacklist(mUid);
- assertRestrictBackgroundChangedReceived(0);
-
- // Initial state
- setBatterySaverMode(false);
- setRestrictBackground(false);
- setAppIdle(false);
-
- // Get transports of the active network, this has to be done before changing meteredness,
- // since wifi will be disconnected when changing from non-metered to metered.
- final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
-
- // Mark network as metered.
- mMeterednessConfiguration.configureNetworkMeteredness(true);
-
- // Register callback, copy the capabilities from the active network to expect the "original"
- // network before disconnecting, but null out some fields to prevent over-specified.
- registerNetworkCallback(new NetworkRequest.Builder()
- .setCapabilities(networkCapabilities.setTransportInfo(null))
- .removeCapability(NET_CAPABILITY_NOT_METERED)
- .setSignalStrength(SIGNAL_STRENGTH_UNSPECIFIED).build(), mTestNetworkCallback);
- // Wait for onAvailable() callback to ensure network is available before the test
- // and store the default network.
- mNetwork = mTestNetworkCallback.expectAvailableCallbackAndGetNetwork();
- // Check that the network is metered.
- mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
- false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
- mTestNetworkCallback.drainAndWaitForIdle();
-
- // Before Android T, DNS queries over private DNS should be but are not restricted by Power
- // Saver or Data Saver. The issue is fixed in mainline update and apps can no longer request
- // DNS queries when its network is restricted by Power Saver. The fix takes effect backwards
- // starting from Android T. But for Data Saver, the fix is not backward compatible since
- // there are some platform changes involved. It is only available on devices that a specific
- // trunk flag is enabled.
- //
- // This test can not only verify that the network traffic from apps is blocked at the right
- // time, but also verify whether it is correctly blocked at the DNS stage, or at a later
- // socket connection stage.
- if (SdkLevel.isAtLeastT()) {
- // Enable private DNS
- mCtsNetUtils = new CtsNetUtils(mContext);
- mCtsNetUtils.storePrivateDnsSetting();
- mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
- mCtsNetUtils.awaitPrivateDnsSetting(
- "NetworkCallbackTest wait private DNS setting timeout", mNetwork,
- GOOGLE_PRIVATE_DNS_SERVER, true);
- }
- }
-
- @After
- public void tearDown() throws Exception {
- super.tearDown();
-
- setRestrictBackground(false);
- setBatterySaverMode(false);
- unregisterNetworkCallback();
- stopApp();
-
- if (SdkLevel.isAtLeastT() && (mCtsNetUtils != null)) {
- mCtsNetUtils.restorePrivateDnsSetting();
- }
- }
-
- @RequiredProperties({DATA_SAVER_MODE})
- @Test
- public void testOnBlockedStatusChanged_dataSaver() throws Exception {
- try {
- // Enable restrict background
- setRestrictBackground(true);
- // TODO: Verify expectedUnavailableError when aconfig support mainline.
- // (see go/aconfig-in-mainline-problems)
- assertBackgroundNetworkAccess(false);
- assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-
- // Add to whitelist
- addRestrictBackgroundWhitelist(mUid);
- assertBackgroundNetworkAccess(true);
- assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-
- // Remove from whitelist
- removeRestrictBackgroundWhitelist(mUid);
- // TODO: Verify expectedUnavailableError when aconfig support mainline.
- assertBackgroundNetworkAccess(false);
- assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
- } finally {
- mMeterednessConfiguration.resetNetworkMeteredness();
- }
-
- // Set to non-metered network
- mMeterednessConfiguration.configureNetworkMeteredness(false);
- mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
- true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
- try {
- assertBackgroundNetworkAccess(true);
- assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-
- // Disable restrict background, should not trigger callback
- setRestrictBackground(false);
- assertBackgroundNetworkAccess(true);
- assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
- } finally {
- mMeterednessConfiguration.resetNetworkMeteredness();
- }
- }
-
- @RequiredProperties({BATTERY_SAVER_MODE})
- @Test
- public void testOnBlockedStatusChanged_powerSaver() throws Exception {
- try {
- // Enable Power Saver
- setBatterySaverMode(true);
- if (SdkLevel.isAtLeastT()) {
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- assertNetworkAccess(false, "java.net.UnknownHostException");
- } else {
- assertBackgroundNetworkAccess(false);
- }
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
- assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-
- // Disable Power Saver
- setBatterySaverMode(false);
- assertBackgroundNetworkAccess(true);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
- assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
- } finally {
- mMeterednessConfiguration.resetNetworkMeteredness();
- }
-
- // Set to non-metered network
- mMeterednessConfiguration.configureNetworkMeteredness(false);
- mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
- true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
- try {
- // Enable Power Saver
- setBatterySaverMode(true);
- if (SdkLevel.isAtLeastT()) {
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- assertNetworkAccess(false, "java.net.UnknownHostException");
- } else {
- assertBackgroundNetworkAccess(false);
- }
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
- assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
-
- // Disable Power Saver
- setBatterySaverMode(false);
- assertBackgroundNetworkAccess(true);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
- assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
- } finally {
- mMeterednessConfiguration.resetNetworkMeteredness();
- }
- }
-
- @Test
- public void testOnBlockedStatusChanged_default() throws Exception {
- assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-
- try {
- assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
- assertNetworkAccess(false, null);
- assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-
- launchActivity();
- assertTopState();
- assertNetworkAccess(true, null);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
- assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
-
- finishActivity();
- assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
- SystemClock.sleep(mProcessStateTransitionLongDelayMs);
- assertNetworkAccess(false, null);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
- assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-
- } finally {
- mMeterednessConfiguration.resetNetworkMeteredness();
- }
-
- // Set to non-metered network
- mMeterednessConfiguration.configureNetworkMeteredness(false);
- mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
- true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
- try {
- assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
- assertNetworkAccess(false, null);
- assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
-
- launchActivity();
- assertTopState();
- assertNetworkAccess(true, null);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
- assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
-
- finishActivity();
- assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
- SystemClock.sleep(mProcessStateTransitionLongDelayMs);
- assertNetworkAccess(false, null);
- mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
- assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
- } finally {
- mMeterednessConfiguration.resetNetworkMeteredness();
- }
- }
-
- // TODO: 1. test against VPN lockdown.
- // 2. test against multiple networks.
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
deleted file mode 100644
index 6c5f2ff..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyManagerTest.java
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-import static android.os.Process.SYSTEM_UID;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.assertNetworkingBlockedStatusForUid;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isUidNetworkingBlocked;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.netpolicy.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.netpolicy.hostside.Property.DATA_SAVER_MODE;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class NetworkPolicyManagerTest extends AbstractRestrictBackgroundNetworkTestCase {
- private static final boolean METERED = true;
- private static final boolean NON_METERED = false;
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- registerBroadcastReceiver();
-
- removeRestrictBackgroundWhitelist(mUid);
- removeRestrictBackgroundBlacklist(mUid);
- assertRestrictBackgroundChangedReceived(0);
-
- // Initial state
- setBatterySaverMode(false);
- setRestrictBackground(false);
- setRestrictedNetworkingMode(false);
- }
-
- @After
- public void tearDown() throws Exception {
- super.tearDown();
-
- setBatterySaverMode(false);
- setRestrictBackground(false);
- setRestrictedNetworkingMode(false);
- unregisterNetworkCallback();
- stopApp();
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
- // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
- // test the cases of non-metered network and uid not matched by any rule.
- // If mUid is not blocked by data saver mode or power saver mode, no matter the network is
- // metered or non-metered, mUid shouldn't be blocked.
- assertFalse(isUidNetworkingBlocked(mUid, METERED)); // Match NTWK_ALLOWED_DEFAULT
- assertFalse(isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
- }
-
- @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE})
- @Test
- public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
- // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
- // test the case of uid is system uid.
- // SYSTEM_UID will never be blocked.
- assertFalse(isUidNetworkingBlocked(SYSTEM_UID, METERED)); // Match NTWK_ALLOWED_SYSTEM
- assertFalse(isUidNetworkingBlocked(SYSTEM_UID, NON_METERED)); // Match NTWK_ALLOWED_SYSTEM
- try {
- setRestrictBackground(true);
- setBatterySaverMode(true);
- setRestrictedNetworkingMode(true);
- assertNetworkingBlockedStatusForUid(SYSTEM_UID, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_SYSTEM
- assertFalse(
- isUidNetworkingBlocked(SYSTEM_UID, NON_METERED)); // Match NTWK_ALLOWED_SYSTEM
- } finally {
- setRestrictBackground(false);
- setBatterySaverMode(false);
- setRestrictedNetworkingMode(false);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
- }
- }
-
- @RequiredProperties({DATA_SAVER_MODE})
- @Test
- public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
- // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
- // test the cases of non-metered network, uid is matched by restrict background blacklist,
- // uid is matched by restrict background whitelist, app is in the foreground with restrict
- // background enabled and the app is in the background with restrict background enabled.
- try {
- // Enable restrict background and mUid will be blocked because it's not in the
- // foreground.
- setRestrictBackground(true);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
-
- // Although restrict background is enabled and mUid is in the background, but mUid will
- // not be blocked if network is non-metered.
- assertFalse(
- isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-
- // Add mUid into the restrict background blacklist.
- addRestrictBackgroundBlacklist(mUid);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- true /* expectedResult */); // Match NTWK_BLOCKED_DENYLIST
-
- // Although mUid is in the restrict background blacklist, but mUid won't be blocked if
- // the network is non-metered.
- assertFalse(
- isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
- removeRestrictBackgroundBlacklist(mUid);
-
- // Add mUid into the restrict background whitelist.
- addRestrictBackgroundWhitelist(mUid);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_ALLOWLIST
- assertFalse(
- isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
- removeRestrictBackgroundWhitelist(mUid);
-
- // Make TEST_APP2_PKG go to foreground and mUid will be allowed temporarily.
- launchActivity();
- assertTopState();
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_TMP_ALLOWLIST
-
- // Back to background.
- finishActivity();
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
- } finally {
- setRestrictBackground(false);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
- }
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
- // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
- // test the cases of restricted networking mode enabled.
- try {
- // All apps should be blocked if restricted networking mode is enabled except for those
- // apps who have CONNECTIVITY_USE_RESTRICTED_NETWORKS permission.
- // This test won't test if an app who has CONNECTIVITY_USE_RESTRICTED_NETWORKS will not
- // be blocked because CONNECTIVITY_USE_RESTRICTED_NETWORKS is a signature/privileged
- // permission that CTS cannot acquire. Also it's not good for this test to use those
- // privileged apps which have CONNECTIVITY_USE_RESTRICTED_NETWORKS to test because there
- // is no guarantee that those apps won't remove this permission someday, and if it
- // happens, then this test will fail.
- setRestrictedNetworkingMode(true);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- true /* expectedResult */); // Match NTWK_BLOCKED_RESTRICTED_MODE
- assertTrue(isUidNetworkingBlocked(mUid,
- NON_METERED)); // Match NTWK_BLOCKED_RESTRICTED_MODE
- } finally {
- setRestrictedNetworkingMode(false);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
- }
- }
-
- @RequiredProperties({BATTERY_SAVER_MODE})
- @Test
- public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
- // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
- // test the cases of power saver mode enabled, uid in the power saver mode whitelist and
- // uid in the power saver mode whitelist with non-metered network.
- try {
- // mUid should be blocked if power saver mode is enabled.
- setBatterySaverMode(true);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- true /* expectedResult */); // Match NTWK_BLOCKED_POWER
- assertTrue(isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_BLOCKED_POWER
-
- // Add TEST_APP2_PKG into power saver mode whitelist, its uid rule is RULE_ALLOW_ALL and
- // it shouldn't be blocked.
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
- assertFalse(
- isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- } finally {
- setBatterySaverMode(false);
- assertNetworkingBlockedStatusForUid(mUid, METERED,
- false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
- }
- }
-
- @RequiredProperties({DATA_SAVER_MODE})
- @Test
- public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
- try {
- // isUidRestrictedOnMeteredNetworks() will only return true when restrict background is
- // enabled and mUid is not in the restrict background whitelist and TEST_APP2_PKG is not
- // in the foreground. For other cases, it will return false.
- setRestrictBackground(true);
- assertIsUidRestrictedOnMeteredNetworks(mUid, true /* expectedResult */);
-
- // Make TEST_APP2_PKG go to foreground and isUidRestrictedOnMeteredNetworks() will
- // return false.
- launchActivity();
- assertTopState();
- assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
- // Back to background.
- finishActivity();
- assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-
- // Add mUid into restrict background whitelist and isUidRestrictedOnMeteredNetworks()
- // will return false.
- addRestrictBackgroundWhitelist(mUid);
- assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
- removeRestrictBackgroundWhitelist(mUid);
- } finally {
- // Restrict background is disabled and isUidRestrictedOnMeteredNetworks() will return
- // false.
- setRestrictBackground(false);
- assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
- }
- }
-
- @Test
- public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
- assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-
- try {
- assertProcessStateBelow(PROCESS_STATE_LAST_ACTIVITY);
- SystemClock.sleep(mProcessStateTransitionShortDelayMs);
- assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
- assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
-
- launchActivity();
- assertTopState();
- assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
- assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
-
- finishActivity();
- assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
- SystemClock.sleep(mProcessStateTransitionLongDelayMs);
- assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
- assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
- assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
- } finally {
- removePowerSaveModeWhitelist(TEST_APP2_PKG);
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java
deleted file mode 100644
index 0207b00..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestRunner.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2020 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.cts.netpolicy.hostside;
-
-import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
-
-import org.junit.rules.RunRules;
-import org.junit.rules.TestRule;
-import org.junit.runners.model.FrameworkMethod;
-import org.junit.runners.model.InitializationError;
-import org.junit.runners.model.Statement;
-
-import java.util.List;
-
-/**
- * Custom runner to allow dumping logs after a test failure before the @After methods get to run.
- */
-public class NetworkPolicyTestRunner extends AndroidJUnit4ClassRunner {
- private TestRule mDumpOnFailureRule = new DumpOnFailureRule();
-
- public NetworkPolicyTestRunner(Class<?> klass) throws InitializationError {
- super(klass);
- }
-
- @Override
- public Statement methodInvoker(FrameworkMethod method, Object test) {
- return new RunRules(super.methodInvoker(method, test), List.of(mDumpOnFailureRule),
- describeChild(method));
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java
deleted file mode 100644
index 26a88f2..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/NetworkPolicyTestUtils.java
+++ /dev/null
@@ -1,486 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
-import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
-
-import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.location.LocationManager;
-import android.net.ConnectivityManager;
-import android.net.ConnectivityManager.NetworkCallback;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkPolicyManager;
-import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiManager;
-import android.net.wifi.WifiManager.ActionListener;
-import android.os.PersistableBundle;
-import android.os.Process;
-import android.os.UserHandle;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.data.ApnSetting;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.UiDevice;
-
-import com.android.compatibility.common.util.AppStandbyUtils;
-import com.android.compatibility.common.util.BatteryUtils;
-import com.android.compatibility.common.util.PollingCheck;
-import com.android.compatibility.common.util.ShellIdentityUtils;
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-public class NetworkPolicyTestUtils {
-
- // android.telephony.CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS
- // TODO: Expose it as a @TestApi instead of copying the constant
- private static final String KEY_CARRIER_METERED_APN_TYPES_STRINGS =
- "carrier_metered_apn_types_strings";
-
- private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 10_000;
-
- private static ConnectivityManager mCm;
- private static WifiManager mWm;
- private static CarrierConfigManager mCarrierConfigManager;
- private static NetworkPolicyManager sNpm;
-
- private static Boolean mBatterySaverSupported;
- private static Boolean mDataSaverSupported;
- private static Boolean mDozeModeSupported;
- private static Boolean mAppStandbySupported;
-
- private NetworkPolicyTestUtils() {}
-
- public static boolean isBatterySaverSupported() {
- if (mBatterySaverSupported == null) {
- mBatterySaverSupported = BatteryUtils.isBatterySaverSupported();
- }
- return mBatterySaverSupported;
- }
-
- private static boolean isWear() {
- return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
- }
-
- /**
- * As per CDD requirements, if the device doesn't support data saver mode then
- * ConnectivityManager.getRestrictBackgroundStatus() will always return
- * RESTRICT_BACKGROUND_STATUS_DISABLED. So, enable the data saver mode and check if
- * ConnectivityManager.getRestrictBackgroundStatus() for an app in background returns
- * RESTRICT_BACKGROUND_STATUS_DISABLED or not.
- */
- public static boolean isDataSaverSupported() {
- if (isWear()) {
- return false;
- }
- if (mDataSaverSupported == null) {
- setRestrictBackgroundInternal(false);
- assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
- try {
- setRestrictBackgroundInternal(true);
- mDataSaverSupported = !isMyRestrictBackgroundStatus(
- RESTRICT_BACKGROUND_STATUS_DISABLED);
- } finally {
- setRestrictBackgroundInternal(false);
- }
- }
- return mDataSaverSupported;
- }
-
- public static boolean isDozeModeSupported() {
- if (mDozeModeSupported == null) {
- final String result = executeShellCommand("cmd deviceidle enabled deep");
- mDozeModeSupported = result.equals("1");
- }
- return mDozeModeSupported;
- }
-
- public static boolean isAppStandbySupported() {
- if (mAppStandbySupported == null) {
- mAppStandbySupported = AppStandbyUtils.isAppStandbyEnabled();
- }
- return mAppStandbySupported;
- }
-
- public static boolean isLowRamDevice() {
- final ActivityManager am = (ActivityManager) getContext().getSystemService(
- Context.ACTIVITY_SERVICE);
- return am.isLowRamDevice();
- }
-
- /** Forces JobScheduler to run the job if constraints are met. */
- public static void forceRunJob(String pkg, int jobId) {
- executeShellCommand("cmd jobscheduler run -f -u " + UserHandle.myUserId()
- + " " + pkg + " " + jobId);
- }
-
- public static boolean isLocationEnabled() {
- final LocationManager lm = (LocationManager) getContext().getSystemService(
- Context.LOCATION_SERVICE);
- return lm.isLocationEnabled();
- }
-
- public static void setLocationEnabled(boolean enabled) {
- final LocationManager lm = (LocationManager) getContext().getSystemService(
- Context.LOCATION_SERVICE);
- lm.setLocationEnabledForUser(enabled, Process.myUserHandle());
- assertEquals("Couldn't change location enabled state", lm.isLocationEnabled(), enabled);
- Log.d(TAG, "Changed location enabled state to " + enabled);
- }
-
- public static boolean isActiveNetworkMetered(boolean metered) {
- return getConnectivityManager().isActiveNetworkMetered() == metered;
- }
-
- public static boolean canChangeActiveNetworkMeteredness() {
- final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
- return networkCapabilities.hasTransport(TRANSPORT_WIFI)
- || networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
- }
-
- /**
- * Updates the meteredness of the active network. Right now we can only change meteredness
- * of either Wifi or cellular network, so if the active network is not either of these, this
- * will throw an exception.
- *
- * @return a {@link ThrowingRunnable} object that can used to reset the meteredness change
- * made by this method.
- */
- public static ThrowingRunnable setupActiveNetworkMeteredness(boolean metered) throws Exception {
- if (isActiveNetworkMetered(metered)) {
- return null;
- }
- final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
- if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
- final String ssid = getWifiSsid();
- setWifiMeteredStatus(ssid, metered);
- return () -> setWifiMeteredStatus(ssid, !metered);
- } else if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
- final int subId = SubscriptionManager.getActiveDataSubscriptionId();
- setCellularMeteredStatus(subId, metered);
- return () -> setCellularMeteredStatus(subId, !metered);
- } else {
- // Right now, we don't have a way to change meteredness of networks other
- // than Wi-Fi or Cellular, so just throw an exception.
- throw new IllegalStateException("Can't change meteredness of current active network");
- }
- }
-
- private static String getWifiSsid() {
- final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
- try {
- uiAutomation.adoptShellPermissionIdentity();
- final String ssid = getWifiManager().getConnectionInfo().getSSID();
- assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
- return ssid;
- } finally {
- uiAutomation.dropShellPermissionIdentity();
- }
- }
-
- static NetworkCapabilities getActiveNetworkCapabilities() {
- final Network activeNetwork = getConnectivityManager().getActiveNetwork();
- assertNotNull("No active network available", activeNetwork);
- return getConnectivityManager().getNetworkCapabilities(activeNetwork);
- }
-
- private static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
- final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
- try {
- uiAutomation.adoptShellPermissionIdentity();
- final WifiConfiguration currentConfig = getWifiConfiguration(ssid);
- currentConfig.meteredOverride = metered
- ? METERED_OVERRIDE_METERED : METERED_OVERRIDE_NONE;
- BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
- getWifiManager().save(currentConfig, createActionListener(
- blockingQueue, Integer.MAX_VALUE));
- Integer resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
- TimeUnit.MILLISECONDS);
- if (resultCode == null) {
- fail("Timed out waiting for meteredness to change; ssid=" + ssid
- + ", metered=" + metered);
- } else if (resultCode != Integer.MAX_VALUE) {
- fail("Error overriding the meteredness; ssid=" + ssid
- + ", metered=" + metered + ", error=" + resultCode);
- }
- final boolean success = assertActiveNetworkMetered(metered, false /* throwOnFailure */);
- if (!success) {
- Log.i(TAG, "Retry connecting to wifi; ssid=" + ssid);
- blockingQueue = new LinkedBlockingQueue<>();
- getWifiManager().connect(currentConfig, createActionListener(
- blockingQueue, Integer.MAX_VALUE));
- resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
- TimeUnit.MILLISECONDS);
- if (resultCode == null) {
- fail("Timed out waiting for wifi to connect; ssid=" + ssid);
- } else if (resultCode != Integer.MAX_VALUE) {
- fail("Error connecting to wifi; ssid=" + ssid
- + ", error=" + resultCode);
- }
- assertActiveNetworkMetered(metered, true /* throwOnFailure */);
- }
- } finally {
- uiAutomation.dropShellPermissionIdentity();
- }
- }
-
- private static WifiConfiguration getWifiConfiguration(String ssid) {
- final List<String> ssids = new ArrayList<>();
- for (WifiConfiguration config : getWifiManager().getConfiguredNetworks()) {
- if (config.SSID.equals(ssid)) {
- return config;
- }
- ssids.add(config.SSID);
- }
- fail("Couldn't find the wifi config; ssid=" + ssid
- + ", all=" + Arrays.toString(ssids.toArray()));
- return null;
- }
-
- private static ActionListener createActionListener(BlockingQueue<Integer> blockingQueue,
- int successCode) {
- return new ActionListener() {
- @Override
- public void onSuccess() {
- blockingQueue.offer(successCode);
- }
-
- @Override
- public void onFailure(int reason) {
- blockingQueue.offer(reason);
- }
- };
- }
-
- private static void setCellularMeteredStatus(int subId, boolean metered) throws Exception {
- final PersistableBundle bundle = new PersistableBundle();
- bundle.putStringArray(KEY_CARRIER_METERED_APN_TYPES_STRINGS,
- new String[] {ApnSetting.TYPE_MMS_STRING});
- ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(getCarrierConfigManager(),
- (cm) -> cm.overrideConfig(subId, metered ? null : bundle));
- assertActiveNetworkMetered(metered, true /* throwOnFailure */);
- }
-
- private static boolean assertActiveNetworkMetered(boolean expectedMeteredStatus,
- boolean throwOnFailure) throws Exception {
- final CountDownLatch latch = new CountDownLatch(1);
- final NetworkCallback networkCallback = new NetworkCallback() {
- @Override
- public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
- final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
- if (metered == expectedMeteredStatus) {
- latch.countDown();
- }
- }
- };
- // Registering a callback here guarantees onCapabilitiesChanged is called immediately
- // with the current setting. Therefore, if the setting has already been changed,
- // this method will return right away, and if not it will wait for the setting to change.
- getConnectivityManager().registerDefaultNetworkCallback(networkCallback);
- try {
- if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) {
- final String errorMsg = "Timed out waiting for active network metered status "
- + "to change to " + expectedMeteredStatus + "; network = "
- + getConnectivityManager().getActiveNetwork();
- if (throwOnFailure) {
- fail(errorMsg);
- }
- Log.w(TAG, errorMsg);
- return false;
- }
- return true;
- } finally {
- getConnectivityManager().unregisterNetworkCallback(networkCallback);
- }
- }
-
- public static void setRestrictBackground(boolean enabled) {
- if (!isDataSaverSupported()) {
- return;
- }
- setRestrictBackgroundInternal(enabled);
- }
-
- static void setRestrictBackgroundInternal(boolean enabled) {
- executeShellCommand("cmd netpolicy set restrict-background " + enabled);
- final String output = executeShellCommand("cmd netpolicy get restrict-background");
- final String expectedSuffix = enabled ? "enabled" : "disabled";
- assertTrue("output '" + output + "' should end with '" + expectedSuffix + "'",
- output.endsWith(expectedSuffix));
- }
-
- public static boolean isMyRestrictBackgroundStatus(int expectedStatus) {
- final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
- if (expectedStatus != actualStatus) {
- Log.d(TAG, "MyRestrictBackgroundStatus: "
- + "Expected: " + restrictBackgroundValueToString(expectedStatus)
- + "; Actual: " + restrictBackgroundValueToString(actualStatus));
- return false;
- }
- return true;
- }
-
- // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
- private static String unquoteSSID(String ssid) {
- // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
- // Otherwise it's guaranteed not to start with a quote.
- if (ssid.charAt(0) == '"') {
- return ssid.substring(1, ssid.length() - 1);
- } else {
- return ssid;
- }
- }
-
- public static String restrictBackgroundValueToString(int status) {
- switch (status) {
- case RESTRICT_BACKGROUND_STATUS_DISABLED:
- return "DISABLED";
- case RESTRICT_BACKGROUND_STATUS_WHITELISTED:
- return "WHITELISTED";
- case RESTRICT_BACKGROUND_STATUS_ENABLED:
- return "ENABLED";
- default:
- return "UNKNOWN_STATUS_" + status;
- }
- }
-
- public static void clearSnoozeTimestamps() {
- executeShellCommand("dumpsys netpolicy --unsnooze");
- }
-
- public static String executeShellCommand(String command) {
- final String result = runShellCommandOrThrow(command).trim();
- Log.d(TAG, "Output of '" + command + "': '" + result + "'");
- return result;
- }
-
- public static void assertMyRestrictBackgroundStatus(int expectedStatus) {
- final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
- assertEquals(restrictBackgroundValueToString(expectedStatus),
- restrictBackgroundValueToString(actualStatus));
- }
-
- public static ConnectivityManager getConnectivityManager() {
- if (mCm == null) {
- mCm = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
- }
- return mCm;
- }
-
- public static WifiManager getWifiManager() {
- if (mWm == null) {
- mWm = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
- }
- return mWm;
- }
-
- public static CarrierConfigManager getCarrierConfigManager() {
- if (mCarrierConfigManager == null) {
- mCarrierConfigManager = (CarrierConfigManager) getContext().getSystemService(
- Context.CARRIER_CONFIG_SERVICE);
- }
- return mCarrierConfigManager;
- }
-
- public static NetworkPolicyManager getNetworkPolicyManager() {
- if (sNpm == null) {
- sNpm = getContext().getSystemService(NetworkPolicyManager.class);
- }
- return sNpm;
- }
-
- public static Context getContext() {
- return getInstrumentation().getContext();
- }
-
- public static Instrumentation getInstrumentation() {
- return InstrumentationRegistry.getInstrumentation();
- }
-
- public static UiDevice getUiDevice() {
- return UiDevice.getInstance(getInstrumentation());
- }
-
- // When power saver mode or restrict background enabled or adding any white/black list into
- // those modes, NetworkPolicy may need to take some time to update the rules of uids. So having
- // this function and using PollingCheck to try to make sure the uid has updated and reduce the
- // flaky rate.
- public static void assertNetworkingBlockedStatusForUid(int uid, boolean metered,
- boolean expectedResult) {
- final String errMsg = String.format("Unexpected result from isUidNetworkingBlocked; "
- + "uid= " + uid + ", metered=" + metered + ", expected=" + expectedResult);
- PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)),
- errMsg);
- }
-
- public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult) {
- final String errMsg = String.format(
- "Unexpected result from isUidRestrictedOnMeteredNetworks; "
- + "uid= " + uid + ", expected=" + expectedResult);
- PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)),
- errMsg);
- }
-
- public static boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
- final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
- try {
- uiAutomation.adoptShellPermissionIdentity();
- return getNetworkPolicyManager().isUidNetworkingBlocked(uid, meteredNetwork);
- } finally {
- uiAutomation.dropShellPermissionIdentity();
- }
- }
-
- public static boolean isUidRestrictedOnMeteredNetworks(int uid) {
- final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
- try {
- uiAutomation.adoptShellPermissionIdentity();
- return getNetworkPolicyManager().isUidRestrictedOnMeteredNetworks(uid);
- } finally {
- uiAutomation.dropShellPermissionIdentity();
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java
deleted file mode 100644
index a03833f..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/Property.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDataSaverSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
-import static com.android.cts.netpolicy.hostside.NetworkPolicyTestUtils.isLowRamDevice;
-
-public enum Property {
- BATTERY_SAVER_MODE(1 << 0) {
- public boolean isSupported() { return isBatterySaverSupported(); }
- },
-
- DATA_SAVER_MODE(1 << 1) {
- public boolean isSupported() { return isDataSaverSupported(); }
- },
-
- NO_DATA_SAVER_MODE(~DATA_SAVER_MODE.getValue()) {
- public boolean isSupported() { return !isDataSaverSupported(); }
- },
-
- DOZE_MODE(1 << 2) {
- public boolean isSupported() { return isDozeModeSupported(); }
- },
-
- APP_STANDBY_MODE(1 << 3) {
- public boolean isSupported() { return isAppStandbySupported(); }
- },
-
- NOT_LOW_RAM_DEVICE(1 << 4) {
- public boolean isSupported() { return !isLowRamDevice(); }
- },
-
- METERED_NETWORK(1 << 5) {
- public boolean isSupported() {
- return isActiveNetworkMetered(true) || canChangeActiveNetworkMeteredness();
- }
- },
-
- NON_METERED_NETWORK(~METERED_NETWORK.getValue()) {
- public boolean isSupported() {
- return isActiveNetworkMetered(false) || canChangeActiveNetworkMeteredness();
- }
- };
-
- private int mValue;
-
- Property(int value) { mValue = value; }
-
- public int getValue() { return mValue; }
-
- abstract boolean isSupported();
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java
deleted file mode 100644
index 799a513..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredProperties.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-@Retention(RUNTIME)
-@Target({METHOD, TYPE})
-@Inherited
-public @interface RequiredProperties {
- Property[] value();
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java
deleted file mode 100644
index 5dea67c..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RequiredPropertiesRule.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy.hostside;
-
-import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-
-import android.text.TextUtils;
-import android.util.ArraySet;
-import android.util.Log;
-
-import com.android.compatibility.common.util.BeforeAfterRule;
-
-import org.junit.Assume;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-public class RequiredPropertiesRule extends BeforeAfterRule {
-
- private static ArraySet<Property> mRequiredProperties;
-
- @Override
- public void onBefore(Statement base, Description description) {
- mRequiredProperties = getAllRequiredProperties(description);
-
- final String testName = description.getClassName() + "#" + description.getMethodName();
- assertTestIsValid(testName, mRequiredProperties);
- Log.i(TAG, "Running test " + testName + " with required properties: "
- + propertiesToString(mRequiredProperties));
- }
-
- private ArraySet<Property> getAllRequiredProperties(Description description) {
- final ArraySet<Property> allRequiredProperties = new ArraySet<>();
- RequiredProperties requiredProperties = description.getAnnotation(RequiredProperties.class);
- if (requiredProperties != null) {
- Collections.addAll(allRequiredProperties, requiredProperties.value());
- }
-
- for (Class<?> clazz = description.getTestClass();
- clazz != null; clazz = clazz.getSuperclass()) {
- requiredProperties = clazz.getDeclaredAnnotation(RequiredProperties.class);
- if (requiredProperties == null) {
- continue;
- }
- for (Property requiredProperty : requiredProperties.value()) {
- for (Property p : Property.values()) {
- if (p.getValue() == ~requiredProperty.getValue()
- && allRequiredProperties.contains(p)) {
- continue;
- }
- }
- allRequiredProperties.add(requiredProperty);
- }
- }
- return allRequiredProperties;
- }
-
- private void assertTestIsValid(String testName, ArraySet<Property> requiredProperies) {
- if (requiredProperies == null) {
- return;
- }
- final ArrayList<Property> unsupportedProperties = new ArrayList<>();
- for (Property property : requiredProperies) {
- if (!property.isSupported()) {
- unsupportedProperties.add(property);
- }
- }
- Assume.assumeTrue("Unsupported properties: "
- + propertiesToString(unsupportedProperties), unsupportedProperties.isEmpty());
- }
-
- public static ArraySet<Property> getRequiredProperties() {
- return mRequiredProperties;
- }
-
- private static String propertiesToString(Iterable<Property> properties) {
- return "[" + TextUtils.join(",", properties) + "]";
- }
-}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java b/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java
deleted file mode 100644
index f183f4e..0000000
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/RestrictedModeTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public final class RestrictedModeTest extends AbstractRestrictBackgroundNetworkTestCase {
- @Before
- public void setUp() throws Exception {
- super.setUp();
- setRestrictedNetworkingMode(false);
- }
-
- @After
- public void tearDown() throws Exception {
- setRestrictedNetworkingMode(false);
- super.tearDown();
- }
-
- @Test
- public void testNetworkAccess() throws Exception {
- // go to foreground state and enable restricted mode
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- setRestrictedNetworkingMode(true);
- assertTopNetworkAccess(false);
-
- // go to background state
- finishActivity();
- assertBackgroundNetworkAccess(false);
-
- // disable restricted mode and assert network access in foreground and background states
- setRestrictedNetworkingMode(false);
- launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- assertTopNetworkAccess(true);
-
- // go to background state
- finishActivity();
- assertBackgroundNetworkAccess(true);
- }
-
- @Test
- public void testNetworkAccess_withBatterySaver() throws Exception {
- setBatterySaverMode(true);
- try {
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
- assertBackgroundNetworkAccess(true);
-
- setRestrictedNetworkingMode(true);
- // App would be denied network access since Restricted mode is on.
- assertBackgroundNetworkAccess(false);
- setRestrictedNetworkingMode(false);
- // Given that Restricted mode is turned off, app should be able to access network again.
- assertBackgroundNetworkAccess(true);
- } finally {
- setBatterySaverMode(false);
- }
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/Android.bp b/tests/cts/hostside-network-policy/app2/Android.bp
deleted file mode 100644
index 6ef0b06..0000000
--- a/tests/cts/hostside-network-policy/app2/Android.bp
+++ /dev/null
@@ -1,39 +0,0 @@
-//
-// 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_framework_backstage_power",
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test_helper_app {
- name: "CtsHostsideNetworkPolicyTestsApp2",
- defaults: ["cts_support_defaults"],
- platform_apis: true,
- static_libs: [
- "androidx.annotation_annotation",
- "CtsHostsideNetworkPolicyTestsAidl",
- "modules-utils-build",
- ],
- srcs: ["src/**/*.java"],
- // Tag this module as a cts test artifact
- test_suites: [
- "cts",
- "general-tests",
- "sts",
- ],
- certificate: ":cts-netpolicy-app",
-}
diff --git a/tests/cts/hostside-network-policy/app2/AndroidManifest.xml b/tests/cts/hostside-network-policy/app2/AndroidManifest.xml
deleted file mode 100644
index 668f2da..0000000
--- a/tests/cts/hostside-network-policy/app2/AndroidManifest.xml
+++ /dev/null
@@ -1,77 +0,0 @@
-<?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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.cts.netpolicy.hostside.app2">
-
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
- <uses-permission android:name="android.permission.INTERNET"/>
- <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
- <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
-
- <!--
- This application is used to listen to RESTRICT_BACKGROUND_CHANGED intents and store
- them in a shared preferences which is then read by the test app. These broadcasts are
- handled by 2 listeners, one defined the manifest and another dynamically registered by
- a service.
-
- The manifest-defined listener also handles ordered broadcasts used to share data with the
- test app.
-
- This application also provides a service, RemoteSocketFactoryService, that the test app can
- use to open sockets to remote hosts as a different user ID.
- -->
- <application android:usesCleartextTraffic="true"
- android:testOnly="true"
- android:debuggable="true">
-
- <activity android:name=".MyActivity"
- android:exported="true"/>
- <service android:name=".MyService"
- android:exported="true"/>
- <service android:name=".MyForegroundService"
- android:foregroundServiceType="specialUse"
- android:exported="true">
- <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
- android:value="Connectivity" />
- </service>
- <receiver android:name=".MyBroadcastReceiver"
- android:exported="true">
- <intent-filter>
- <action android:name="android.net.conn.RESTRICT_BACKGROUND_CHANGED"/>
- <action android:name="com.android.cts.netpolicy.hostside.app2.action.GET_COUNTERS"/>
- <action android:name="com.android.cts.netpolicy.hostside.app2.action.GET_RESTRICT_BACKGROUND_STATUS"/>
- <action android:name="com.android.cts.netpolicy.hostside.app2.action.CHECK_NETWORK"/>
- <action android:name="com.android.cts.netpolicy.hostside.app2.action.SEND_NOTIFICATION"/>
- <action android:name="com.android.cts.netpolicy.hostside.app2.action.SHOW_TOAST"/>
- </intent-filter>
- </receiver>
- <service android:name=".MyJobService"
- android:permission="android.permission.BIND_JOB_SERVICE" />
- </application>
-
- <!--
- Adding this to make sure that receiving the broadcast is not restricted by
- package visibility restrictions.
- -->
- <queries>
- <package android:name="android" />
- </queries>
-
-</manifest>
diff --git a/tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png b/tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png
deleted file mode 100644
index 6ae570b..0000000
--- a/tests/cts/hostside-network-policy/app2/res/drawable/ic_notification.png
+++ /dev/null
Binary files differ
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java
deleted file mode 100644
index 1719f9b..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/Common.java
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_OTHER;
-import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES;
-import static com.android.cts.netpolicy.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.netpolicy.hostside.INetworkStateObserver;
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.URL;
-import java.util.concurrent.TimeUnit;
-
-public final class Common {
-
- static final String TAG = "CtsNetApp2";
-
- // Constants below must match values defined on app's
- // AbstractRestrictBackgroundNetworkTestCase.java
- static final String MANIFEST_RECEIVER = "ManifestReceiver";
- static final String DYNAMIC_RECEIVER = "DynamicReceiver";
-
- static final String ACTION_RECEIVER_READY =
- "com.android.cts.netpolicy.hostside.app2.action.RECEIVER_READY";
- static final String ACTION_FINISH_ACTIVITY =
- "com.android.cts.netpolicy.hostside.app2.action.FINISH_ACTIVITY";
- static final String ACTION_FINISH_JOB =
- "com.android.cts.netpolicy.hostside.app2.action.FINISH_JOB";
- static final String ACTION_SHOW_TOAST =
- "com.android.cts.netpolicy.hostside.app2.action.SHOW_TOAST";
- // Copied from com.android.server.net.NetworkPolicyManagerService class
- static final String ACTION_SNOOZE_WARNING =
- "com.android.server.net.action.SNOOZE_WARNING";
-
- private static final String DEFAULT_TEST_URL =
- "https://connectivitycheck.android.com/generate_204";
-
- static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
- static final String NOTIFICATION_TYPE_DELETE = "DELETE";
- static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
- static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE";
- static final String NOTIFICATION_TYPE_ACTION = "ACTION";
- static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
- static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
-
- static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
- static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
- static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
- static final String KEY_CUSTOM_URL = TEST_PKG + ".custom_url";
-
- static final int TYPE_COMPONENT_ACTIVTY = 0;
- static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
- static final int TYPE_COMPONENT_EXPEDITED_JOB = 2;
- private static final int NETWORK_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10);
-
- static int getUid(Context context) {
- final String packageName = context.getPackageName();
- try {
- return context.getPackageManager().getPackageUid(packageName, 0);
- } catch (NameNotFoundException e) {
- throw new IllegalStateException("Could not get UID for " + packageName, e);
- }
- }
-
- private static NetworkCheckResult createNetworkCheckResult(boolean connected, String details,
- NetworkInfo networkInfo) {
- final NetworkCheckResult checkResult = new NetworkCheckResult();
- checkResult.connected = connected;
- checkResult.details = details;
- checkResult.networkInfo = networkInfo;
- return checkResult;
- }
-
- private static boolean validateComponentState(Context context, int componentType,
- INetworkStateObserver observer) throws RemoteException {
- final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
- switch (componentType) {
- case TYPE_COMPONENT_ACTIVTY: {
- final int procState = activityManager.getUidProcessState(Process.myUid());
- if (procState != ActivityManager.PROCESS_STATE_TOP) {
- observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_PROC_STATE,
- createNetworkCheckResult(false, "Unexpected procstate: " + procState,
- null));
- return false;
- }
- return true;
- }
- case TYPE_COMPONENT_FOREGROUND_SERVICE: {
- final int procState = activityManager.getUidProcessState(Process.myUid());
- if (procState != ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
- observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_PROC_STATE,
- createNetworkCheckResult(false, "Unexpected procstate: " + procState,
- null));
- return false;
- }
- return true;
- }
- case TYPE_COMPONENT_EXPEDITED_JOB: {
- final int capabilities = activityManager.getUidProcessCapabilities(Process.myUid());
- if ((capabilities
- & ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK) == 0) {
- observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_CAPABILITIES,
- createNetworkCheckResult(false,
- "Unexpected capabilities: " + capabilities, null));
- return false;
- }
- return true;
- }
- default: {
- observer.onNetworkStateChecked(RESULT_ERROR_OTHER,
- createNetworkCheckResult(false, "Unknown component type: " + componentType,
- null));
- return false;
- }
- }
- }
-
- static void notifyNetworkStateObserver(Context context, Intent intent, int componentType) {
- if (intent == null) {
- return;
- }
- final Bundle extras = intent.getExtras();
- notifyNetworkStateObserver(context, extras, componentType);
- }
-
- static void notifyNetworkStateObserver(Context context, Bundle extras, int componentType) {
- if (extras == null) {
- return;
- }
- final INetworkStateObserver observer = INetworkStateObserver.Stub.asInterface(
- extras.getBinder(KEY_NETWORK_STATE_OBSERVER));
- if (observer != null) {
- final String customUrl = extras.getString(KEY_CUSTOM_URL);
- try {
- final boolean skipValidation = extras.getBoolean(KEY_SKIP_VALIDATION_CHECKS);
- if (!skipValidation && !validateComponentState(context, componentType, observer)) {
- return;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Error occurred while informing the validation result: " + e);
- }
- AsyncTask.execute(() -> {
- try {
- observer.onNetworkStateChecked(
- INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED,
- checkNetworkStatus(context, customUrl));
- } catch (RemoteException e) {
- Log.e(TAG, "Error occurred while notifying the observer: " + e);
- }
- });
- }
- }
-
- /**
- * Checks whether the network is available by attempting a connection to the given address
- * and returns a {@link NetworkCheckResult} object containing all the relevant details for
- * debugging. Uses a default address if the given address is {@code null}.
- *
- * <p>
- * The returned object has the following fields:
- *
- * <ul>
- * <li>{@code connected}: whether or not the connection was successful.
- * <li>{@code networkInfo}: the {@link NetworkInfo} describing the current active network as
- * visible to this app.
- * <li>{@code details}: A human readable string giving useful information about the success or
- * failure.
- * </ul>
- */
- static NetworkCheckResult checkNetworkStatus(Context context, String customUrl) {
- final String address = (customUrl == null) ? DEFAULT_TEST_URL : customUrl;
-
- // The current Android DNS resolver returns an UnknownHostException whenever network access
- // is blocked. This can get cached in the current process-local InetAddress cache. Clearing
- // the cache before attempting a connection ensures we never report a failure due to a
- // negative cache entry.
- InetAddress.clearDnsCache();
-
- final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
-
- final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
- Log.d(TAG, "Running checkNetworkStatus() on thread "
- + Thread.currentThread().getName() + " for UID " + getUid(context)
- + "\n\tactiveNetworkInfo: " + networkInfo + "\n\tURL: " + address);
- boolean checkStatus = false;
- String checkDetails = "N/A";
- try {
- final URL url = new URL(address);
- final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setReadTimeout(NETWORK_TIMEOUT_MS);
- conn.setConnectTimeout(NETWORK_TIMEOUT_MS / 2);
- conn.setRequestMethod("GET");
- conn.connect();
- final int response = conn.getResponseCode();
- checkStatus = true;
- checkDetails = "HTTP response for " + address + ": " + response;
- } catch (Exception e) {
- checkStatus = false;
- checkDetails = "Exception getting " + address + ": " + e;
- }
- final NetworkCheckResult result = createNetworkCheckResult(checkStatus, checkDetails,
- networkInfo);
- Log.d(TAG, "Offering: " + result);
- return result;
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java
deleted file mode 100644
index d274c50..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyActivity.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_FINISH_ACTIVITY;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TYPE_COMPONENT_ACTIVTY;
-
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.os.RemoteCallback;
-import android.util.Log;
-import android.view.WindowManager;
-
-import androidx.annotation.GuardedBy;
-
-/**
- * Activity used to bring process to foreground.
- */
-public class MyActivity extends Activity {
-
- @GuardedBy("this")
- private BroadcastReceiver finishCommandReceiver = null;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Log.d(TAG, "MyActivity.onCreate()");
-
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- }
-
- @Override
- public void finish() {
- synchronized (this) {
- if (finishCommandReceiver != null) {
- unregisterReceiver(finishCommandReceiver);
- finishCommandReceiver = null;
- }
- }
- super.finish();
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- Log.d(TAG, "MyActivity.onStart()");
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- Log.d(TAG, "MyActivity.onNewIntent()");
- setIntent(intent);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- Log.d(TAG, "MyActivity.onResume(): " + getIntent());
- Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
- synchronized (this) {
- finishCommandReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- Log.d(TAG, "Finishing MyActivity");
- MyActivity.this.finish();
- }
- };
- registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY),
- Context.RECEIVER_EXPORTED);
- }
- final RemoteCallback callback = getIntent().getParcelableExtra(
- Intent.EXTRA_REMOTE_CALLBACK);
- if (callback != null) {
- callback.sendResult(null);
- }
- }
-
- @Override
- protected void onDestroy() {
- Log.d(TAG, "MyActivity.onDestroy()");
- super.onDestroy();
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java
deleted file mode 100644
index 27aec8c..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyBroadcastReceiver.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside.app2;
-
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_RECEIVER_READY;
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SHOW_TOAST;
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SNOOZE_WARNING;
-import static com.android.cts.netpolicy.hostside.app2.Common.MANIFEST_RECEIVER;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_BUNDLE;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_REMOTE_INPUT;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_BUNDLE;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_CONTENT;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_DELETE;
-import static com.android.cts.netpolicy.hostside.app2.Common.NOTIFICATION_TYPE_FULL_SCREEN;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-
-import android.app.Notification;
-import android.app.Notification.Action;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.RemoteInput;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.ConnectivityManager;
-import android.os.Bundle;
-import android.util.Log;
-import android.widget.Toast;
-
-/**
- * Receiver used to:
- * <ol>
- * <li>Count number of {@code RESTRICT_BACKGROUND_CHANGED} broadcasts received.
- * <li>Show a toast.
- * </ol>
- */
-public class MyBroadcastReceiver extends BroadcastReceiver {
-
- private final String mName;
-
- public MyBroadcastReceiver() {
- this(MANIFEST_RECEIVER);
- }
-
- MyBroadcastReceiver(String name) {
- Log.d(TAG, "Constructing MyBroadcastReceiver named " + name);
- mName = name;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- Log.d(TAG, "onReceive() for " + mName + ": " + intent);
- final String action = intent.getAction();
- switch (action) {
- case ACTION_SNOOZE_WARNING:
- increaseCounter(context, action);
- break;
- case ACTION_RESTRICT_BACKGROUND_CHANGED:
- increaseCounter(context, action);
- break;
- case ACTION_RECEIVER_READY:
- final String message = mName + " is ready to rumble";
- Log.d(TAG, message);
- setResultData(message);
- break;
- case ACTION_SHOW_TOAST:
- showToast(context);
- break;
- default:
- Log.e(TAG, "received unexpected action: " + action);
- }
- }
-
- @Override
- public String toString() {
- return "[MyBroadcastReceiver: mName=" + mName + "]";
- }
-
- private void increaseCounter(Context context, String action) {
- final SharedPreferences prefs = context.getApplicationContext()
- .getSharedPreferences(mName, Context.MODE_PRIVATE);
- final int value = prefs.getInt(action, 0) + 1;
- Log.d(TAG, "increaseCounter('" + action + "'): setting '" + mName + "' to " + value);
- prefs.edit().putInt(action, value).apply();
- }
-
- static int getCounter(Context context, String action, String receiverName) {
- final SharedPreferences prefs = context.getSharedPreferences(receiverName,
- Context.MODE_PRIVATE);
- final int value = prefs.getInt(action, 0);
- Log.d(TAG, "getCounter('" + action + "', '" + receiverName + "'): " + value);
- return value;
- }
-
- static String getRestrictBackgroundStatus(Context context) {
- final ConnectivityManager cm = (ConnectivityManager) context
- .getSystemService(Context.CONNECTIVITY_SERVICE);
- final int apiStatus = cm.getRestrictBackgroundStatus();
- Log.d(TAG, "getRestrictBackgroundStatus: returning " + apiStatus);
- return String.valueOf(apiStatus);
- }
-
- /**
- * Sends a system notification containing actions with pending intents to launch the app's
- * main activitiy or service.
- */
- static void sendNotification(Context context, String channelId, int notificationId,
- String notificationType ) {
- Log.d(TAG, "sendNotification: id=" + notificationId + ", type=" + notificationType);
- final Intent serviceIntent = new Intent(context, MyService.class);
- final PendingIntent pendingIntent = PendingIntent.getService(context, 0, serviceIntent,
- PendingIntent.FLAG_MUTABLE);
- final Bundle bundle = new Bundle();
- bundle.putCharSequence("parcelable", "I am not");
-
- final Notification.Builder builder = new Notification.Builder(context, channelId)
- .setSmallIcon(R.drawable.ic_notification);
-
- Action action = null;
- switch (notificationType) {
- case NOTIFICATION_TYPE_CONTENT:
- builder
- .setContentTitle("Light, Cameras...")
- .setContentIntent(pendingIntent);
- break;
- case NOTIFICATION_TYPE_DELETE:
- builder.setDeleteIntent(pendingIntent);
- break;
- case NOTIFICATION_TYPE_FULL_SCREEN:
- builder.setFullScreenIntent(pendingIntent, true);
- break;
- case NOTIFICATION_TYPE_BUNDLE:
- bundle.putParcelable("Magnum P.I. (Pending Intent)", pendingIntent);
- builder.setExtras(bundle);
- break;
- case NOTIFICATION_TYPE_ACTION:
- action = new Action.Builder(
- R.drawable.ic_notification, "ACTION", pendingIntent)
- .build();
- builder.addAction(action);
- break;
- case NOTIFICATION_TYPE_ACTION_BUNDLE:
- bundle.putParcelable("Magnum A.P.I. (Action Pending Intent)", pendingIntent);
- action = new Action.Builder(
- R.drawable.ic_notification, "ACTION WITH BUNDLE", null)
- .addExtras(bundle)
- .build();
- builder.addAction(action);
- break;
- case NOTIFICATION_TYPE_ACTION_REMOTE_INPUT:
- bundle.putParcelable("Magnum R.I. (Remote Input)", null);
- final RemoteInput remoteInput = new RemoteInput.Builder("RI")
- .addExtras(bundle)
- .build();
- action = new Action.Builder(
- R.drawable.ic_notification, "ACTION WITH REMOTE INPUT", pendingIntent)
- .addRemoteInput(remoteInput)
- .build();
- builder.addAction(action);
- break;
- default:
- Log.e(TAG, "Unknown notification type: " + notificationType);
- return;
- }
-
- final Notification notification = builder.build();
- ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
- .notify(notificationId, notification);
- }
-
- private void showToast(Context context) {
- Toast.makeText(context, "Toast from CTS test", Toast.LENGTH_SHORT).show();
- setResultData("Shown");
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java
deleted file mode 100644
index 54cee3c..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyForegroundService.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TEST_PKG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TYPE_COMPONENT_FOREGROUND_SERVICE;
-
-import android.R;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.Service;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.netpolicy.hostside.INetworkStateObserver;
-
-/**
- * Service used to change app state to FOREGROUND_SERVICE.
- */
-public class MyForegroundService extends Service {
- private static final String NOTIFICATION_CHANNEL_ID = "cts/MyForegroundService";
- private static final int FLAG_START_FOREGROUND = 1;
- private static final int FLAG_STOP_FOREGROUND = 2;
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- Log.v(TAG, "MyForegroundService.onStartCommand(): " + intent);
- NotificationManager notificationManager = getSystemService(NotificationManager.class);
- notificationManager.createNotificationChannel(new NotificationChannel(
- NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID,
- NotificationManager.IMPORTANCE_DEFAULT));
- switch (intent.getFlags()) {
- case FLAG_START_FOREGROUND:
- Log.d(TAG, "Starting foreground");
- startForeground(42, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_dialog_alert) // any icon is fine
- .build());
- Common.notifyNetworkStateObserver(this, intent, TYPE_COMPONENT_FOREGROUND_SERVICE);
- break;
- case FLAG_STOP_FOREGROUND:
- Log.d(TAG, "Stopping foreground");
- stopForeground(true);
- break;
- default:
- Log.wtf(TAG, "Invalid flag on intent " + intent);
- }
- return START_STICKY;
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java
deleted file mode 100644
index eba55ed..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyJobService.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy.hostside.app2;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_FINISH_JOB;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-import static com.android.cts.netpolicy.hostside.app2.Common.TYPE_COMPONENT_EXPEDITED_JOB;
-
-import android.app.job.JobParameters;
-import android.app.job.JobService;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.util.Log;
-
-public class MyJobService extends JobService {
-
- private BroadcastReceiver mFinishCommandReceiver = null;
-
- @Override
- public void onCreate() {
- super.onCreate();
- Log.v(TAG, "MyJobService.onCreate()");
- }
-
- @Override
- public boolean onStartJob(JobParameters params) {
- Log.v(TAG, "MyJobService.onStartJob()");
- Common.notifyNetworkStateObserver(this, params.getTransientExtras(),
- TYPE_COMPONENT_EXPEDITED_JOB);
- mFinishCommandReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- Log.v(TAG, "Finishing MyJobService");
- try {
- jobFinished(params, /*wantsReschedule=*/ false);
- } finally {
- if (mFinishCommandReceiver != null) {
- unregisterReceiver(mFinishCommandReceiver);
- mFinishCommandReceiver = null;
- }
- }
- }
- };
- registerReceiver(mFinishCommandReceiver, new IntentFilter(ACTION_FINISH_JOB),
- Context.RECEIVER_EXPORTED);
- return true;
- }
-
- @Override
- public boolean onStopJob(JobParameters params) {
- // If this job is stopped before it had a chance to send network status via
- // INetworkStateObserver, the test will fail. It could happen either due to test timing out
- // or this app moving to a lower proc_state and losing network access.
- Log.v(TAG, "MyJobService.onStopJob()");
- if (mFinishCommandReceiver != null) {
- unregisterReceiver(mFinishCommandReceiver);
- mFinishCommandReceiver = null;
- }
- return false;
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- Log.v(TAG, "MyJobService.onDestroy()");
- }
-}
diff --git a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java b/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java
deleted file mode 100644
index 71bcead..0000000
--- a/tests/cts/hostside-network-policy/app2/src/com/android/cts/netpolicy/hostside/app2/MyService.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy.hostside.app2;
-
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_RECEIVER_READY;
-import static com.android.cts.netpolicy.hostside.app2.Common.ACTION_SNOOZE_WARNING;
-import static com.android.cts.netpolicy.hostside.app2.Common.DYNAMIC_RECEIVER;
-import static com.android.cts.netpolicy.hostside.app2.Common.TAG;
-
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.Service;
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.netpolicy.hostside.IMyService;
-import com.android.cts.netpolicy.hostside.INetworkCallback;
-import com.android.cts.netpolicy.hostside.NetworkCheckResult;
-import com.android.modules.utils.build.SdkLevel;
-
-/**
- * Service used to dynamically register a broadcast receiver.
- */
-public class MyService extends Service {
- private static final String NOTIFICATION_CHANNEL_ID = "MyService";
-
- ConnectivityManager mCm;
-
- private MyBroadcastReceiver mReceiver;
- private ConnectivityManager.NetworkCallback mNetworkCallback;
-
- // TODO: move MyBroadcast static functions here - they were kept there to make git diff easier.
-
- private IMyService.Stub mBinder = new IMyService.Stub() {
- @Override
- public void registerBroadcastReceiver() {
- if (mReceiver != null) {
- Log.d(TAG, "receiver already registered: " + mReceiver);
- return;
- }
- final Context context = getApplicationContext();
- final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0;
- mReceiver = new MyBroadcastReceiver(DYNAMIC_RECEIVER);
- context.registerReceiver(mReceiver,
- new IntentFilter(ACTION_RECEIVER_READY), flags);
- context.registerReceiver(mReceiver,
- new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED), flags);
- context.registerReceiver(mReceiver,
- new IntentFilter(ACTION_SNOOZE_WARNING), flags);
- Log.d(TAG, "receiver registered");
- }
-
- @Override
- public int getCounters(String receiverName, String action) {
- return MyBroadcastReceiver.getCounter(getApplicationContext(), action, receiverName);
- }
-
- @Override
- public NetworkCheckResult checkNetworkStatus(String customUrl) {
- return Common.checkNetworkStatus(getApplicationContext(), customUrl);
- }
-
- @Override
- public String getRestrictBackgroundStatus() {
- return MyBroadcastReceiver.getRestrictBackgroundStatus(getApplicationContext());
- }
-
- @Override
- public void sendNotification(int notificationId, String notificationType) {
- MyBroadcastReceiver.sendNotification(getApplicationContext(), NOTIFICATION_CHANNEL_ID,
- notificationId, notificationType);
- }
-
- @Override
- public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb) {
- if (mNetworkCallback != null) {
- Log.d(TAG, "unregister previous network callback: " + mNetworkCallback);
- unregisterNetworkCallback();
- }
- Log.d(TAG, "registering network callback for " + request);
-
- mNetworkCallback = new ConnectivityManager.NetworkCallback() {
- @Override
- public void onBlockedStatusChanged(Network network, boolean blocked) {
- try {
- cb.onBlockedStatusChanged(network, blocked);
- } catch (RemoteException e) {
- Log.d(TAG, "Cannot send onBlockedStatusChanged: " + e);
- unregisterNetworkCallback();
- }
- }
-
- @Override
- public void onAvailable(Network network) {
- try {
- cb.onAvailable(network);
- } catch (RemoteException e) {
- Log.d(TAG, "Cannot send onAvailable: " + e);
- unregisterNetworkCallback();
- }
- }
-
- @Override
- public void onLost(Network network) {
- try {
- cb.onLost(network);
- } catch (RemoteException e) {
- Log.d(TAG, "Cannot send onLost: " + e);
- unregisterNetworkCallback();
- }
- }
-
- @Override
- public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
- try {
- cb.onCapabilitiesChanged(network, cap);
- } catch (RemoteException e) {
- Log.d(TAG, "Cannot send onCapabilitiesChanged: " + e);
- unregisterNetworkCallback();
- }
- }
- };
- mCm.registerNetworkCallback(request, mNetworkCallback);
- try {
- cb.asBinder().linkToDeath(() -> unregisterNetworkCallback(), 0);
- } catch (RemoteException e) {
- unregisterNetworkCallback();
- }
- }
-
- @Override
- public void unregisterNetworkCallback() {
- Log.d(TAG, "unregistering network callback");
- if (mNetworkCallback != null) {
- mCm.unregisterNetworkCallback(mNetworkCallback);
- mNetworkCallback = null;
- }
- }
-
- @Override
- public int scheduleJob(JobInfo jobInfo) {
- final JobScheduler jobScheduler = getApplicationContext()
- .getSystemService(JobScheduler.class);
- return jobScheduler.schedule(jobInfo);
- }
- };
-
- @Override
- public IBinder onBind(Intent intent) {
- return mBinder;
- }
-
- @Override
- public void onCreate() {
- final Context context = getApplicationContext();
- ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
- .createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID,
- NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT));
- mCm = (ConnectivityManager) getApplicationContext()
- .getSystemService(Context.CONNECTIVITY_SERVICE);
- }
-
- @Override
- public void onDestroy() {
- final Context context = getApplicationContext();
- ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
- .deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
- if (mReceiver != null) {
- Log.d(TAG, "onDestroy(): unregistering " + mReceiver);
- getApplicationContext().unregisterReceiver(mReceiver);
- }
-
- super.onDestroy();
- }
-}
diff --git a/tests/cts/hostside-network-policy/certs/Android.bp b/tests/cts/hostside-network-policy/certs/Android.bp
deleted file mode 100644
index bfbc341..0000000
--- a/tests/cts/hostside-network-policy/certs/Android.bp
+++ /dev/null
@@ -1,9 +0,0 @@
-package {
- default_team: "trendy_team_framework_backstage_power",
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_app_certificate {
- name: "cts-netpolicy-app",
- certificate: "cts-net-app",
-}
diff --git a/tests/cts/hostside-network-policy/certs/README b/tests/cts/hostside-network-policy/certs/README
deleted file mode 100644
index b660a82..0000000
--- a/tests/cts/hostside-network-policy/certs/README
+++ /dev/null
@@ -1,2 +0,0 @@
-# Generated with:
-development/tools/make_key cts-net-app '/CN=cts-net-app'
diff --git a/tests/cts/hostside-network-policy/certs/cts-net-app.pk8 b/tests/cts/hostside-network-policy/certs/cts-net-app.pk8
deleted file mode 100644
index 1703e4e..0000000
--- a/tests/cts/hostside-network-policy/certs/cts-net-app.pk8
+++ /dev/null
Binary files differ
diff --git a/tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem b/tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem
deleted file mode 100644
index a15ff48..0000000
--- a/tests/cts/hostside-network-policy/certs/cts-net-app.x509.pem
+++ /dev/null
@@ -1,19 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDAjCCAeqgAwIBAgIJAMhWwIIqr1r6MA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
-BAMMC2N0cy1uZXQtYXBwMB4XDTE4MDYyMDAyMjAwN1oXDTQ1MTEwNTAyMjAwN1ow
-FjEUMBIGA1UEAwwLY3RzLW5ldC1hcHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
-ggEKAoIBAQDefOayWQss1E+FQIONK6IhlXhe0BEyHshIrnPOOmuCPa/Svfbnmziy
-hr1KTjaQ3ET/mGShwlt6AUti7nKx9aB71IJp5mSBuwW62A8jvN3yNOo45YV8+n1o
-TrEoMWMf7hQmoOSqaSJ+VFuVms/kPSEh99okDgHCej6rsEkEcDoh6pJajQyUYDwR
-SNAF8SrqCDhqFbZW/LWedvuikCUlNtzuv7/GrcLcsiWEfHv7UOBKpMjLo9BhD1XF
-IefnxImcBQrQGMnE9TLixBiEeX5yauLgbZuxBqD/zsI2TH1FjxTeuJan83kLbqqH
-FgyvPaUjwckAdQPyom7ZUYFnBc0LQ9xzAgMBAAGjUzBRMB0GA1UdDgQWBBRZrBEw
-tAB2WNXj8dQ7ZOuJ34kY5DAfBgNVHSMEGDAWgBRZrBEwtAB2WNXj8dQ7ZOuJ34kY
-5DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDeI9AnLW6l/39y
-z96w/ldxZVFPzBRiFIsJsPHVyXlD5vUHZv/ju2jFn8TZSZR5TK0bzCEoVLp34Sho
-bbS0magP82yIvCRibyoyD+TDNnZkNJwjYnikE+/oyshTSQtpkn/rDA+0Y09BUC1E
-N2I6bV9pTXLFg7oah2FmqPRPzhgeYUKENgOQkrrjUCn6y0i/k374n7aftzdniSIz
-2kCRVEeN9gws6CnoMPx0vr32v/JVuPV6zfdJYadgj/eFRyTNE4msd9kE82Wc46eU
-YiI+LuXZ3ZMUNWGY7MK2pOUUS52JsBQ3K235dA5WaU4x8OBlY/WkNYX/eLbNs5jj
-FzLmhZZ1
------END CERTIFICATE-----
diff --git a/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp b/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp
deleted file mode 100644
index cdede36..0000000
--- a/tests/cts/hostside-network-policy/instrumentation_arguments/Android.bp
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-filegroup {
- name: "ArgumentConstants",
- srcs: ["src/**/*.java"],
-}
diff --git a/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java b/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java
deleted file mode 100644
index 0fe98e9..0000000
--- a/tests/cts/hostside-network-policy/instrumentation_arguments/src/com/android/cts/netpolicy/arguments/InstrumentationArguments.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.cts.netpolicy.arguments;
-
-public interface InstrumentationArguments {
- String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
- String ARG_CONNECTION_CHECK_CUSTOM_URL = "connection_check_custom_url";
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java
deleted file mode 100644
index 422231d..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideConnOnActivityStartTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2022 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.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import android.platform.test.annotations.FlakyTest;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
-import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
-
-import org.junit.Test;
-
-import java.util.Map;
-
-@FlakyTest(bugId = 288324467)
-public class HostsideConnOnActivityStartTest extends HostsideNetworkPolicyTestCase {
- private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
-
- @BeforeClassWithInfo
- public static void setUpOnce(TestInformation testInfo) throws Exception {
- uninstallPackage(testInfo, TEST_APP2_PKG, false);
- installPackage(testInfo, TEST_APP2_APK);
- }
-
- @AfterClassWithInfo
- public static void tearDownOnce(TestInformation testInfo) throws DeviceNotAvailableException {
- uninstallPackage(testInfo, TEST_APP2_PKG, true);
- }
-
- @Test
- public void testStartActivity_batterySaver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
- }
-
- @Test
- public void testStartActivity_dataSaver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
- }
-
- @FlakyTest(bugId = 231440256)
- @Test
- public void testStartActivity_doze() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
- }
-
- @Test
- public void testStartActivity_appStandby() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
- }
-
- // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
- @Test
- public void testStartActivity_default() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_default",
- Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
- }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java
deleted file mode 100644
index 62952bb..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideDefaultNetworkRestrictionsTests.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import android.platform.test.annotations.FlakyTest;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Map;
-
-// TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side tests.
-@FlakyTest(bugId = 288324467)
-public class HostsideDefaultNetworkRestrictionsTests extends HostsideNetworkPolicyTestCase {
- private static final String METERED_TEST_CLASS = TEST_PKG + ".DefaultRestrictionsMeteredTest";
- private static final String NON_METERED_TEST_CLASS =
- TEST_PKG + ".DefaultRestrictionsNonMeteredTest";
-
- @Before
- public void setUp() throws Exception {
- uninstallPackage(TEST_APP2_PKG, false);
- installPackage(TEST_APP2_APK);
- }
-
- @After
- public void tearDown() throws Exception {
- uninstallPackage(TEST_APP2_PKG, true);
- }
-
- private void runMeteredTest(String methodName) throws DeviceNotAvailableException {
- runDeviceTestsWithCustomOptions(TEST_PKG, METERED_TEST_CLASS, methodName,
- Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
- }
-
- private void runNonMeteredTest(String methodName) throws DeviceNotAvailableException {
- runDeviceTestsWithCustomOptions(TEST_PKG, NON_METERED_TEST_CLASS, methodName,
- Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
- }
-
- @Test
- public void testMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
- throws Exception {
- runMeteredTest("testActivityNetworkAccess");
- }
-
- @Test
- public void testMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
- throws Exception {
- runMeteredTest("testFgsNetworkAccess");
- }
-
- @Test
- public void testMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
- runMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
- }
-
- @Test
- public void testMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
- throws Exception {
- runMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
- }
-
- @Test
- public void testNonMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
- throws Exception {
- runNonMeteredTest("testActivityNetworkAccess");
- }
-
- @Test
- public void testNonMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
- throws Exception {
- runNonMeteredTest("testFgsNetworkAccess");
- }
-
- @Test
- public void testNonMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
- runNonMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
- }
-
- @Test
- public void testNonMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
- throws Exception {
- runNonMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
- }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java
deleted file mode 100644
index 2c2b118..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkCallbackTests.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2019 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.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import android.platform.test.annotations.FlakyTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Map;
-
-@FlakyTest(bugId = 288324467)
-public class HostsideNetworkCallbackTests extends HostsideNetworkPolicyTestCase {
-
- @Before
- public void setUp() throws Exception {
- uninstallPackage(TEST_APP2_PKG, false);
- installPackage(TEST_APP2_APK);
- }
-
- @After
- public void tearDown() throws Exception {
- uninstallPackage(TEST_APP2_PKG, true);
- }
-
- @Test
- public void testOnBlockedStatusChanged_dataSaver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
- }
-
- @Test
- public void testOnBlockedStatusChanged_powerSaver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
- }
-
- // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
- @Test
- public void testOnBlockedStatusChanged_default() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkCallbackTest",
- "testOnBlockedStatusChanged_default", Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
- }
-}
-
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java
deleted file mode 100644
index 8ffe360..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyManagerTests.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2021 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.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Map;
-
-public class HostsideNetworkPolicyManagerTests extends HostsideNetworkPolicyTestCase {
- @Before
- public void setUp() throws Exception {
- uninstallPackage(TEST_APP2_PKG, false);
- installPackage(TEST_APP2_APK);
- }
-
- @After
- public void tearDown() throws Exception {
- uninstallPackage(TEST_APP2_PKG, true);
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest",
- "testIsUidNetworkingBlocked_withUidNotBlocked");
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidNetworkingBlocked_withSystemUid");
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest",
- "testIsUidNetworkingBlocked_withDataSaverMode");
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest",
- "testIsUidNetworkingBlocked_withRestrictedNetworkingMode");
- }
-
- @Test
- public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest",
- "testIsUidNetworkingBlocked_withPowerSaverMode");
- }
-
- @Test
- public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG,
- TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidRestrictedOnMeteredNetworks");
- }
-
- // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
- @Test
- public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkPolicyManagerTest",
- "testIsUidNetworkingBlocked_whenInBackground",
- Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
- }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java
deleted file mode 100644
index 6de6b17..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideNetworkPolicyTestCase.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * 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.cts.netpolicy;
-
-import static com.android.cts.netpolicy.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.fail;
-
-import com.android.ddmlib.Log;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.targetprep.BuildError;
-import com.android.tradefed.targetprep.TargetSetupError;
-import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
-import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
-import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
-import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
-import com.android.tradefed.util.RunUtil;
-
-import org.junit.runner.RunWith;
-
-import java.util.Map;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-abstract class HostsideNetworkPolicyTestCase extends BaseHostJUnit4Test {
- protected static final boolean DEBUG = false;
- protected static final String TAG = "HostsideNetworkPolicyTests";
- protected static final String TEST_PKG = "com.android.cts.netpolicy.hostside";
- protected static final String TEST_APK = "CtsHostsideNetworkPolicyTestsApp.apk";
- protected static final String TEST_APP2_PKG = "com.android.cts.netpolicy.hostside.app2";
- protected static final String TEST_APP2_APK = "CtsHostsideNetworkPolicyTestsApp2.apk";
-
- @Option(name = "custom-url", importance = Option.Importance.IF_UNSET,
- description = "A custom url to use for testing network connections")
- protected String mCustomUrl;
-
- @BeforeClassWithInfo
- public static void setUpOnceBase(TestInformation testInfo) throws Exception {
- uninstallPackage(testInfo, TEST_PKG, false);
- installPackage(testInfo, TEST_APK);
- }
-
- @AfterClassWithInfo
- public static void tearDownOnceBase(TestInformation testInfo)
- throws DeviceNotAvailableException {
- uninstallPackage(testInfo, TEST_PKG, true);
- }
-
- // Custom static method to install the specified package, this is used to bypass auto-cleanup
- // per test in BaseHostJUnit4.
- protected static void installPackage(TestInformation testInfo, String apk)
- throws DeviceNotAvailableException, TargetSetupError {
- assertNotNull(testInfo);
- final int userId = testInfo.getDevice().getCurrentUser();
- final SuiteApkInstaller installer = new SuiteApkInstaller();
- // Force the apk clean up
- installer.setCleanApk(true);
- installer.addTestFileName(apk);
- installer.setUserId(userId);
- installer.setShouldGrantPermission(true);
- installer.addInstallArg("-t");
- try {
- installer.setUp(testInfo);
- } catch (BuildError e) {
- throw new TargetSetupError(
- e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor(), e.getErrorId());
- }
- }
-
- protected void installPackage(String apk) throws DeviceNotAvailableException, TargetSetupError {
- installPackage(getTestInformation(), apk);
- }
-
- protected static void uninstallPackage(TestInformation testInfo, String packageName,
- boolean shouldSucceed)
- throws DeviceNotAvailableException {
- assertNotNull(testInfo);
- final String result = testInfo.getDevice().uninstallPackage(packageName);
- if (shouldSucceed) {
- assertNull("uninstallPackage(" + packageName + ") failed: " + result, result);
- }
- }
-
- protected void uninstallPackage(String packageName,
- boolean shouldSucceed)
- throws DeviceNotAvailableException {
- uninstallPackage(getTestInformation(), packageName, shouldSucceed);
- }
-
- protected void assertPackageUninstalled(String packageName) throws DeviceNotAvailableException {
- final String command = "cmd package list packages " + packageName;
- final int max_tries = 5;
- for (int i = 1; i <= max_tries; i++) {
- final String result = runCommand(command);
- if (result.trim().isEmpty()) {
- return;
- }
- // 'list packages' filters by substring, so we need to iterate with the results
- // and check one by one, otherwise 'com.android.cts.netpolicy.hostside' could return
- // 'com.android.cts.netpolicy.hostside.app2'
- boolean found = false;
- for (String line : result.split("[\\r\\n]+")) {
- if (line.endsWith(packageName)) {
- found = true;
- break;
- }
- }
- if (!found) {
- return;
- }
- Log.v(TAG, "Package " + packageName + " not uninstalled yet (" + result
- + "); sleeping 1s before polling again");
- RunUtil.getDefault().sleep(1000);
- }
- fail("Package '" + packageName + "' not uinstalled after " + max_tries + " seconds");
- }
-
- protected int getUid(String packageName) throws DeviceNotAvailableException {
- final int currentUser = getDevice().getCurrentUser();
- final String uidLines = runCommand(
- "cmd package list packages -U --user " + currentUser + " " + packageName);
- for (String uidLine : uidLines.split("\n")) {
- if (uidLine.startsWith("package:" + packageName + " uid:")) {
- final String[] uidLineParts = uidLine.split(":");
- // 3rd entry is package uid
- return Integer.parseInt(uidLineParts[2].trim());
- }
- }
- throw new IllegalStateException("Failed to find the test app on the device; pkg="
- + packageName + ", u=" + currentUser);
- }
-
- protected boolean runDeviceTestsWithCustomOptions(String packageName, String className)
- throws DeviceNotAvailableException {
- return runDeviceTestsWithCustomOptions(packageName, className, null);
- }
-
- protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
- String methodName) throws DeviceNotAvailableException {
- return runDeviceTestsWithCustomOptions(packageName, className, methodName, null);
- }
-
- protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
- String methodName, Map<String, String> testArgs) throws DeviceNotAvailableException {
- final DeviceTestRunOptions deviceTestRunOptions = new DeviceTestRunOptions(packageName)
- .setTestClassName(className)
- .setTestMethodName(methodName);
-
- // Currently there is only one custom option that the test exposes.
- if (mCustomUrl != null) {
- deviceTestRunOptions.addInstrumentationArg(ARG_CONNECTION_CHECK_CUSTOM_URL, mCustomUrl);
- }
- // Pass over any test specific arguments.
- if (testArgs != null) {
- for (Map.Entry<String, String> arg : testArgs.entrySet()) {
- deviceTestRunOptions.addInstrumentationArg(arg.getKey(), arg.getValue());
- }
- }
- return runDeviceTests(deviceTestRunOptions);
- }
-
- protected String runCommand(String command) throws DeviceNotAvailableException {
- Log.d(TAG, "Command: '" + command + "'");
- final String output = getDevice().executeShellCommand(command);
- if (DEBUG) Log.v(TAG, "Output: " + output.trim());
- return output;
- }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java
deleted file mode 100644
index 0261c7d..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/HostsideRestrictBackgroundNetworkTests.java
+++ /dev/null
@@ -1,466 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.netpolicy;
-
-import static org.junit.Assert.fail;
-
-import android.platform.test.annotations.FlakyTest;
-import android.platform.test.annotations.SecurityTest;
-
-import com.android.ddmlib.Log;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.util.RunUtil;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@FlakyTest(bugId = 288324467)
-public class HostsideRestrictBackgroundNetworkTests extends HostsideNetworkPolicyTestCase {
-
- @Before
- public void setUp() throws Exception {
- uninstallPackage(TEST_APP2_PKG, false);
- installPackage(TEST_APP2_APK);
- }
-
- @After
- public void tearDown() throws Exception {
- uninstallPackage(TEST_APP2_PKG, true);
- }
-
- @SecurityTest
- @Test
- public void testDataWarningReceiver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataWarningReceiverTest",
- "testSnoozeWarningNotReceived");
- }
-
- /**************************
- * Data Saver Mode tests. *
- **************************/
-
- @Test
- public void testDataSaverMode_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testGetRestrictBackgroundStatus_disabled");
- }
-
- @Test
- public void testDataSaverMode_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testGetRestrictBackgroundStatus_whitelisted");
- }
-
- @Test
- public void testDataSaverMode_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testGetRestrictBackgroundStatus_enabled");
- }
-
- @Test
- public void testDataSaverMode_blacklisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testGetRestrictBackgroundStatus_blacklisted");
- }
-
- @Test
- public void testDataSaverMode_reinstall() throws Exception {
- final int oldUid = getUid(TEST_APP2_PKG);
-
- // Make sure whitelist is revoked when package is removed
- addRestrictBackgroundWhitelist(oldUid);
-
- uninstallPackage(TEST_APP2_PKG, true);
- assertPackageUninstalled(TEST_APP2_PKG);
- assertRestrictBackgroundWhitelist(oldUid, false);
-
- installPackage(TEST_APP2_APK);
- final int newUid = getUid(TEST_APP2_PKG);
- assertRestrictBackgroundWhitelist(oldUid, false);
- assertRestrictBackgroundWhitelist(newUid, false);
- }
-
- @Test
- public void testDataSaverMode_requiredWhitelistedPackages() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testGetRestrictBackgroundStatus_requiredWhitelistedPackages");
- }
-
- @Test
- public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
- "testBroadcastNotSentOnUnsupportedDevices");
- }
-
- /*****************************
- * Battery Saver Mode tests. *
- *****************************/
-
- @Test
- public void testBatterySaverModeMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
- @Test
- public void testBatterySaverModeMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testBatterySaverModeMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- @Test
- public void testBatterySaverMode_reinstall() throws Exception {
- if (!isDozeModeEnabled()) {
- Log.w(TAG, "testBatterySaverMode_reinstall() skipped because device does not support "
- + "Doze Mode");
- return;
- }
-
- addPowerSaveModeWhitelist(TEST_APP2_PKG);
-
- uninstallPackage(TEST_APP2_PKG, true);
- assertPackageUninstalled(TEST_APP2_PKG);
- assertPowerSaveModeWhitelist(TEST_APP2_PKG, false);
-
- installPackage(TEST_APP2_APK);
- assertPowerSaveModeWhitelist(TEST_APP2_PKG, false);
- }
-
- @Test
- public void testBatterySaverModeNonMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
- @Test
- public void testBatterySaverModeNonMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testBatterySaverModeNonMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- /*******************
- * App idle tests. *
- *******************/
-
- @Test
- public void testAppIdleMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
- @Test
- public void testAppIdleMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testAppIdleMetered_tempWhitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testBackgroundNetworkAccess_tempWhitelisted");
- }
-
- @Test
- public void testAppIdleMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- @Test
- public void testAppIdleMetered_idleWhitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testAppIdleNetworkAccess_idleWhitelisted");
- }
-
- // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
- // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
- // public void testAppIdle_reinstall() throws Exception {
- // }
-
- @Test
- public void testAppIdleNonMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
-
- @Test
- public void testAppIdleNonMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testAppIdleNonMetered_tempWhitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testBackgroundNetworkAccess_tempWhitelisted");
- }
-
- @Test
- public void testAppIdleNonMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- @Test
- public void testAppIdleNonMetered_idleWhitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testAppIdleNetworkAccess_idleWhitelisted");
- }
-
- @Test
- public void testAppIdleNonMetered_whenCharging() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testAppIdleNetworkAccess_whenCharging");
- }
-
- @Test
- public void testAppIdleMetered_whenCharging() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
- "testAppIdleNetworkAccess_whenCharging");
- }
-
- @Test
- public void testAppIdle_toast() throws Exception {
- // Check that showing a toast doesn't bring an app out of standby
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
- "testAppIdle_toast");
- }
-
- /********************
- * Doze Mode tests. *
- ********************/
-
- @Test
- public void testDozeModeMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
- @Test
- public void testDozeModeMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testDozeModeMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- @Test
- public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
- "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
- }
-
- // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
- // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
- // public void testDozeMode_reinstall() throws Exception {
- // }
-
- @Test
- public void testDozeModeNonMetered_disabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
- "testBackgroundNetworkAccess_disabled");
- }
-
- @Test
- public void testDozeModeNonMetered_whitelisted() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
- "testBackgroundNetworkAccess_whitelisted");
- }
-
- @Test
- public void testDozeModeNonMetered_enabled() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
- "testBackgroundNetworkAccess_enabled");
- }
-
- @Test
- public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
- throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
- "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
- }
-
- /**********************
- * Mixed modes tests. *
- **********************/
-
- @Test
- public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testDataAndBatterySaverModes_meteredNetwork");
- }
-
- @Test
- public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testDataAndBatterySaverModes_nonMeteredNetwork");
- }
-
- @Test
- public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testDozeAndBatterySaverMode_powerSaveWhitelists");
- }
-
- @Test
- public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testDozeAndAppIdle_powerSaveWhitelists");
- }
-
- @Test
- public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testAppIdleAndDoze_tempPowerSaveWhitelists");
- }
-
- @Test
- public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testAppIdleAndBatterySaver_tempPowerSaveWhitelists");
- }
-
- @Test
- public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testDozeAndAppIdle_appIdleWhitelist");
- }
-
- @Test
- public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists");
- }
-
- @Test
- public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
- "testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
- }
-
- /**************************
- * Restricted mode tests. *
- **************************/
-
- @Test
- public void testNetworkAccess_restrictedMode() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
- "testNetworkAccess");
- }
-
- @Test
- public void testNetworkAccess_restrictedMode_withBatterySaver() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
- "testNetworkAccess_withBatterySaver");
- }
-
- /************************
- * Expedited job tests. *
- ************************/
-
- @Test
- public void testMeteredNetworkAccess_expeditedJob() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
- }
-
- @Test
- public void testNonMeteredNetworkAccess_expeditedJob() throws Exception {
- runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobNonMeteredTest");
- }
-
- /*******************
- * Helper methods. *
- *******************/
-
- private void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
- final int max_tries = 5;
- boolean actual = false;
- for (int i = 1; i <= max_tries; i++) {
- final String output = runCommand("cmd netpolicy list restrict-background-whitelist ");
- actual = output.contains(Integer.toString(uid));
- if (expected == actual) {
- return;
- }
- Log.v(TAG, "whitelist check for uid " + uid + " doesn't match yet (expected "
- + expected + ", got " + actual + "); sleeping 1s before polling again");
- RunUtil.getDefault().sleep(1000);
- }
- fail("whitelist check for uid " + uid + " failed: expected "
- + expected + ", got " + actual);
- }
-
- private void assertPowerSaveModeWhitelist(String packageName, boolean expected)
- throws Exception {
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- assertDelayedCommand("dumpsys deviceidle whitelist =" + packageName,
- Boolean.toString(expected));
- }
-
- /**
- * Asserts the result of a command, wait and re-running it a couple times if necessary.
- */
- private void assertDelayedCommand(String command, String expectedResult)
- throws InterruptedException, DeviceNotAvailableException {
- final int maxTries = 5;
- for (int i = 1; i <= maxTries; i++) {
- final String result = runCommand(command).trim();
- if (result.equals(expectedResult)) return;
- Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
- + expectedResult + "' on attempt #; sleeping 1s before polling again");
- RunUtil.getDefault().sleep(1000);
- }
- fail("Command '" + command + "' did not return '" + expectedResult + "' after " + maxTries
- + " attempts");
- }
-
- protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
- runCommand("cmd netpolicy add restrict-background-whitelist " + uid);
- assertRestrictBackgroundWhitelist(uid, true);
- }
-
- private void addPowerSaveModeWhitelist(String packageName) throws Exception {
- Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist");
- // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
- // need to use netpolicy for whitelisting
- runCommand("dumpsys deviceidle whitelist +" + packageName);
- assertPowerSaveModeWhitelist(packageName, true);
- }
-
- protected boolean isDozeModeEnabled() throws Exception {
- final String result = runCommand("cmd deviceidle enabled deep").trim();
- return result.equals("1");
- }
-}
diff --git a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java b/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java
deleted file mode 100644
index cbf2f4d..0000000
--- a/tests/cts/hostside-network-policy/src/com/android/cts/netpolicy/NetworkPolicyTestsPreparer.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2020 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.cts.netpolicy;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.log.LogUtil;
-import com.android.tradefed.targetprep.ITargetPreparer;
-
-public class NetworkPolicyTestsPreparer implements ITargetPreparer {
- private ITestDevice mDevice;
- private boolean mOriginalAirplaneModeEnabled;
- private String mOriginalAppStandbyEnabled;
- private String mOriginalBatteryStatsConstants;
- private final static String KEY_STABLE_CHARGING_DELAY_MS = "battery_charged_delay_ms";
- private final static int DESIRED_STABLE_CHARGING_DELAY_MS = 0;
-
- @Override
- public void setUp(TestInformation testInformation) throws DeviceNotAvailableException {
- mDevice = testInformation.getDevice();
- mOriginalAppStandbyEnabled = getAppStandbyEnabled();
- setAppStandbyEnabled("1");
- LogUtil.CLog.d("Original app_standby_enabled: " + mOriginalAppStandbyEnabled);
-
- mOriginalBatteryStatsConstants = getBatteryStatsConstants();
- setBatteryStatsConstants(
- KEY_STABLE_CHARGING_DELAY_MS + "=" + DESIRED_STABLE_CHARGING_DELAY_MS);
- LogUtil.CLog.d("Original battery_saver_constants: " + mOriginalBatteryStatsConstants);
-
- mOriginalAirplaneModeEnabled = getAirplaneModeEnabled();
- // Turn off airplane mode in case another test left the device in that state.
- setAirplaneModeEnabled(false);
- LogUtil.CLog.d("Original airplane mode state: " + mOriginalAirplaneModeEnabled);
- }
-
- @Override
- public void tearDown(TestInformation testInformation, Throwable e)
- throws DeviceNotAvailableException {
- setAirplaneModeEnabled(mOriginalAirplaneModeEnabled);
- setAppStandbyEnabled(mOriginalAppStandbyEnabled);
- setBatteryStatsConstants(mOriginalBatteryStatsConstants);
- }
-
- private void setAirplaneModeEnabled(boolean enable) throws DeviceNotAvailableException {
- executeCmd("cmd connectivity airplane-mode " + (enable ? "enable" : "disable"));
- }
-
- private boolean getAirplaneModeEnabled() throws DeviceNotAvailableException {
- return "enabled".equals(executeCmd("cmd connectivity airplane-mode").trim());
- }
-
- private void setAppStandbyEnabled(String appStandbyEnabled) throws DeviceNotAvailableException {
- if ("null".equals(appStandbyEnabled)) {
- executeCmd("settings delete global app_standby_enabled");
- } else {
- executeCmd("settings put global app_standby_enabled " + appStandbyEnabled);
- }
- }
-
- private String getAppStandbyEnabled() throws DeviceNotAvailableException {
- return executeCmd("settings get global app_standby_enabled").trim();
- }
-
- private void setBatteryStatsConstants(String batteryStatsConstants)
- throws DeviceNotAvailableException {
- executeCmd("settings put global battery_stats_constants \"" + batteryStatsConstants + "\"");
- }
-
- private String getBatteryStatsConstants() throws DeviceNotAvailableException {
- return executeCmd("settings get global battery_stats_constants");
- }
-
- private String executeCmd(String cmd) throws DeviceNotAvailableException {
- final String output = mDevice.executeShellCommand(cmd).trim();
- LogUtil.CLog.d("Output for '%s': %s", cmd, output);
- return output;
- }
-}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index 14d5d54..0ac9ce1 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -12,17 +12,27 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-next_app_data = [":CtsHostsideNetworkTestsAppNext"]
-
-// The above line is put in place to prevent any future automerger merge conflict between aosp,
-// downstream branches. The CtsHostsideNetworkTestsAppNext target will not exist in
-// some downstream branches, but it should exist in aosp and some downstream branches.
-
package {
default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
+java_defaults {
+ name: "CtsHostsideNetworkTestsAllAppDefaults",
+ platform_apis: true,
+ min_sdk_version: "30",
+ // Set target SDK to 10000 so that all the test helper apps are always subject to the most
+ // recent (and possibly most restrictive) target SDK behaviour. Also, this matches the target
+ // SDK of the tests themselves, and of other tests such as CtsNetTestCases.
+ // Note that some of the test helper apps (e.g., CtsHostsideNetworkCapTestsAppSdk33) override
+ // this with older SDK versions.
+ // Also note that unlike android_test targets, "current" does not work: the target SDK is set to
+ // something like "VanillaIceCream" instead of 100000. This means that the tests will not run on
+ // released devices with errors such as "Requires development platform VanillaIceCream but this
+ // is a release platform".
+ target_sdk_version: "10000",
+}
+
java_test_host {
name: "CtsHostsideNetworkTests",
defaults: ["cts_defaults"],
@@ -46,12 +56,12 @@
"mts-tethering",
"sts",
],
- data: [
+ device_common_data: [
":CtsHostsideNetworkTestsApp",
":CtsHostsideNetworkTestsApp2",
":CtsHostsideNetworkCapTestsAppWithoutProperty",
":CtsHostsideNetworkCapTestsAppWithProperty",
":CtsHostsideNetworkCapTestsAppSdk33",
- ] + next_app_data,
+ ],
per_testcase_directory: true,
}
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index ea6b078..03ea178 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -20,6 +20,9 @@
<option name="config-descriptor:metadata" key="parameter" value="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="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
+ <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk" />
+ <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.tethering.apex" />
<target_preparer class="com.android.compatibility.common.tradefed.targetprep.LocationCheck" />
@@ -46,4 +49,21 @@
<option name="directory-keys" value="/sdcard/CtsHostsideNetworkTests" />
<option name="collect-on-run-ended-only" value="true" />
</metrics_collector>
+
+<!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
+ one of the Mainline modules below is present on the device used for testing. -->
+<object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <!-- Tethering Module (internal version). -->
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ <!-- Tethering Module (AOSP version). -->
+ <option name="mainline-module-package-name" value="com.android.tethering" />
+ <!-- NetworkStack Module (internal version). Should always be installed with CaptivePortalLogin. -->
+ <option name="mainline-module-package-name" value="com.google.android.networkstack" />
+ <!-- NetworkStack Module (AOSP version). Should always be installed with CaptivePortalLogin. -->
+ <option name="mainline-module-package-name" value="com.android.networkstack" />
+ <!-- Resolver Module (internal version). -->
+ <option name="mainline-module-package-name" value="com.google.android.resolv" />
+ <!-- Resolver Module (AOSP version). -->
+ <option name="mainline-module-package-name" value="com.android.resolv" />
+</object>
</configuration>
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 33761dc..ca3e77f 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -19,7 +19,7 @@
java_test_helper_library {
name: "CtsHostsideNetworkTestsAidl",
- sdk_version: "current",
+ sdk_version: "system_current",
srcs: [
"com/android/cts/net/hostside/*.aidl",
"com/android/cts/net/hostside/*.java",
diff --git a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/ITetheringHelper.aidl
similarity index 74%
rename from tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
rename to tests/cts/hostside/aidl/com/android/cts/net/hostside/ITetheringHelper.aidl
index 7aac2ab..a9f5ed4 100644
--- a/tests/cts/hostside-network-policy/aidl/com/android/cts/netpolicy/hostside/NetworkCheckResult.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/ITetheringHelper.aidl
@@ -14,13 +14,10 @@
* limitations under the License.
*/
-package com.android.cts.netpolicy.hostside;
+package com.android.cts.net.hostside;
-import android.net.NetworkInfo;
+import android.net.TetheringInterface;
-@JavaDerive(toString=true)
-parcelable NetworkCheckResult {
- boolean connected;
- String details;
- NetworkInfo networkInfo;
-}
\ No newline at end of file
+interface ITetheringHelper {
+ TetheringInterface getTetheredWifiInterface();
+}
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 919e025..7fff1c2 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -19,9 +19,13 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-java_defaults {
- name: "CtsHostsideNetworkTestsAppDefaults",
- platform_apis: true,
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsApp",
+ defaults: [
+ "cts_support_defaults",
+ "framework-connectivity-test-defaults",
+ "CtsHostsideNetworkTestsAllAppDefaults",
+ ],
static_libs: [
"CtsHostsideNetworkTestsAidl",
"androidx.test.ext.junit",
@@ -33,40 +37,10 @@
"modules-utils-build",
],
libs: [
- "android.test.runner",
- "android.test.base",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
],
srcs: [
"src/**/*.java",
],
- // Tag this module as a cts test artifact
- test_suites: [
- "general-tests",
- "sts",
- ],
-}
-
-android_test_helper_app {
- name: "CtsHostsideNetworkTestsApp",
- defaults: [
- "cts_support_defaults",
- "framework-connectivity-test-defaults",
- "CtsHostsideNetworkTestsAppDefaults",
- ],
- static_libs: [
- "NetworkStackApiStableShims",
- ],
-}
-
-android_test_helper_app {
- name: "CtsHostsideNetworkTestsAppNext",
- defaults: [
- "cts_support_defaults",
- "framework-connectivity-test-defaults",
- "CtsHostsideNetworkTestsAppDefaults",
- "ConnectivityNextEnableDefaults",
- ],
- static_libs: [
- "NetworkStackApiCurrentShims",
- ],
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
index fe522a0..a39a8d0 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
@@ -29,9 +29,6 @@
import android.util.Pair;
import com.android.modules.utils.build.SdkLevel;
-import com.android.networkstack.apishim.VpnServiceBuilderShimImpl;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-import com.android.networkstack.apishim.common.VpnServiceBuilderShim;
import com.android.testutils.PacketReflector;
import java.io.IOException;
@@ -102,8 +99,7 @@
}
private void start(String packageName, Intent intent) {
- Builder builder = new Builder();
- VpnServiceBuilderShim vpnServiceBuilderShim = VpnServiceBuilderShimImpl.newInstance();
+ VpnService.Builder builder = new VpnService.Builder();
final String addresses = parseIpAndMaskListArgument(packageName, intent, "addresses",
builder::addAddress);
@@ -112,11 +108,7 @@
if (SdkLevel.isAtLeastT() && intent.getBooleanExtra(packageName + ".addRoutesByIpPrefix",
false)) {
addedRoutes = parseIpPrefixListArgument(packageName, intent, "routes", (prefix) -> {
- try {
- vpnServiceBuilderShim.addRoute(builder, prefix);
- } catch (UnsupportedApiLevelException e) {
- throw new RuntimeException(e);
- }
+ builder.addRoute(prefix);
});
} else {
addedRoutes = parseIpAndMaskListArgument(packageName, intent, "routes",
@@ -127,11 +119,7 @@
if (SdkLevel.isAtLeastT()) {
excludedRoutes = parseIpPrefixListArgument(packageName, intent, "excludedRoutes",
(prefix) -> {
- try {
- vpnServiceBuilderShim.excludeRoute(builder, prefix);
- } catch (UnsupportedApiLevelException e) {
- throw new RuntimeException(e);
- }
+ builder.excludeRoute(prefix);
});
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringHelperClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringHelperClient.java
new file mode 100644
index 0000000..5f5ebb0
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringHelperClient.java
@@ -0,0 +1,88 @@
+/*
+ * 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.cts.net.hostside;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.TetheringInterface;
+import android.os.ConditionVariable;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+public class TetheringHelperClient {
+ private static final int TIMEOUT_MS = 5000;
+ private static final String PACKAGE = TetheringHelperClient.class.getPackage().getName();
+ private static final String APP2_PACKAGE = PACKAGE + ".app2";
+ private static final String SERVICE_NAME = APP2_PACKAGE + ".TetheringHelperService";
+
+ private Context mContext;
+ private ServiceConnection mServiceConnection;
+ private ITetheringHelper mService;
+
+ public TetheringHelperClient(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Binds to TetheringHelperService.
+ */
+ public void bind() {
+ if (mService != null) {
+ throw new IllegalStateException("Already bound");
+ }
+
+ final ConditionVariable cv = new ConditionVariable();
+ mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = ITetheringHelper.Stub.asInterface(service);
+ cv.open();
+ }
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ }
+ };
+
+ final Intent intent = new Intent();
+ intent.setComponent(new ComponentName(APP2_PACKAGE, SERVICE_NAME));
+ mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ cv.block(TIMEOUT_MS);
+ if (mService == null) {
+ throw new IllegalStateException(
+ "Could not bind to TetheringHelperService after " + TIMEOUT_MS + "ms");
+ }
+ }
+
+ /**
+ * Unbinds from TetheringHelperService.
+ */
+ public void unbind() {
+ if (mService != null) {
+ mContext.unbindService(mServiceConnection);
+ }
+ }
+
+ /**
+ * Returns the tethered Wifi interface as seen from TetheringHelperService.
+ */
+ public TetheringInterface getTetheredWifiInterface() throws RemoteException {
+ return mService.getTetheredWifiInterface();
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
new file mode 100644
index 0000000..ad98a29
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.cts.net.hostside;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.content.Context;
+import android.net.TetheringInterface;
+import android.net.cts.util.CtsTetheringUtils;
+import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.WifiSsid;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.charset.StandardCharsets;
+
+public class TetheringTest {
+ private CtsTetheringUtils mCtsTetheringUtils;
+ private TetheringHelperClient mTetheringHelperClient;
+
+ @Before
+ public void setUp() throws Exception {
+ Context targetContext = getInstrumentation().getTargetContext();
+ mCtsTetheringUtils = new CtsTetheringUtils(targetContext);
+ mTetheringHelperClient = new TetheringHelperClient(targetContext);
+ mTetheringHelperClient.bind();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mTetheringHelperClient.unbind();
+ }
+
+ /**
+ * Starts Wifi tethering and tests that the SoftApConfiguration is redacted from
+ * TetheringEventCallback for other apps.
+ */
+ @Test
+ public void testSoftApConfigurationRedactedForOtherUids() throws Exception {
+ final CtsTetheringUtils.TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+ SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes("This is an SSID!"
+ .getBytes(StandardCharsets.UTF_8))).build();
+ final TetheringInterface tetheringInterface =
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
+ assertNotNull(tetheringInterface);
+ assertEquals(softApConfig, tetheringInterface.getSoftApConfiguration());
+ try {
+ TetheringInterface tetheringInterfaceForApp2 =
+ mTetheringHelperClient.getTetheredWifiInterface();
+ assertNotNull(tetheringInterfaceForApp2);
+ assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
+ assertEquals(
+ tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
+ } finally {
+ mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
+ }
+ }
+}
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 e186c6b..3430196 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
@@ -20,8 +20,12 @@
import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
+import static android.content.Context.RECEIVER_EXPORTED;
import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.TYPE_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
@@ -46,11 +50,7 @@
import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_RESUMED;
import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_STARTED;
import static com.android.cts.net.hostside.VpnTest.TestSocketKeepaliveCallback.CallbackType.ON_STOPPED;
-import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
-import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
-import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
import static com.android.testutils.Cleanup.testAndCleanup;
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.RecorderCallback.CallbackEntry.BLOCKED_STATUS_INT;
import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -213,6 +213,8 @@
private static final String AUTOMATIC_ON_OFF_KEEPALIVE_VERSION =
"automatic_on_off_keepalive_version";
+ private static final String INGRESS_TO_VPN_ADDRESS_FILTERING =
+ "ingress_to_vpn_address_filtering";
// Enabled since version 1 means it's always enabled because the version is always above 1
private static final String AUTOMATIC_ON_OFF_KEEPALIVE_ENABLED = "1";
private static final long TEST_TCP_POLLING_TIMER_EXPIRED_PERIOD_MS = 60_000L;
@@ -890,7 +892,7 @@
entry -> entry.getCaps().hasTransport(TRANSPORT_VPN));
}
- @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+ @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testChangeUnderlyingNetworks() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
@@ -995,6 +997,13 @@
FIREWALL_CHAIN_BACKGROUND));
otherUidCallback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
true /* validated */, isOtherUidBlocked, TIMEOUT_MS);
+ } else {
+ // R does not have per-UID callback or system default callback APIs, and sends an
+ // additional CAP_CHANGED callback.
+ registerDefaultNetworkCallback(myUidCallback);
+ myUidCallback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
+ true /* validated */, false /* blocked */, TIMEOUT_MS);
+ myUidCallback.expect(CallbackEntry.NETWORK_CAPS_UPDATED, defaultNetwork);
}
FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
@@ -1136,12 +1145,12 @@
return null;
}
- @Test
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Automatic keepalives were added in U.
public void testAutomaticOnOffKeepaliveModeNoClose() throws Exception {
doTestAutomaticOnOffKeepaliveMode(false);
}
- @Test
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Automatic keepalives were added in U.
public void testAutomaticOnOffKeepaliveModeClose() throws Exception {
doTestAutomaticOnOffKeepaliveMode(true);
}
@@ -1201,7 +1210,7 @@
AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
AUTOMATIC_ON_OFF_KEEPALIVE_ENABLED, false /* makeDefault */);
return mode;
- }, READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG);
+ }, READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG);
final IpSecManager ipSec = mTargetContext.getSystemService(IpSecManager.class);
SocketKeepalive kp = null;
@@ -1241,7 +1250,7 @@
AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
origMode, false);
mCM.setTestLowTcpPollingTimerForKeepalive(0);
- }, WRITE_DEVICE_CONFIG, NETWORK_SETTINGS);
+ }, WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG, NETWORK_SETTINGS);
}
}
@@ -1707,7 +1716,8 @@
}
private void maybeExpectVpnTransportInfo(Network network) {
- assumeTrue(SdkLevel.isAtLeastS());
+ // VpnTransportInfo was only added in S.
+ if (!SdkLevel.isAtLeastS()) return;
final NetworkCapabilities vpnNc = mCM.getNetworkCapabilities(network);
assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
final TransportInfo ti = vpnNc.getTransportInfo();
@@ -1949,6 +1959,9 @@
*/
private void doTestDropPacketToVpnAddress(final boolean duplicatedAddress)
throws Exception {
+ assumeTrue(mCM.isConnectivityServiceFeatureEnabledForTesting(
+ INGRESS_TO_VPN_ADDRESS_FILTERING));
+
final NetworkRequest request = new NetworkRequest.Builder()
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index 12ea23b..05abcdd 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -21,19 +21,14 @@
android_test_helper_app {
name: "CtsHostsideNetworkTestsApp2",
- defaults: ["cts_support_defaults"],
- platform_apis: true,
+ defaults: [
+ "cts_support_defaults",
+ "CtsHostsideNetworkTestsAllAppDefaults",
+ ],
static_libs: [
"androidx.annotation_annotation",
"CtsHostsideNetworkTestsAidl",
- "NetworkStackApiStableShims",
],
srcs: ["src/**/*.java"],
- // Tag this module as a cts test artifact
- test_suites: [
- "cts",
- "general-tests",
- "sts",
- ],
sdk_version: "test_current",
}
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
index 412b307..6ccaf4f 100644
--- a/tests/cts/hostside/app2/AndroidManifest.xml
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -42,6 +42,8 @@
android:debuggable="true">
<service android:name=".RemoteSocketFactoryService"
android:exported="true"/>
+ <service android:name=".TetheringHelperService"
+ android:exported="true"/>
</application>
<!--
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/TetheringHelperService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/TetheringHelperService.java
new file mode 100644
index 0000000..56a8cbb
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/TetheringHelperService.java
@@ -0,0 +1,78 @@
+/*
+ * 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.cts.net.hostside.app2;
+
+
+import static android.net.TetheringManager.TETHERING_WIFI;
+
+import android.app.Service;
+import android.content.Intent;
+import android.net.TetheringInterface;
+import android.net.TetheringManager;
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+
+import com.android.cts.net.hostside.ITetheringHelper;
+
+import java.util.Set;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class TetheringHelperService extends Service {
+ private static final String TAG = TetheringHelperService.class.getSimpleName();
+
+ private ITetheringHelper.Stub mBinder = new ITetheringHelper.Stub() {
+ public TetheringInterface getTetheredWifiInterface() {
+ ArrayBlockingQueue<TetheringInterface> queue = new ArrayBlockingQueue<>(1);
+ TetheringManager.TetheringEventCallback callback =
+ new TetheringManager.TetheringEventCallback() {
+ @Override
+ public void onTetheredInterfacesChanged(
+ @NonNull Set<TetheringInterface> interfaces) {
+ for (TetheringInterface iface : interfaces) {
+ if (iface.getType() == TETHERING_WIFI) {
+ queue.offer(iface);
+ break;
+ }
+ }
+ }
+ };
+ TetheringManager tm =
+ TetheringHelperService.this.getSystemService(TetheringManager.class);
+ tm.registerTetheringEventCallback(Runnable::run, callback);
+ TetheringInterface iface;
+ try {
+ iface = queue.poll(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new IllegalStateException("Wait for wifi TetheredInterface interrupted");
+ } finally {
+ tm.unregisterTetheringEventCallback(callback);
+ }
+ if (iface == null) {
+ throw new IllegalStateException(
+ "No wifi TetheredInterface received after 5 seconds");
+ }
+ return iface;
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+}
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index c220000..0eed51c 100644
--- a/tests/cts/hostside/networkslicingtestapp/Android.bp
+++ b/tests/cts/hostside/networkslicingtestapp/Android.bp
@@ -21,7 +21,6 @@
java_defaults {
name: "CtsHostsideNetworkCapTestsAppDefaults",
- platform_apis: true,
static_libs: [
"androidx.test.ext.junit",
"androidx.test.rules",
@@ -29,12 +28,6 @@
"cts-net-utils",
],
srcs: ["src/**/*.java"],
- // Tag this module as a cts test artifact
- test_suites: [
- "cts",
- "general-tests",
- "sts",
- ],
}
android_test_helper_app {
@@ -42,6 +35,7 @@
defaults: [
"cts_support_defaults",
"CtsHostsideNetworkCapTestsAppDefaults",
+ "CtsHostsideNetworkTestsAllAppDefaults",
],
manifest: "AndroidManifestWithoutProperty.xml",
sdk_version: "test_current",
@@ -52,6 +46,7 @@
defaults: [
"cts_support_defaults",
"CtsHostsideNetworkCapTestsAppDefaults",
+ "CtsHostsideNetworkTestsAllAppDefaults",
],
manifest: "AndroidManifestWithProperty.xml",
sdk_version: "test_current",
@@ -62,6 +57,7 @@
defaults: [
"cts_support_defaults",
"CtsHostsideNetworkCapTestsAppDefaults",
+ "CtsHostsideNetworkTestsAllAppDefaults",
],
target_sdk_version: "33",
manifest: "AndroidManifestWithoutProperty.xml",
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index 69d61b3..e222ff6 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -19,7 +19,6 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
-import com.android.modules.utils.build.testing.DeviceSdkLevel;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.targetprep.BuildError;
@@ -36,17 +35,13 @@
abstract class HostsideNetworkTestCase extends BaseHostJUnit4Test {
protected static final String TEST_PKG = "com.android.cts.net.hostside";
protected static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk";
- protected static final String TEST_APK_NEXT = "CtsHostsideNetworkTestsAppNext.apk";
protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
@BeforeClassWithInfo
public static void setUpOnceBase(TestInformation testInfo) throws Exception {
- DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(testInfo.getDevice());
- String testApk = deviceSdkLevel.isDeviceAtLeastV() ? TEST_APK_NEXT : TEST_APK;
-
uninstallPackage(testInfo, TEST_PKG, false);
- installPackage(testInfo, testApk);
+ installPackage(testInfo, TEST_APK);
}
@AfterClassWithInfo
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideTetheringTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideTetheringTests.java
new file mode 100644
index 0000000..d73e01a
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideTetheringTests.java
@@ -0,0 +1,53 @@
+/*
+ * 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.cts.net;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
+import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class HostsideTetheringTests extends HostsideNetworkTestCase {
+ /**
+ * Set up the test once before running all the tests.
+ */
+ @BeforeClassWithInfo
+ public static void setUpOnce(TestInformation testInfo) throws Exception {
+ uninstallPackage(testInfo, TEST_APP2_PKG, false);
+ installPackage(testInfo, TEST_APP2_APK);
+ }
+
+ /**
+ * Tear down the test once after running all the tests.
+ */
+ @AfterClassWithInfo
+ public static void tearDownOnce(TestInformation testInfo)
+ throws DeviceNotAvailableException {
+ uninstallPackage(testInfo, TEST_APP2_PKG, true);
+ }
+
+ @Test
+ public void testSoftApConfigurationRedactedForOtherApps() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".TetheringTest",
+ "testSoftApConfigurationRedactedForOtherUids");
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
index fa68e3e..ae572e6 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -166,6 +166,8 @@
*/
@Test
public void testRouterSolicitations() throws Exception {
+ assumeTrue(new DeviceSdkLevel(mDevice).isDeviceAtLeastU());
+
for (String interfaceDir : mSysctlDirs) {
String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitations";
int value = readIntFromPath(path);
@@ -186,8 +188,7 @@
*/
@Test
public void testCongestionControl() throws Exception {
- final DeviceSdkLevel dsl = new DeviceSdkLevel(mDevice);
- assumeTrue(dsl.isDeviceAtLeastV());
+ assumeTrue(new DeviceSdkLevel(mDevice).isDeviceAtLeastV());
String path = "/proc/sys/net/ipv4/tcp_congestion_control";
String value = mDevice.executeAdbCommand("shell", "cat", path).trim();
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index dc90adb..949be85 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -19,11 +19,14 @@
python_test_host {
name: "CtsConnectivityMultiDevicesTestCases",
- main: "connectivity_multi_devices_test.py",
+ main: "run_tests.py",
srcs: [
+ "apfv4_test.py",
"connectivity_multi_devices_test.py",
+ "run_tests.py",
],
libs: [
+ "absl-py",
"mobly",
"net-tests-utils-host-python-common",
],
@@ -34,7 +37,7 @@
test_options: {
unit_test: false,
},
- data: [
+ device_common_data: [
// Package the snippet with the mobly test
":connectivity_multi_devices_snippet",
],
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
new file mode 100644
index 0000000..7795be5
--- /dev/null
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -0,0 +1,64 @@
+# 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.
+
+from absl.testing import parameterized
+from mobly import asserts
+from net_tests_utils.host.python import apf_test_base, apf_utils
+
+# Constants.
+COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED = "DROPPED_ETHERTYPE_NOT_ALLOWED"
+ETHER_BROADCAST_ADDR = "FFFFFFFFFFFF"
+
+
+class ApfV4Test(apf_test_base.ApfTestBase, parameterized.TestCase):
+ def setup_class(self):
+ super().setup_class()
+ # Check apf version preconditions.
+ caps = apf_utils.get_apf_capabilities(
+ self.clientDevice, self.client_iface_name
+ )
+ if self.client.getVsrApiLevel() >= 34:
+ # Enforce APFv4 support for Android 14+ VSR.
+ asserts.assert_true(
+ caps.apf_version_supported >= 4,
+ "APFv4 became mandatory in Android 14 VSR.",
+ )
+ else:
+ # Skip tests for APF version < 4 before Android 14 VSR.
+ apf_utils.assume_apf_version_support_at_least(
+ self.clientDevice, self.client_iface_name, 4
+ )
+
+ # APF L2 packet filtering on V+ Android allows only specific
+ # types: IPv4, ARP, IPv6, EAPOL, WAPI.
+ # Tests can use any disallowed packet type. Currently,
+ # several ethertypes from the legacy ApfFilter denylist are used.
+ @parameterized.parameters(
+ "88a2", # ATA over Ethernet
+ "88a4", # EtherCAT
+ "88b8", # GOOSE (Generic Object Oriented Substation event)
+ "88cd", # SERCOS III
+ "88e3", # Media Redundancy Protocol (IEC62439-2)
+ ) # Declare inputs for state_str and expected_result.
+ def test_apf_drop_ethertype_not_allowed(self, blocked_ether_type):
+ # Ethernet header (14 bytes).
+ packet = ETHER_BROADCAST_ADDR # Destination MAC (broadcast)
+ packet += self.server_mac_address.replace(":", "") # Source MAC
+ packet += blocked_ether_type
+
+ # Pad with zeroes to minimum ethernet frame length.
+ packet += "00" * 46
+ self.send_packet_and_expect_counter_increased(
+ packet, COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED
+ )
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index cdb684a..416a2e8 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -1,38 +1,25 @@
-# Lint as: python3
-"""Connectivity multi devices tests."""
-import sys
+# 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.
-from mobly import base_test
-from mobly import test_runner
-from mobly import utils
-from mobly.controllers import android_device
-from net_tests_utils.host.python import adb_utils, apf_utils, assert_utils, tether_utils
+from net_tests_utils.host.python import mdns_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python import wifip2p_utils
from net_tests_utils.host.python.tether_utils import UpstreamType
-CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
-
-class ConnectivityMultiDevicesTest(base_test.BaseTestClass):
-
- def setup_class(self):
- # Declare that two Android devices are needed.
- self.clientDevice, self.serverDevice = self.register_controller(
- android_device, min_number=2
- )
-
- def setup_device(device):
- device.load_snippet(
- "connectivity_multi_devices_snippet",
- CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
- )
-
- # Set up devices in parallel to save time.
- utils.concurrent_exec(
- setup_device,
- ((self.clientDevice,), (self.serverDevice,)),
- max_workers=2,
- raise_on_exception=True,
- )
+class ConnectivityMultiDevicesTest(
+ multi_devices_test_base.MultiDevicesTestBase
+):
def test_hotspot_upstream_wifi(self):
tether_utils.assume_hotspot_test_preconditions(
@@ -62,9 +49,41 @@
self.serverDevice, UpstreamType.CELLULAR
)
-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()
+ def test_mdns_via_hotspot(self):
+ tether_utils.assume_hotspot_test_preconditions(
+ self.serverDevice, self.clientDevice, UpstreamType.NONE
+ )
+ mdns_utils.assume_mdns_test_preconditions(
+ self.clientDevice, self.serverDevice
+ )
+ try:
+ # Connectivity of the client verified by asserting the validated capability.
+ tether_utils.setup_hotspot_and_client_for_upstream_type(
+ self.serverDevice, self.clientDevice, UpstreamType.NONE
+ )
+ mdns_utils.register_mdns_service_and_discover_resolve(
+ self.clientDevice, self.serverDevice
+ )
+ finally:
+ mdns_utils.cleanup_mdns_service(self.clientDevice, self.serverDevice)
+ tether_utils.cleanup_tethering_for_upstream_type(
+ self.serverDevice, UpstreamType.NONE
+ )
+
+ def test_mdns_via_wifip2p(self):
+ wifip2p_utils.assume_wifi_p2p_test_preconditions(
+ self.serverDevice, self.clientDevice
+ )
+ mdns_utils.assume_mdns_test_preconditions(
+ self.clientDevice, self.serverDevice
+ )
+ try:
+ wifip2p_utils.setup_wifi_p2p_server_and_client(
+ self.serverDevice, self.clientDevice
+ )
+ mdns_utils.register_mdns_service_and_discover_resolve(
+ self.clientDevice, self.serverDevice
+ )
+ finally:
+ mdns_utils.cleanup_mdns_service(self.clientDevice, self.serverDevice)
+ wifip2p_utils.cleanup_wifi_p2p(self.serverDevice, self.clientDevice)
diff --git a/tests/cts/multidevices/run_tests.py b/tests/cts/multidevices/run_tests.py
new file mode 100644
index 0000000..1391d13
--- /dev/null
+++ b/tests/cts/multidevices/run_tests.py
@@ -0,0 +1,38 @@
+# 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.
+
+"""Main entrypoint for all of test cases."""
+
+import sys
+from apfv4_test import ApfV4Test
+from connectivity_multi_devices_test import ConnectivityMultiDevicesTest
+from mobly import suite_runner
+
+
+if __name__ == "__main__":
+ # For MoblyBinaryHostTest, this entry point will be called twice:
+ # 1. List tests.
+ # <mobly-par-file-name> -- --list_tests
+ # 2. Run tests.
+ # <mobly-par-file-name> -- --config=<yaml-path> \
+ # --device_serial=<device-serial> --log_path=<log-path>
+ # Strip the "--" since suite runner doesn't recognize it.
+ # While the parameters before "--" is for the infrastructure,
+ # ignore them if any. Also, do not alter parameters if there
+ # is no "--", in case the binary is invoked manually.
+ if "--" in sys.argv:
+ index = sys.argv.index("--")
+ sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
+ # TODO: make the tests can be executed without manually list classes.
+ suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test], sys.argv)
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 7368669..49688cc 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -36,6 +36,7 @@
import android.net.wifi.WifiNetworkSpecifier
import android.net.wifi.WifiSsid
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.PropertyUtil
import com.android.modules.utils.build.SdkLevel
import com.android.testutils.AutoReleaseNetworkCallbackRule
import com.android.testutils.ConnectUtil
@@ -75,6 +76,12 @@
@Rpc(description = "Check whether the device SDK is as least T")
fun isAtLeastT() = SdkLevel.isAtLeastT()
+ @Rpc(description = "Return whether the Sdk level is at least V.")
+ fun isAtLeastV() = SdkLevel.isAtLeastV()
+
+ @Rpc(description = "Return the API level that the VSR requirement must be fulfilled.")
+ fun getVsrApiLevel() = PropertyUtil.getVsrApiLevel()
+
@Rpc(description = "Request cellular connection and ensure it is the default network.")
fun requestCellularAndEnsureDefault() {
ctsNetUtils.disableWifi()
diff --git a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
index e0929bb..f8c9351 100644
--- a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
@@ -16,13 +16,28 @@
package com.google.snippet.connectivity
+import android.Manifest.permission.MANAGE_WIFI_NETWORK_SELECTION
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.MacAddress
import android.net.wifi.WifiManager
+import android.net.wifi.p2p.WifiP2pConfig
+import android.net.wifi.p2p.WifiP2pDevice
+import android.net.wifi.p2p.WifiP2pDeviceList
+import android.net.wifi.p2p.WifiP2pGroup
import android.net.wifi.p2p.WifiP2pManager
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.runAsShell
import com.google.android.mobly.snippet.Snippet
import com.google.android.mobly.snippet.rpc.Rpc
+import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.ConnectionChanged
+import com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet.Wifip2pIntentReceiver.IntentReceivedEvent.PeersChanged
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
+import kotlin.test.assertNotNull
import kotlin.test.fail
private const val TIMEOUT_MS = 60000L
@@ -38,6 +53,35 @@
?: fail("Could not get WifiP2pManager service")
}
private lateinit var wifip2pChannel: WifiP2pManager.Channel
+ private val wifip2pIntentReceiver = Wifip2pIntentReceiver()
+
+ private class Wifip2pIntentReceiver : BroadcastReceiver() {
+ val history = ArrayTrackRecord<IntentReceivedEvent>().newReadHead()
+
+ sealed class IntentReceivedEvent {
+ abstract val intent: Intent
+ data class ConnectionChanged(override val intent: Intent) : IntentReceivedEvent()
+ data class PeersChanged(override val intent: Intent) : IntentReceivedEvent()
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
+ history.add(ConnectionChanged(intent))
+ }
+ WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
+ history.add(PeersChanged(intent))
+ }
+ }
+ }
+
+ inline fun <reified T : IntentReceivedEvent> eventuallyExpectedIntent(
+ timeoutMs: Long = TIMEOUT_MS,
+ crossinline predicate: (T) -> Boolean = { true }
+ ): T = history.poll(timeoutMs) { it is T && predicate(it) }.also {
+ assertNotNull(it, "Intent ${T::class} not received within ${timeoutMs}ms.")
+ } as T
+ }
@Rpc(description = "Check whether the device supports Wi-Fi P2P.")
fun isP2pSupported() = wifiManager.isP2pSupported()
@@ -55,6 +99,10 @@
}
}
p2pStateEnabledFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ // Register an intent filter to receive Wi-Fi P2P intents
+ val filter = IntentFilter(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
+ filter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
+ context.registerReceiver(wifip2pIntentReceiver, filter)
}
@Rpc(description = "Stop Wi-Fi P2P")
@@ -63,5 +111,202 @@
wifip2pManager.cancelConnect(wifip2pChannel, null)
wifip2pManager.removeGroup(wifip2pChannel, null)
}
+ // Unregister the intent filter
+ context.unregisterReceiver(wifip2pIntentReceiver)
+ }
+
+ @Rpc(description = "Get the current device name")
+ fun getDeviceName(): String {
+ // Retrieve current device info
+ val deviceFuture = CompletableFuture<String>()
+ wifip2pManager.requestDeviceInfo(wifip2pChannel) { wifiP2pDevice ->
+ if (wifiP2pDevice != null) {
+ deviceFuture.complete(wifiP2pDevice.deviceName)
+ }
+ }
+ // Return current device name
+ return deviceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ @Rpc(description = "Wait for a p2p connection changed intent and check the group")
+ @Suppress("DEPRECATION")
+ fun waitForP2pConnectionChanged(ignoreGroupCheck: Boolean, groupName: String) {
+ wifip2pIntentReceiver.eventuallyExpectedIntent<ConnectionChanged>() {
+ val p2pGroup: WifiP2pGroup? =
+ it.intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)
+ val groupMatched = p2pGroup?.networkName == groupName
+ return@eventuallyExpectedIntent ignoreGroupCheck || groupMatched
+ }
+ }
+
+ @Rpc(description = "Create a Wi-Fi P2P group")
+ fun createGroup(groupName: String, groupPassphrase: String) {
+ // Create a Wi-Fi P2P group
+ val wifip2pConfig = WifiP2pConfig.Builder()
+ .setNetworkName(groupName)
+ .setPassphrase(groupPassphrase)
+ .build()
+ val createGroupFuture = CompletableFuture<Boolean>()
+ wifip2pManager.createGroup(
+ wifip2pChannel,
+ wifip2pConfig,
+ object : WifiP2pManager.ActionListener {
+ override fun onFailure(reason: Int) = Unit
+ override fun onSuccess() { createGroupFuture.complete(true) }
+ }
+ )
+ createGroupFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+ // Ensure the Wi-Fi P2P group is created.
+ waitForP2pConnectionChanged(false, groupName)
+ }
+
+ @Rpc(description = "Start Wi-Fi P2P peers discovery")
+ fun startPeersDiscovery() {
+ // Start discovery Wi-Fi P2P peers
+ wifip2pManager.discoverPeers(wifip2pChannel, null)
+
+ // Ensure the discovery is started
+ val p2pDiscoveryStartedFuture = CompletableFuture<Boolean>()
+ wifip2pManager.requestDiscoveryState(wifip2pChannel) { state ->
+ if (state == WifiP2pManager.WIFI_P2P_DISCOVERY_STARTED) {
+ p2pDiscoveryStartedFuture.complete(true)
+ }
+ }
+ p2pDiscoveryStartedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ /**
+ * Get the device address from the given intent that matches the given device name.
+ *
+ * @param peersChangedIntent the intent to get the device address from
+ * @param deviceName the target device name
+ * @return the address of the target device or null if no devices match.
+ */
+ @Suppress("DEPRECATION")
+ private fun getDeviceAddress(peersChangedIntent: Intent, deviceName: String): String? {
+ val peers: WifiP2pDeviceList? =
+ peersChangedIntent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST)
+ return peers?.deviceList?.firstOrNull { it.deviceName == deviceName }?.deviceAddress
+ }
+
+ /**
+ * Ensure the given device has been discovered and returns the associated device address for
+ * connection.
+ *
+ * @param deviceName the target device name
+ * @return the address of the target device.
+ */
+ @Rpc(description = "Ensure the target Wi-Fi P2P device is discovered")
+ fun ensureDeviceDiscovered(deviceName: String): String {
+ val changedEvent = wifip2pIntentReceiver.eventuallyExpectedIntent<PeersChanged>() {
+ return@eventuallyExpectedIntent getDeviceAddress(it.intent, deviceName) != null
+ }
+ return getDeviceAddress(changedEvent.intent, deviceName)
+ ?: fail("Missing device in filtered intent")
+ }
+
+ @Rpc(description = "Invite a Wi-Fi P2P device to the group")
+ fun inviteDeviceToGroup(groupName: String, groupPassphrase: String, deviceAddress: String) {
+ // Connect to the device to send invitation
+ val wifip2pConfig = WifiP2pConfig.Builder()
+ .setNetworkName(groupName)
+ .setPassphrase(groupPassphrase)
+ .setDeviceAddress(MacAddress.fromString(deviceAddress))
+ .build()
+ val connectedFuture = CompletableFuture<Boolean>()
+ wifip2pManager.connect(
+ wifip2pChannel,
+ wifip2pConfig,
+ object : WifiP2pManager.ActionListener {
+ override fun onFailure(reason: Int) = Unit
+ override fun onSuccess() {
+ connectedFuture.complete(true)
+ }
+ }
+ )
+ connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ private fun runExternalApproverForGroupProcess(
+ deviceAddress: String,
+ isGroupInvitation: Boolean
+ ) {
+ val peer = MacAddress.fromString(deviceAddress)
+ runAsShell(MANAGE_WIFI_NETWORK_SELECTION) {
+ val connectionRequestFuture = CompletableFuture<Boolean>()
+ val attachedFuture = CompletableFuture<Boolean>()
+ wifip2pManager.addExternalApprover(
+ wifip2pChannel,
+ peer,
+ object : WifiP2pManager.ExternalApproverRequestListener {
+ override fun onAttached(deviceAddress: MacAddress) {
+ attachedFuture.complete(true)
+ }
+ override fun onDetached(deviceAddress: MacAddress, reason: Int) = Unit
+ override fun onConnectionRequested(
+ requestType: Int,
+ config: WifiP2pConfig,
+ device: WifiP2pDevice
+ ) {
+ connectionRequestFuture.complete(true)
+ }
+ override fun onPinGenerated(deviceAddress: MacAddress, pin: String) = Unit
+ }
+ )
+ if (isGroupInvitation) attachedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) else
+ connectionRequestFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+ val resultFuture = CompletableFuture<Boolean>()
+ wifip2pManager.setConnectionRequestResult(
+ wifip2pChannel,
+ peer,
+ WifiP2pManager.CONNECTION_REQUEST_ACCEPT,
+ object : WifiP2pManager.ActionListener {
+ override fun onFailure(reason: Int) = Unit
+ override fun onSuccess() {
+ resultFuture.complete(true)
+ }
+ }
+ )
+ resultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+
+ val removeFuture = CompletableFuture<Boolean>()
+ wifip2pManager.removeExternalApprover(
+ wifip2pChannel,
+ peer,
+ object : WifiP2pManager.ActionListener {
+ override fun onFailure(reason: Int) = Unit
+ override fun onSuccess() {
+ removeFuture.complete(true)
+ }
+ }
+ )
+ removeFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+ }
+
+ @Rpc(description = "Accept P2P group invitation from device")
+ fun acceptGroupInvitation(deviceAddress: String) {
+ // Accept the Wi-Fi P2P group invitation
+ runExternalApproverForGroupProcess(deviceAddress, true /* isGroupInvitation */)
+ }
+
+ @Rpc(description = "Wait for connection request from the peer and accept joining")
+ fun waitForPeerConnectionRequestAndAcceptJoining(deviceAddress: String) {
+ // Wait for connection request from the peer and accept joining
+ runExternalApproverForGroupProcess(deviceAddress, false /* isGroupInvitation */)
+ }
+
+ @Rpc(description = "Ensure the target device is connected")
+ fun ensureDeviceConnected(deviceName: String) {
+ // Retrieve peers and ensure the target device is connected
+ val connectedFuture = CompletableFuture<Boolean>()
+ wifip2pManager.requestPeers(wifip2pChannel) { peers -> peers?.deviceList?.any {
+ it.deviceName == deviceName && it.status == WifiP2pDevice.CONNECTED }.let {
+ connectedFuture.complete(true)
+ }
+ }
+ connectedFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
}
}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 6e2f849..a9ac29c 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -29,7 +29,7 @@
libs: [
"voip-common",
- "android.test.base",
+ "android.test.base.stubs",
],
jni_libs: [
@@ -70,6 +70,14 @@
":ConnectivityTestPreparer",
":CtsCarrierServicePackage",
],
+ errorprone: {
+ enabled: true,
+ // Error-prone checking only warns of problems when building. To make the build fail with
+ // these errors, list the specific error-prone problems below.
+ javacflags: [
+ "-Xep:NullablePrimitive:ERROR",
+ ],
+ },
}
// Networking CTS tests for development and release. These tests always target the platform SDK
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index a424b90..55b6494 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -26,6 +26,7 @@
<option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.tethering.apex" />
<option name="not-shardable" value="true" />
<target_preparer class="com.android.compatibility.common.tradefed.targetprep.DynamicConfigPusher">
+ <option name="has-server-side-config" value="false" />
<option name="target" value="device" />
<option name="config-filename" value="{MODULE}" />
<option name="version" value="1.0" />
@@ -56,7 +57,13 @@
the runner will only run the tests annotated with that annotation, but if it does not,
the runner will run all the tests. -->
<option name="include-annotation" value="com.android.testutils.filters.{MODULE}" />
+ <option name="device-listeners" value="com.android.testutils.ConnectivityDiagnosticsCollector" />
</test>
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <!-- Pattern matching the fileKey used by ConnectivityDiagnosticsCollector when calling addFileMetric -->
+ <option name="pull-pattern-keys" value="com.android.testutils.ConnectivityDiagnosticsCollector.*"/>
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
<!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
one of the Mainline modules below is present on the device used for testing. -->
<object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 587d5a5..7d93c3a 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -25,7 +25,7 @@
compile_multilib: "both",
libs: [
- "android.test.base",
+ "android.test.base.stubs.test",
],
srcs: [
diff --git a/tests/cts/net/jni/NativeMultinetworkJni.cpp b/tests/cts/net/jni/NativeMultinetworkJni.cpp
index f2214a3..1d848ec 100644
--- a/tests/cts/net/jni/NativeMultinetworkJni.cpp
+++ b/tests/cts/net/jni/NativeMultinetworkJni.cpp
@@ -415,9 +415,17 @@
strlcpy(dst, buf, size);
}
+static jobject create_query_test_result(JNIEnv* env, uint16_t src_port, int attempts, int errnum) {
+ jclass clazz = env->FindClass(
+ "android/net/cts/MultinetworkApiTest$QueryTestResult");
+ jmethodID ctor = env->GetMethodID(clazz, "<init>", "(III)V");
+
+ return env->NewObject(clazz, ctor, src_port, attempts, errnum);
+}
+
extern "C"
-JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runDatagramCheck(
- JNIEnv*, jclass, jlong nethandle) {
+JNIEXPORT jobject Java_android_net_cts_MultinetworkApiTest_runDatagramCheck(
+ JNIEnv* env, jclass, jlong nethandle, jint src_port) {
const struct addrinfo kHints = {
.ai_flags = AI_ADDRCONFIG,
.ai_family = AF_UNSPEC,
@@ -433,7 +441,7 @@
LOGD("android_getaddrinfofornetwork(%llu, %s) returned rval=%d errno=%d",
handle, kHostname, rval, errno);
freeaddrinfo(res);
- return -errno;
+ return create_query_test_result(env, 0, 0, errno);
}
// Rely upon getaddrinfo sorting the best destination to the front.
@@ -442,7 +450,7 @@
LOGD("socket(%d, %d, %d) failed, errno=%d",
res->ai_family, res->ai_socktype, res->ai_protocol, errno);
freeaddrinfo(res);
- return -errno;
+ return create_query_test_result(env, 0, 0, errno);
}
rval = android_setsocknetwork(handle, fd);
@@ -451,7 +459,31 @@
if (rval != 0) {
close(fd);
freeaddrinfo(res);
- return -errno;
+ return create_query_test_result(env, 0, 0, errno);
+ }
+
+ sockaddr_storage src_addr;
+ socklen_t src_addrlen = sizeof(src_addr);
+ if (src_port) {
+ if (res->ai_family == AF_INET6) {
+ *reinterpret_cast<sockaddr_in6*>(&src_addr) = (sockaddr_in6) {
+ .sin6_family = AF_INET6,
+ .sin6_port = htons(src_port),
+ .sin6_addr = in6addr_any,
+ };
+ } else {
+ *reinterpret_cast<sockaddr_in*>(&src_addr) = (sockaddr_in) {
+ .sin_family = AF_INET,
+ .sin_port = htons(src_port),
+ .sin_addr = { .s_addr = INADDR_ANY },
+ };
+ }
+ if (bind(fd, (sockaddr *)&src_addr, src_addrlen) != 0) {
+ LOGD("Error binding to port %d", src_port);
+ close(fd);
+ freeaddrinfo(res);
+ return create_query_test_result(env, 0, 0, errno);
+ }
}
char addrstr[kSockaddrStrLen+1];
@@ -462,19 +494,28 @@
if (rval != 0) {
close(fd);
freeaddrinfo(res);
- return -errno;
+ return create_query_test_result(env, 0, 0, errno);
}
freeaddrinfo(res);
- struct sockaddr_storage src_addr;
- socklen_t src_addrlen = sizeof(src_addr);
if (getsockname(fd, (struct sockaddr *)&src_addr, &src_addrlen) != 0) {
close(fd);
- return -errno;
+ return create_query_test_result(env, 0, 0, errno);
}
sockaddr_ntop((const struct sockaddr *)&src_addr, sizeof(src_addr), addrstr, sizeof(addrstr));
LOGD("... from %s", addrstr);
+ uint16_t socket_src_port;
+ if (res->ai_family == AF_INET6) {
+ socket_src_port = ntohs(reinterpret_cast<sockaddr_in6*>(&src_addr)->sin6_port);
+ } else if (src_addr.ss_family == AF_INET) {
+ socket_src_port = ntohs(reinterpret_cast<sockaddr_in*>(&src_addr)->sin_port);
+ } else {
+ LOGD("Invalid source address family %d", src_addr.ss_family);
+ close(fd);
+ return create_query_test_result(env, 0, 0, EAFNOSUPPORT);
+ }
+
// Don't let reads or writes block indefinitely.
const struct timeval timeo = { 2, 0 }; // 2 seconds
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo));
@@ -503,7 +544,7 @@
errnum = errno;
LOGD("send(QUIC packet) returned sent=%zd, errno=%d", sent, errnum);
close(fd);
- return -errnum;
+ return create_query_test_result(env, socket_src_port, i + 1, errnum);
}
rcvd = recv(fd, response, sizeof(response), 0);
@@ -521,18 +562,19 @@
LOGD("Does this network block UDP port %s?", kPort);
}
close(fd);
- return -EPROTO;
+ return create_query_test_result(env, socket_src_port, i + 1,
+ rcvd <= 0 ? errnum : EPROTO);
}
int conn_id_cmp = memcmp(quic_packet + 6, response + 7, 8);
if (conn_id_cmp != 0) {
LOGD("sent and received connection IDs do not match");
close(fd);
- return -EPROTO;
+ return create_query_test_result(env, socket_src_port, i + 1, EPROTO);
}
// TODO: Replace this quick 'n' dirty test with proper QUIC-capable code.
close(fd);
- return 0;
+ return create_query_test_result(env, socket_src_port, i + 1, 0);
}
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index dea2848..320622b 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -19,6 +19,7 @@
package android.net.cts
+import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
import android.Manifest.permission.WRITE_DEVICE_CONFIG
import android.content.pm.PackageManager
import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
@@ -189,7 +190,7 @@
// created.
// APF adb cmds are only implemented in ApfFilter.java. Enable experiment to prevent
// LegacyApfFilter.java from being used.
- runAsShell(WRITE_DEVICE_CONFIG) {
+ runAsShell(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG) {
DeviceConfig.setProperty(
NAMESPACE_CONNECTIVITY,
APF_NEW_RA_FILTER_VERSION,
@@ -434,6 +435,12 @@
}
}
+ private fun installAndVerifyProgram(program: ByteArray) {
+ installProgram(program)
+ val readResult = readProgram().take(program.size).toByteArray()
+ assertThat(readResult).isEqualTo(program)
+ }
+
fun ApfV4GeneratorBase<*>.addPassIfNotIcmpv6EchoReply() {
// If not IPv6 -> PASS
addLoad16(R0, ETH_ETHERTYPE_OFFSET)
@@ -491,8 +498,7 @@
gen.addJump(BaseApfGenerator.DROP_LABEL)
val program = gen.generate()
- installProgram(program)
- readProgram() // wait for install completion
+ installAndVerifyProgram(program)
packetReader.sendPing(data, payloadSize)
packetReader.expectPingDropped()
@@ -537,8 +543,7 @@
val program = gen.generate()
assertThat(program.size).isLessThan(counterRegion)
- installProgram(program)
- readProgram() // wait for install completion
+ installAndVerifyProgram(program)
// Trigger the program by sending a ping and waiting on the reply.
val payloadSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
@@ -586,8 +591,7 @@
gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS)
gen.addStoreData(R0, 0)
- installProgram(gen.generate())
- readProgram() // wait for install completion
+ installAndVerifyProgram(gen.generate())
val payloadSize = 56
val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
@@ -627,8 +631,7 @@
gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_16384THS)
gen.addStoreCounter(FILTER_AGE_16384THS, R0)
- installProgram(gen.generate())
- readProgram() // wait for install completion
+ installAndVerifyProgram(gen.generate())
val payloadSize = 56
val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
@@ -736,8 +739,7 @@
.addPass()
.generate()
- installProgram(program)
- readProgram() // Ensure installation is complete
+ installAndVerifyProgram(program)
packetReader.sendPing(payload, payloadSize)
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 07e2024..1389be7 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -44,7 +44,6 @@
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.AndroidJUnit4
-import com.android.modules.utils.build.SdkLevel.isAtLeastR
import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL
import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL
import com.android.testutils.AutoReleaseNetworkCallbackRule
@@ -201,10 +200,7 @@
"access."
assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
- val startPortalAppPermission =
- if (isAtLeastR()) NETWORK_SETTINGS
- else CONNECTIVITY_INTERNAL
- runAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
+ runAsShell(NETWORK_SETTINGS) { cm.startCaptivePortalApp(network) }
// Expect the portal content to be fetched at some point after detecting the portal.
// Some implementations may fetch the URL before startCaptivePortalApp is called.
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 3338712..feb4621 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -113,6 +113,7 @@
import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+import static com.android.testutils.MiscAsserts.assertEventuallyTrue;
import static com.android.testutils.MiscAsserts.assertThrows;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -144,6 +145,7 @@
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.ConnectivitySettingsManager;
+import android.net.DnsResolver;
import android.net.InetAddresses;
import android.net.IpSecManager;
import android.net.IpSecManager.UdpEncapsulationSocket;
@@ -195,12 +197,12 @@
import androidx.test.filters.RequiresDevice;
import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.DynamicConfigDeviceSide;
import com.android.internal.util.ArrayUtils;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsPacket;
import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
import com.android.networkstack.apishim.ConstantsShim;
import com.android.networkstack.apishim.NetworkInformationShimImpl;
@@ -212,6 +214,7 @@
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.DeviceConfigRule;
import com.android.testutils.DeviceInfoUtils;
import com.android.testutils.DumpTestUtils;
@@ -248,7 +251,6 @@
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
-import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
@@ -275,7 +277,8 @@
import fi.iki.elonen.NanoHTTPD.Response.IStatus;
import fi.iki.elonen.NanoHTTPD.Response.Status;
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
public class ConnectivityManagerTest {
@Rule(order = 1)
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
@@ -304,6 +307,7 @@
private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 30_000;
private static final int NO_CALLBACK_TIMEOUT_MS = 100;
private static final int NETWORK_REQUEST_TIMEOUT_MS = 3000;
+ private static final int DNS_REQUEST_TIMEOUT_MS = 1000;
private static final int SOCKET_TIMEOUT_MS = 100;
private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
@@ -859,6 +863,55 @@
});
}
+ @NonNull
+ private static String getDeviceIpv6AddressThroughDnsQuery(Network network) throws Exception {
+ final InetAddress dnsAddr = getAddrByName("ns1.google.com", AF_INET6);
+ assertNotNull("IPv6 address for ns1.google.com should not be null", dnsAddr);
+
+ try (DatagramSocket udpSocket = new DatagramSocket()) {
+ network.bindSocket(udpSocket);
+
+ final DnsPacket queryDnsPkt = new DnsPacket(
+ new DnsPacket.DnsHeader(new Random().nextInt(), DnsResolver.FLAG_EMPTY,
+ 1 /* qdcount */,
+ 0 /* ancount */),
+ List.of(DnsPacket.DnsRecord.makeQuestion("o-o.myaddr.l.google.com",
+ DnsResolver.TYPE_TXT, DnsResolver.CLASS_IN)),
+ List.of() /* an */
+ );
+ final byte[] queryDnsRawBytes = queryDnsPkt.getBytes();
+ final byte[] receiveBuffer = new byte[1500];
+ final int maxRetry = 3;
+ for (int attempt = 1; attempt <= maxRetry; ++attempt) {
+ try {
+ final DatagramPacket queryUdpPkt = new DatagramPacket(queryDnsRawBytes,
+ queryDnsRawBytes.length, dnsAddr, 53 /* port */);
+ udpSocket.send(queryUdpPkt);
+
+ final DatagramPacket replyUdpPkt = new DatagramPacket(receiveBuffer,
+ receiveBuffer.length);
+ udpSocket.setSoTimeout(DNS_REQUEST_TIMEOUT_MS);
+ udpSocket.receive(replyUdpPkt);
+ break;
+ } catch (IOException e) {
+ if (attempt == maxRetry) {
+ throw e; // If the last attempt fails, rethrow the exception.
+ } else {
+ Log.e(TAG, "DNS request failed (attempt " + attempt + ")" + e);
+ }
+ }
+ }
+
+ final DnsPacket replyDnsPkt = new DnsPacket(receiveBuffer);
+ final DnsPacket.DnsRecord answerRecord = replyDnsPkt.getRecords(
+ DnsPacket.ANSECTION).get(0);
+ final byte[] txtReplyRecord = answerRecord.getRR();
+ final byte dataLength = txtReplyRecord[0];
+ assertEquals(dataLength, txtReplyRecord.length - 1);
+ return new String(Arrays.copyOfRange(txtReplyRecord, 1, txtReplyRecord.length));
+ }
+ }
+
/**
* Tests that connections can be opened on WiFi and cellphone networks,
* and that they are made from different IP addresses.
@@ -884,8 +937,30 @@
// Verify that the IP addresses that the requests appeared to come from are actually on the
// respective networks.
- assertOnNetwork(wifiAddressString, wifiNetwork);
- assertOnNetwork(cellAddressString, cellNetwork);
+ final InetAddress wifiAddress = InetAddresses.parseNumericAddress(wifiAddressString);
+ final LinkProperties wifiLinkProperties = mCm.getLinkProperties(wifiNetwork);
+ // To make sure that the request went out on the right network, check that
+ // the IP address seen by the server is assigned to the expected network.
+ // We can only do this for IPv6 addresses, because in IPv4 we will likely
+ // have a private IPv4 address, and that won't match what the server sees.
+ if (wifiAddress instanceof Inet6Address) {
+ assertContains(wifiLinkProperties.getAddresses(), wifiAddress);
+ }
+
+ final LinkProperties cellLinkProperties = mCm.getLinkProperties(cellNetwork);
+ final InetAddress cellAddress = InetAddresses.parseNumericAddress(cellAddressString);
+ final List<InetAddress> cellNetworkAddresses = cellLinkProperties.getAddresses();
+ // In userdebug build, on cellular network, if the onNetwork check failed, we also try to
+ // re-verify it by obtaining the IP address through DNS query.
+ if (cellAddress instanceof Inet6Address) {
+ if (DeviceInfoUtils.isDebuggable() && !cellNetworkAddresses.contains(cellAddress)) {
+ final InetAddress ipv6AddressThroughDns = InetAddresses.parseNumericAddress(
+ getDeviceIpv6AddressThroughDnsQuery(cellNetwork));
+ assertContains(cellNetworkAddresses, ipv6AddressThroughDns);
+ } else {
+ assertContains(cellNetworkAddresses, cellAddress);
+ }
+ }
assertFalse("Unexpectedly equal: " + wifiNetwork, wifiNetwork.equals(cellNetwork));
}
@@ -917,17 +992,6 @@
}
}
- private void assertOnNetwork(String adressString, Network network) throws UnknownHostException {
- InetAddress address = InetAddress.getByName(adressString);
- LinkProperties linkProperties = mCm.getLinkProperties(network);
- // To make sure that the request went out on the right network, check that
- // the IP address seen by the server is assigned to the expected network.
- // We can only do this for IPv6 addresses, because in IPv4 we will likely
- // have a private IPv4 address, and that won't match what the server sees.
- if (address instanceof Inet6Address) {
- assertContains(linkProperties.getAddresses(), address);
- }
- }
private static<T> void assertContains(Collection<T> collection, T element) {
assertTrue(element + " not found in " + collection, collection.contains(element));
@@ -1054,7 +1118,7 @@
@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
@Test @IgnoreUpTo(Build.VERSION_CODES.Q)
- public void testIsPrivateDnsBroken() throws InterruptedException {
+ public void testIsPrivateDnsBroken() throws Exception {
final String invalidPrivateDnsServer = "invalidhostname.example.com";
final String goodPrivateDnsServer = "dns.google";
mCtsNetUtils.storePrivateDnsSetting();
@@ -1076,11 +1140,9 @@
.isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
} finally {
mCtsNetUtils.restorePrivateDnsSetting();
- // Toggle network to make sure it is re-validated
- mCm.reportNetworkConnectivity(networkForPrivateDns, true);
- cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
- entry -> !(((CallbackEntry.CapabilitiesChanged) entry).getCaps()
- .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
+ // Toggle networks to make sure they are re-validated
+ mCtsNetUtils.reconnectWifiIfSupported();
+ mCtsNetUtils.reconnectCellIfSupported();
}
}
@@ -1713,7 +1775,8 @@
}
}
- private InetAddress getAddrByName(final String hostname, final int family) throws Exception {
+ private static InetAddress getAddrByName(final String hostname, final int family)
+ throws Exception {
final InetAddress[] allAddrs = InetAddress.getAllByName(hostname);
for (InetAddress addr : allAddrs) {
if (family == AF_INET && addr instanceof Inet4Address) return addr;
@@ -2935,12 +2998,7 @@
mCm.getActiveNetwork(), false /* accept */ , false /* always */));
}
- private void ensureCellIsValidatedBeforeMockingValidationUrls() {
- // Verify that current supported network is validated so that the mock http server will not
- // apply to unexpected networks. Also see aosp/2208680.
- //
- // This may also apply to wifi in principle, but in practice methods that mock validation
- // URL all disconnect wifi forcefully anyway, so don't wait for wifi to validate.
+ private void ensureCellIsValidated() {
if (mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
new ConnectUtil(mContext).ensureCellularValidated();
}
@@ -3023,9 +3081,13 @@
networkCallbackRule.requestCell();
final Network wifiNetwork = prepareUnvalidatedNetwork();
- // Default network should not be wifi ,but checking that wifi is not the default doesn't
- // guarantee that it won't become the default in the future.
- assertNotEquals(wifiNetwork, mCm.getActiveNetwork());
+ // Default network should not be wifi ,but checking that Wi-Fi is not the default
+ // doesn't guarantee that it won't become the default in the future.
+ // On U 24Q2+ telephony may teardown (unregisterAfterReplacement) its network when Wi-Fi
+ // is toggled (as part of prepareUnvalidatedNetwork here). Give some time for Wi-Fi to
+ // not be default in case telephony is reconnecting.
+ assertEventuallyTrue("Wifi remained default despite being unvalidated",
+ WIFI_CONNECT_TIMEOUT_MS, () -> !wifiNetwork.equals(mCm.getActiveNetwork()));
final TestableNetworkCallback wifiCb = networkCallbackRule.registerNetworkCallback(
makeWifiNetworkRequest());
@@ -3062,6 +3124,7 @@
try {
final Network cellNetwork = networkCallbackRule.requestCell();
+ ensureCellIsValidated();
final Network wifiNetwork = prepareValidatedNetwork();
final TestableNetworkCallback defaultCb =
@@ -3157,7 +3220,12 @@
}
private Network prepareValidatedNetwork() throws Exception {
- ensureCellIsValidatedBeforeMockingValidationUrls();
+ // Verify that current supported network is validated so that the mock http server will not
+ // apply to unexpected networks. Also see aosp/2208680.
+ //
+ // This may also apply to wifi in principle, but in practice methods that mock validation
+ // URL all disconnect wifi forcefully anyway, so don't wait for wifi to validate.
+ ensureCellIsValidated();
prepareHttpServer();
configTestServer(Status.NO_CONTENT, Status.NO_CONTENT);
@@ -3169,7 +3237,7 @@
}
private Network preparePartialConnectivity() throws Exception {
- ensureCellIsValidatedBeforeMockingValidationUrls();
+ ensureCellIsValidated();
prepareHttpServer();
// Configure response code for partial connectivity
@@ -3184,7 +3252,7 @@
}
private Network prepareUnvalidatedNetwork() throws Exception {
- ensureCellIsValidatedBeforeMockingValidationUrls();
+ ensureCellIsValidated();
prepareHttpServer();
// Configure response code for unvalidated network
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
index fb63a19..b1e5680 100644
--- a/tests/cts/net/src/android/net/cts/DnsTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -16,6 +16,10 @@
package android.net.cts;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
@@ -23,10 +27,16 @@
import android.net.Network;
import android.net.NetworkInfo;
import android.os.SystemClock;
-import android.test.AndroidTestCase;
import android.util.Log;
import androidx.test.filters.RequiresDevice;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -36,7 +46,9 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-public class DnsTest extends AndroidTestCase {
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+public class DnsTest {
static {
System.loadLibrary("nativedns_jni");
@@ -46,10 +58,13 @@
private static final String TAG = "DnsTest";
private static final String PROXY_NETWORK_TYPE = "PROXY";
+ private Context mContext;
private ConnectivityManager mCm;
+ @Before
public void setUp() {
- mCm = getContext().getSystemService(ConnectivityManager.class);
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mCm = mContext.getSystemService(ConnectivityManager.class);
}
/**
@@ -69,6 +84,7 @@
* Perf - measure size of first and second tier caches and their effect
* Assert requires network permission
*/
+ @Test
@RequiresDevice // IPv6 support may be missing on presubmit virtual hardware
public void testDnsWorks() throws Exception {
ensureIpv6Connectivity();
@@ -91,7 +107,7 @@
// Skip the rest of the test if the active network for watch is PROXY.
// TODO: Check NetworkInfo type in addition to type name once ag/601257 is merged.
- if (getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
&& activeNetworkInfoIsProxy()) {
Log.i(TAG, "Skipping test because the active network type name is PROXY.");
return;
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index f73134a..1de4cf9 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -71,7 +71,7 @@
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.RouterAdvertisementResponder
import com.android.testutils.SC_V2
-import com.android.testutils.TapPacketReader
+import com.android.testutils.PollPacketReader
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnDscpPolicyStatusUpdated
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
@@ -135,7 +135,7 @@
private lateinit var srcAddressV6: Inet6Address
private lateinit var iface: TestNetworkInterface
private lateinit var tunNetworkCallback: TestNetworkCallback
- private lateinit var reader: TapPacketReader
+ private lateinit var reader: PollPacketReader
private lateinit var arpResponder: ArpResponder
private lateinit var raResponder: RouterAdvertisementResponder
@@ -169,7 +169,7 @@
}
handlerThread.start()
- reader = TapPacketReader(
+ reader = PollPacketReader(
handlerThread.threadHandler,
iface.fileDescriptor.fileDescriptor,
MAX_PACKET_LENGTH)
@@ -298,7 +298,8 @@
fun sendPacket(
agent: TestableNetworkAgent,
sendV6: Boolean,
- dstPort: Int = 0
+ dstPort: Int = 0,
+ times: Int = 1
) {
val testString = "test string"
val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
@@ -308,9 +309,11 @@
IPPROTO_UDP)
checkNotNull(agent.network).bindSocket(socket)
- val originalPacket = testPacket.readAsArray()
- Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
+ val origPacket = testPacket.readAsArray()
+ repeat(times) {
+ Os.sendto(socket, origPacket, 0 /* bytesOffset */, origPacket.size, 0 /* flags */,
if (sendV6) TEST_TARGET_IPV6_ADDR else TEST_TARGET_IPV4_ADDR, dstPort)
+ }
Os.close(socket)
}
@@ -400,10 +403,11 @@
agent: TestableNetworkAgent,
sendV6: Boolean = false,
dscpValue: Int = 0,
- dstPort: Int = 0
+ dstPort: Int = 0,
+ times: Int = 1
) {
- var packetFound = false
- sendPacket(agent, sendV6, dstPort)
+ var packetFound = 0
+ sendPacket(agent, sendV6, dstPort, times)
// TODO: grab source port from socket in sendPacket
Log.e(TAG, "find DSCP value:" + dscpValue)
@@ -424,10 +428,23 @@
if (parsePacketIp(buffer, sendV6) && parsePacketPort(buffer, 0, dstPort)) {
Log.e(TAG, "DSCP value found")
assertEquals(dscpValue, dscp)
- packetFound = true
+ packetFound++
}
}
- assertTrue(packetFound)
+ assertTrue(packetFound == times)
+ }
+
+ fun validatePackets(
+ agent: TestableNetworkAgent,
+ sendV6: Boolean = false,
+ dscpValue: Int = 0,
+ dstPort: Int = 0
+ ) {
+ // We send two packets from the same socket to verify
+ // socket caching works correctly.
+ validatePacket(agent, sendV6, dscpValue, dstPort, 2)
+ // Try one more time from a different socket.
+ validatePacket(agent, sendV6, dscpValue, dstPort, 1)
}
fun doRemovePolicyTest(
@@ -453,10 +470,7 @@
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, dscpValue = 1, dstPort = 4444)
- // Send a second packet to validate that the stored BPF policy
- // is correct for subsequent packets.
- validatePacket(agent, dscpValue = 1, dstPort = 4444)
+ validatePackets(agent, dscpValue = 1, dstPort = 4444)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -475,7 +489,7 @@
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, dscpValue = 4, dstPort = 5555)
+ validatePackets(agent, dscpValue = 4, dstPort = 5555)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -494,10 +508,7 @@
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
- // Send a second packet to validate that the stored BPF policy
- // is correct for subsequent packets.
- validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
+ validatePackets(agent, true, dscpValue = 1, dstPort = 4444)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -515,7 +526,7 @@
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, true, dscpValue = 4, dstPort = 5555)
+ validatePackets(agent, true, dscpValue = 4, dstPort = 5555)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -533,7 +544,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -541,7 +552,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -549,16 +560,16 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
/* Remove Policies and check CE is no longer set */
doRemovePolicyTest(agent, callback, 1)
- validatePacket(agent, dscpValue = 0, dstPort = 1111)
+ validatePackets(agent, dscpValue = 0, dstPort = 1111)
doRemovePolicyTest(agent, callback, 2)
- validatePacket(agent, dscpValue = 0, dstPort = 2222)
+ validatePackets(agent, dscpValue = 0, dstPort = 2222)
doRemovePolicyTest(agent, callback, 3)
- validatePacket(agent, dscpValue = 0, dstPort = 3333)
+ validatePackets(agent, dscpValue = 0, dstPort = 3333)
}
@Test
@@ -569,7 +580,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
doRemovePolicyTest(agent, callback, 1)
@@ -578,7 +589,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
doRemovePolicyTest(agent, callback, 2)
@@ -587,7 +598,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
doRemovePolicyTest(agent, callback, 3)
}
@@ -601,7 +612,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -609,7 +620,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -617,7 +628,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
/* Remove Policies and check CE is no longer set */
@@ -643,7 +654,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1)
@@ -652,7 +663,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1)
@@ -661,24 +672,24 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
agent.sendRemoveAllDscpPolicies()
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- validatePacket(agent, false, dstPort = 1111)
+ validatePackets(agent, false, dstPort = 1111)
}
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- validatePacket(agent, false, dstPort = 2222)
+ validatePackets(agent, false, dstPort = 2222)
}
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- validatePacket(agent, false, dstPort = 3333)
+ validatePackets(agent, false, dstPort = 3333)
}
}
@@ -690,7 +701,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 4444)
+ validatePackets(agent, dscpValue = 1, dstPort = 4444)
}
val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build()
@@ -700,8 +711,8 @@
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
// Sending packet with old policy should fail
- validatePacket(agent, dscpValue = 0, dstPort = 4444)
- validatePacket(agent, dscpValue = 1, dstPort = 5555)
+ validatePackets(agent, dscpValue = 0, dstPort = 4444)
+ validatePackets(agent, dscpValue = 1, dstPort = 5555)
}
agent.sendRemoveDscpPolicy(1)
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 61ebd8f..9be579b 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -72,7 +72,7 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.RouterAdvertisementResponder
-import com.android.testutils.TapPacketReader
+import com.android.testutils.PollPacketReader
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.assertThrows
import com.android.testutils.runAsShell
@@ -145,13 +145,15 @@
private var tetheredInterfaceRequest: TetheredInterfaceRequest? = null
+ private var ethernetEnabled = true
+
private class EthernetTestInterface(
context: Context,
private val handler: Handler,
hasCarrier: Boolean
) {
private val tapInterface: TestNetworkInterface
- private val packetReader: TapPacketReader
+ private val packetReader: PollPacketReader
private val raResponder: RouterAdvertisementResponder
private val tnm: TestNetworkManager
val name get() = tapInterface.interfaceName
@@ -169,7 +171,11 @@
tnm.createTapInterface(hasCarrier, false /* bringUp */)
}
val mtu = tapInterface.mtu
- packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu)
+ packetReader = PollPacketReader(
+ handler,
+ tapInterface.fileDescriptor.fileDescriptor,
+ mtu
+ )
raResponder = RouterAdvertisementResponder(packetReader)
val iidString = "fe80::${Integer.toHexString(Random().nextInt(65536))}"
val linklocal = InetAddresses.parseNumericAddress(iidString) as Inet6Address
@@ -336,7 +342,7 @@
}
}
- private fun isEthernetSupported() : Boolean {
+ private fun isEthernetSupported(): Boolean {
return context.getSystemService(EthernetManager::class.java) != null
}
@@ -424,7 +430,7 @@
// when an interface comes up, we should always see a down cb before an up cb.
ifaceListener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
- if (hasCarrier) {
+ if (hasCarrier && ethernetEnabled) {
ifaceListener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
}
return iface
@@ -510,6 +516,7 @@
private fun setEthernetEnabled(enabled: Boolean) {
runAsShell(NETWORK_SETTINGS) { em.setEthernetEnabled(enabled) }
+ ethernetEnabled = enabled
val listener = EthernetStateListener()
addEthernetStateListener(listener)
listener.eventuallyExpect(if (enabled) ETHERNET_STATE_ENABLED else ETHERNET_STATE_DISABLED)
@@ -596,26 +603,6 @@
}
}
- @Test
- fun testCallbacks_withRunningInterface() {
- assumeFalse(isAdbOverEthernet())
- // Only run this test when no non-restricted / physical interfaces are present.
- assumeNoInterfaceForTetheringAvailable()
-
- val iface = createInterface()
- val listener = EthernetStateListener()
- addInterfaceStateListener(listener)
- listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
-
- // Remove running interface. The interface stays running but is no longer tracked.
- setEthernetEnabled(false)
- listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
-
- setEthernetEnabled(true)
- listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
- listener.assertNoCallback()
- }
-
private fun assumeNoInterfaceForTetheringAvailable() {
// Interfaces that have configured NetworkCapabilities will never be used for tethering,
// see aosp/2123900.
@@ -907,6 +894,30 @@
}
@Test
+ fun testEnableDisableInterface_disableEnableEthernet() {
+ val iface = createInterface()
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+ // When ethernet is disabled, interface should be down and enable/disableInterface()
+ // should not bring the interfaces up.
+ setEthernetEnabled(false)
+ listener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+ enableInterface(iface).expectError()
+ disableInterface(iface).expectError()
+ listener.assertNoCallback()
+
+ // When ethernet is enabled, enable/disableInterface() should succeed.
+ setEthernetEnabled(true)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+ disableInterface(iface).expectResult(iface.name)
+ listener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+ enableInterface(iface).expectResult(iface.name)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+ }
+
+ @Test
fun testUpdateConfiguration_forBothIpConfigAndCapabilities() {
val iface = createInterface()
val cb = requestNetwork(ETH_REQUEST.copyWithEthernetSpecifier(iface.name))
@@ -1014,4 +1025,68 @@
cb.eventuallyExpectCapabilities(TEST_CAPS)
cb.eventuallyExpectLpForStaticConfig(STATIC_IP_CONFIGURATION.staticIpConfiguration)
}
+
+ @Test
+ fun testAddInterface_disableEnableEthernet() {
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+
+ // When ethernet is disabled, newly added interfaces should not be brought up.
+ setEthernetEnabled(false)
+ val iface = createInterface(/* hasCarrier */ true)
+ listener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+
+ // When ethernet is re-enabled after interface is added, it will be brought up.
+ setEthernetEnabled(true)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+ }
+
+
+ @Test
+ fun testRemoveInterface_disableEnableEthernet() {
+ // Set up 2 interfaces for testing
+ val iface1 = createInterface()
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+ listener.eventuallyExpect(iface1, STATE_LINK_UP, ROLE_CLIENT)
+ val iface2 = createInterface()
+ listener.eventuallyExpect(iface2, STATE_LINK_UP, ROLE_CLIENT)
+
+ // Removing interfaces when ethernet is enabled will first send link down, then
+ // STATE_ABSENT/ROLE_NONE.
+ removeInterface(iface1)
+ listener.expectCallback(iface1, STATE_LINK_DOWN, ROLE_CLIENT)
+ listener.expectCallback(iface1, STATE_ABSENT, ROLE_NONE)
+
+ // Removing interfaces after ethernet is disabled will first send link down when ethernet is
+ // disabled, then STATE_ABSENT/ROLE_NONE when interface is removed.
+ setEthernetEnabled(false)
+ listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+ removeInterface(iface2)
+ listener.expectCallback(iface2, STATE_ABSENT, ROLE_NONE)
+ }
+
+ @Test
+ fun testSetTetheringInterfaceMode_disableEnableEthernet() {
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+
+ val iface = createInterface()
+ requestTetheredInterface().expectOnAvailable()
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
+
+ // (b/234743836): Currently the state of server mode interfaces always returns true due to
+ // that interface state for server mode interfaces is not tracked properly.
+ // So we do not get any state change when disabling ethernet.
+ setEthernetEnabled(false)
+ listener.assertNoCallback()
+
+ // When ethernet is disabled, change interface mode will not bring the interface up.
+ releaseTetheredInterface()
+ listener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+
+ // When ethernet is re-enabled, interface will be brought up.
+ setEthernetEnabled(true)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
index 2a6c638..c480135 100644
--- a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -23,10 +23,13 @@
import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384;
import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512;
import static android.net.IpSecAlgorithm.CRYPT_AES_CBC;
+import static android.net.cts.PacketUtils.ICMP_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
import static android.system.OsConstants.FIONREAD;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.net.ConnectivityManager;
@@ -152,10 +155,17 @@
final IpSecTransformState transformState =
futureIpSecTransform.get(SOCK_TIMEOUT, TimeUnit.MILLISECONDS);
- assertEquals(txHighestSeqNum, transformState.getTxHighestSequenceNumber());
+ // There might be ICMPv6(Router Solicitation) packets. Thus we can only check the lower
+ // bound of the outgoing traffic.
+ final long icmpV6RsCnt = transformState.getTxHighestSequenceNumber() - txHighestSeqNum;
+ assertTrue(icmpV6RsCnt >= 0);
+
+ final long adjustedPacketCnt = packetCnt + icmpV6RsCnt;
+ final long adjustedByteCnt = byteCnt + icmpV6RsCnt * (IP6_HDRLEN + ICMP_HDRLEN);
+
+ assertEquals(adjustedPacketCnt, transformState.getPacketCount());
+ assertEquals(adjustedByteCnt, transformState.getByteCount());
assertEquals(rxHighestSeqNum, transformState.getRxHighestSequenceNumber());
- assertEquals(packetCnt, transformState.getPacketCount());
- assertEquals(byteCnt, transformState.getByteCount());
assertArrayEquals(replayBitmap, transformState.getReplayBitmap());
}
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index b5f43d3..e94d94f 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -451,9 +451,10 @@
long uidTxDelta = 0;
long uidRxDelta = 0;
for (int i = 0; i < 100; i++) {
- // Clear TrafficStats cache is needed to avoid rate-limit caching for
- // TrafficStats API results on V+ devices.
- if (SdkLevel.isAtLeastV()) {
+ // Since the target SDK of this test should always equal or be larger than V,
+ // TrafficStats caching is always enabled. Clearing the cache is needed to
+ // avoid rate-limiting on devices with a mainlined (T+) NetworkStatsService.
+ if (SdkLevel.isAtLeastT()) {
runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
}
uidTxDelta = TrafficStats.getUidTxPackets(Os.getuid()) - uidTxPackets;
@@ -530,9 +531,10 @@
}
private static void initStatsChecker() throws Exception {
- // Clear TrafficStats cache is needed to avoid rate-limit caching for
- // TrafficStats API results on V+ devices.
- if (SdkLevel.isAtLeastV()) {
+ // Since the target SDK of this test should always equal or be larger than V,
+ // TrafficStats caching is always enabled. Clearing the cache is needed to
+ // avoid rate-limiting on devices with a mainlined (T+) NetworkStatsService.
+ if (SdkLevel.isAtLeastT()) {
runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
}
uidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
index 22a51d6..f2c6d33 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
@@ -65,6 +65,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.CollectionUtils;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -119,7 +120,7 @@
private static final int TIMEOUT_MS = 500;
- private static final int PACKET_COUNT = 5000;
+ private static final int PACKET_COUNT = 100;
// Static state to reduce setup/teardown
private static ConnectivityManager sCM;
@@ -1088,6 +1089,27 @@
UdpEncapsulationSocket encapSocket,
IpSecTunnelTestRunnable test)
throws Exception {
+ return buildTunnelNetworkAndRunTests(
+ localInner,
+ remoteInner,
+ localOuter,
+ remoteOuter,
+ spi,
+ encapSocket,
+ test,
+ true /* enableEncrypt */);
+ }
+
+ private int buildTunnelNetworkAndRunTests(
+ InetAddress localInner,
+ InetAddress remoteInner,
+ InetAddress localOuter,
+ InetAddress remoteOuter,
+ int spi,
+ UdpEncapsulationSocket encapSocket,
+ IpSecTunnelTestRunnable test,
+ boolean enableEncrypt)
+ throws Exception {
int innerPrefixLen = localInner instanceof Inet6Address ? IP6_PREFIX_LEN : IP4_PREFIX_LEN;
TestNetworkCallback testNetworkCb = null;
int innerSocketPort;
@@ -1115,8 +1137,12 @@
// Configure Transform parameters
IpSecTransform.Builder transformBuilder = new IpSecTransform.Builder(sContext);
- transformBuilder.setEncryption(
- new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+
+ if (enableEncrypt) {
+ transformBuilder.setEncryption(
+ new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+ }
+
transformBuilder.setAuthentication(
new IpSecAlgorithm(
IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4));
@@ -1167,8 +1193,8 @@
return innerSocketPort;
}
- private int buildTunnelNetworkAndRunTestsSimple(int spi, IpSecTunnelTestRunnable test)
- throws Exception {
+ private int buildTunnelNetworkAndRunTestsSimple(
+ int spi, IpSecTunnelTestRunnable test, boolean enableEncrypt) throws Exception {
return buildTunnelNetworkAndRunTests(
LOCAL_INNER_6,
REMOTE_INNER_6,
@@ -1176,7 +1202,8 @@
REMOTE_OUTER_6,
spi,
null /* encapSocket */,
- test);
+ test,
+ enableEncrypt);
}
private static void receiveAndValidatePacket(JavaUdpSocket socket) throws Exception {
@@ -1787,10 +1814,11 @@
PACKET_COUNT,
PACKET_COUNT,
PACKET_COUNT * (long) innerPacketSize,
- newReplayBitmap(REPLAY_BITMAP_LEN_BYTE * 8));
+ newReplayBitmap(PACKET_COUNT));
return innerSocketPort;
- });
+ },
+ true /* enableEncrypt */);
}
@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -1814,17 +1842,22 @@
ipsecNetwork.bindSocket(outSocket.mSocket);
int innerSocketPort = outSocket.getPort();
- int expectedPacketSize =
- getPacketSize(
- AF_INET6,
- AF_INET6,
- false /* useEncap */,
- false /* transportInTunnelMode */);
+ int outSeqNum = 1;
+ int receivedTestDataEspCnt = 0;
- for (int i = 0; i < PACKET_COUNT; i++) {
+ while (receivedTestDataEspCnt < PACKET_COUNT) {
outSocket.sendTo(TEST_DATA, REMOTE_INNER_6, innerSocketPort);
- tunUtils.awaitEspPacketNoPlaintext(
- spi, TEST_DATA, false /* useEncap */, expectedPacketSize);
+
+ byte[] pkt = null;
+
+ // If it is an ESP that contains the TEST_DATA, move to the next
+ // loop. Otherwise, the ESP may contain an ICMPv6(Router Solicitation).
+ // In this case, just increase the expected sequence number and continue
+ // waiting for the ESP with TEST_DATA
+ do {
+ pkt = tunUtils.awaitEspPacket(spi, false /* useEncap */, outSeqNum++);
+ } while (CollectionUtils.indexOfSubArray(pkt, TEST_DATA) == -1);
+ receivedTestDataEspCnt++;
}
final int innerPacketSize =
@@ -1838,6 +1871,48 @@
newReplayBitmap(0));
return innerSocketPort;
- });
+ },
+ false /* enableEncrypt */);
+ }
+
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ public void testMigrateWhenMultipleTunnelsExist() throws Exception {
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ assumeTrue(mCtsNetUtils.hasIpsecTunnelMigrateFeature());
+
+ final int spi = getRandomSpi(LOCAL_OUTER_6, REMOTE_OUTER_6);
+
+ // Create tunnelIfaceFoo and tunnelIfaceBar. Verify tunnelIfaceBar migration will not throw
+ try (IpSecManager.IpSecTunnelInterface tunnelIfaceFoo =
+ mISM.createIpSecTunnelInterface(
+ LOCAL_OUTER_4, REMOTE_OUTER_4, sTunWrapper.network)) {
+
+ buildTunnelNetworkAndRunTestsSimple(
+ spi,
+ (ipsecNetwork,
+ tunnelIfaceBar,
+ tunUtils,
+ inTunnelTransform,
+ outTunnelTransform,
+ localOuter,
+ remoteOuter,
+ seqNum) -> {
+ tunnelIfaceBar.setUnderlyingNetwork(sTunWrapperNew.network);
+
+ mISM.startTunnelModeTransformMigration(
+ inTunnelTransform, REMOTE_OUTER_6_NEW, LOCAL_OUTER_6_NEW);
+ mISM.startTunnelModeTransformMigration(
+ outTunnelTransform, LOCAL_OUTER_6_NEW, REMOTE_OUTER_6_NEW);
+
+ mISM.applyTunnelModeTransform(
+ tunnelIfaceBar, IpSecManager.DIRECTION_IN, inTunnelTransform);
+ mISM.applyTunnelModeTransform(
+ tunnelIfaceBar, IpSecManager.DIRECTION_OUT, outTunnelTransform);
+
+ return 0 /* not used */;
+ },
+ true /* enableEncrypt */);
+ }
}
}
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 2c7d5c6..c67443e 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -39,10 +39,13 @@
import android.system.ErrnoException;
import android.system.OsConstants;
import android.util.ArraySet;
+import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.net.module.util.CollectionUtils;
import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.DeviceConfigRule;
@@ -51,6 +54,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Set;
@DevSdkIgnoreRunner.RestoreDefaultNetwork
@@ -70,13 +75,34 @@
private static final String TAG = "MultinetworkNativeApiTest";
static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
+ public static class QueryTestResult {
+ public final int sourcePort;
+ public final int attempts;
+ public final int errNo;
+
+ public QueryTestResult(int sourcePort, int attempts, int errNo) {
+ this.sourcePort = sourcePort;
+ this.attempts = attempts;
+ this.errNo = errNo;
+ }
+
+ @Override
+ public String toString() {
+ return "QueryTestResult{"
+ + "sourcePort=" + sourcePort
+ + ", attempts=" + attempts
+ + ", errNo=" + errNo
+ + '}';
+ }
+ }
+
/**
* @return 0 on success
*/
private static native int runGetaddrinfoCheck(long networkHandle);
private static native int runSetprocnetwork(long networkHandle);
private static native int runSetsocknetwork(long networkHandle);
- private static native int runDatagramCheck(long networkHandle);
+ private static native QueryTestResult runDatagramCheck(long networkHandle, int sourcePort);
private static native void runResNapiMalformedCheck(long networkHandle);
private static native void runResNcancelCheck(long networkHandle);
private static native void runResNqueryCheck(long networkHandle);
@@ -165,14 +191,69 @@
}
}
+ private void runNativeDatagramTransmissionDiagnostics(Network network,
+ QueryTestResult failedResult) {
+ final ConnectivityDiagnosticsCollector collector = ConnectivityDiagnosticsCollector
+ .getInstance();
+ if (collector == null) {
+ Log.e(TAG, "Missing ConnectivityDiagnosticsCollector, not adding diagnostics");
+ return;
+ }
+
+ final int numReruns = 10;
+ final ArrayList<QueryTestResult> reruns = new ArrayList<>(numReruns);
+ for (int i = 0; i < numReruns; i++) {
+ final QueryTestResult rerunResult =
+ runDatagramCheck(network.getNetworkHandle(), 0 /* sourcePort */);
+ Log.d(TAG, "Rerun result " + i + ": " + rerunResult);
+ reruns.add(rerunResult);
+ }
+ // Rerun on the original port after trying the other ports, to check that the results are
+ // consistent, as opposed to the network recovering halfway through.
+ int originalPortFailedReruns = 0;
+ for (int i = 0; i < numReruns; i++) {
+ final QueryTestResult originalPortRerun = runDatagramCheck(network.getNetworkHandle(),
+ failedResult.sourcePort);
+ Log.d(TAG, "Rerun result " + i + " with original port: " + originalPortRerun);
+ if (originalPortRerun.errNo != 0) {
+ originalPortFailedReruns++;
+ }
+ }
+
+ final int noRetrySuccessResults = reruns.stream()
+ .filter(result -> result.errNo == 0 && result.attempts == 1)
+ .mapToInt(result -> 1)
+ .sum();
+ final int failedResults = reruns.stream()
+ .filter(result -> result.errNo != 0)
+ .mapToInt(result -> 1)
+ .sum();
+ collector.addFailureAttribute("numReruns", numReruns);
+ collector.addFailureAttribute("noRetrySuccessReruns", noRetrySuccessResults);
+ collector.addFailureAttribute("failedReruns", failedResults);
+ collector.addFailureAttribute("originalPortFailedReruns", originalPortFailedReruns);
+ }
+
@Test
public void testNativeDatagramTransmission() throws Exception {
for (Network network : getTestableNetworks()) {
- int errno = runDatagramCheck(network.getNetworkHandle());
- if (errno != 0) {
- throw new ErrnoException(
- "DatagramCheck on " + mCM.getNetworkInfo(network), -errno);
+ final QueryTestResult result = runDatagramCheck(network.getNetworkHandle(),
+ 0 /* sourcePort */);
+ if (result.errNo == 0) {
+ continue;
}
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ final int[] transports = nc != null ? nc.getTransportTypes() : null;
+ if (CollectionUtils.contains(transports, TRANSPORT_WIFI)) {
+ runNativeDatagramTransmissionDiagnostics(network, result);
+ }
+
+ // Log the whole result (with source port and attempts) to logcat, but use only the
+ // errno and transport in the fail message so similar failures have consistent messages
+ final String error = "DatagramCheck on transport " + Arrays.toString(transports)
+ + " failed: " + result.errNo;
+ Log.e(TAG, error + ", result: " + result);
+ fail(error);
}
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 60081d4..815c3a5 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -83,13 +83,17 @@
import android.os.ConditionVariable
import android.os.Handler
import android.os.HandlerThread
+import android.os.Looper
import android.os.Message
import android.os.PersistableBundle
import android.os.Process
import android.os.SystemClock
import android.platform.test.annotations.AppModeFull
+import android.system.Os
+import android.system.OsConstants.AF_INET6
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
@@ -105,6 +109,10 @@
import com.android.compatibility.common.util.UiccUtil
import com.android.modules.utils.build.SdkLevel
import com.android.net.module.util.ArrayTrackRecord
+import com.android.net.module.util.NetworkStackConstants.ETHER_MTU
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET
+import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
import com.android.testutils.CompatUtil
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
@@ -115,6 +123,7 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.Losing
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.PollPacketReader
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
@@ -133,6 +142,7 @@
import com.android.testutils.assertThrows
import com.android.testutils.runAsShell
import com.android.testutils.tryTest
+import com.android.testutils.waitForIdle
import java.io.Closeable
import java.io.IOException
import java.net.DatagramSocket
@@ -140,10 +150,13 @@
import java.net.InetSocketAddress
import java.net.Socket
import java.security.MessageDigest
+import java.nio.ByteBuffer
import java.time.Duration
import java.util.Arrays
+import java.util.Random
import java.util.UUID
import java.util.concurrent.Executors
+import kotlin.collections.ArrayList
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
@@ -188,6 +201,11 @@
it.obj = obj
}
+private val LINK_ADDRESS = LinkAddress("2001:db8::1/64")
+private val REMOTE_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::123")
+private val PREFIX = IpPrefix("2001:db8::/64")
+private val NEXTHOP = InetAddresses.parseNumericAddress("fe80::abcd")
+
// On T and below, the native network is only created when the agent connects.
// Starting in U, the native network was to be created as soon as the agent is registered,
// but this has been flagged off for now pending resolution of race conditions.
@@ -321,6 +339,15 @@
if (transports.size > 0) removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
}
+ private fun makeTestLinkProperties(ifName: String): LinkProperties {
+ return LinkProperties().apply {
+ interfaceName = ifName
+ addLinkAddress(LINK_ADDRESS)
+ addRoute(RouteInfo(PREFIX, null /* nextHop */, ifName))
+ addRoute(RouteInfo(IpPrefix("::/0"), NEXTHOP, ifName))
+ }
+ }
+
private fun createNetworkAgent(
context: Context = realContext,
specifier: String? = null,
@@ -341,6 +368,7 @@
private fun createConnectedNetworkAgent(
context: Context = realContext,
+ lp: LinkProperties? = null,
specifier: String? = UUID.randomUUID().toString(),
initialConfig: NetworkAgentConfig? = null,
expectedInitSignalStrengthThresholds: IntArray = intArrayOf(),
@@ -350,7 +378,8 @@
// Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
requestNetwork(makeTestNetworkRequest(specifier), callback)
val nc = makeTestNetworkCapabilities(specifier, transports)
- val agent = createNetworkAgent(context, initialConfig = initialConfig, initialNc = nc)
+ val agent = createNetworkAgent(context, initialConfig = initialConfig, initialLp = lp,
+ initialNc = nc)
agent.setTeardownDelayMillis(0)
// Connect the agent and verify initial status callbacks.
agent.register()
@@ -361,8 +390,9 @@
return agent to callback
}
- private fun connectNetwork(vararg transports: Int): Pair<TestableNetworkAgent, Network> {
- val (agent, callback) = createConnectedNetworkAgent(transports = transports)
+ private fun connectNetwork(vararg transports: Int, lp: LinkProperties? = null):
+ Pair<TestableNetworkAgent, Network> {
+ val (agent, callback) = createConnectedNetworkAgent(transports = transports, lp = lp)
val network = agent.network!!
// createConnectedNetworkAgent internally files a request; release it so that the network
// will be torn down if unneeded.
@@ -382,8 +412,9 @@
assertNoCallback()
}
- private fun createTunInterface(): TestNetworkInterface = realContext.getSystemService(
- TestNetworkManager::class.java)!!.createTunInterface(emptyList()).also {
+ private fun createTunInterface(addrs: Collection<LinkAddress> = emptyList()):
+ TestNetworkInterface = realContext.getSystemService(
+ TestNetworkManager::class.java)!!.createTunInterface(addrs).also {
ifacesToCleanUp.add(it)
}
@@ -1501,15 +1532,75 @@
private fun createEpsAttributes(qci: Int = 1): EpsBearerQosSessionAttributes {
val remoteAddresses = ArrayList<InetSocketAddress>()
- remoteAddresses.add(InetSocketAddress("2001:db8::123", 80))
+ remoteAddresses.add(InetSocketAddress(REMOTE_ADDRESS, 80))
return EpsBearerQosSessionAttributes(
qci, 2, 3, 4, 5,
remoteAddresses
)
}
+ fun sendAndExpectUdpPacket(net: Network,
+ reader: PollPacketReader, iface: TestNetworkInterface) {
+ val s = Os.socket(AF_INET6, SOCK_DGRAM, 0)
+ net.bindSocket(s)
+ val content = ByteArray(16)
+ Random().nextBytes(content)
+ Os.sendto(s, ByteBuffer.wrap(content), 0, REMOTE_ADDRESS, 7 /* port */)
+ val match = reader.poll(DEFAULT_TIMEOUT_MS) {
+ val udpStart = IPV6_HEADER_LEN + UDP_HEADER_LEN
+ it.size == udpStart + content.size &&
+ it[0].toInt() and 0xf0 == 0x60 &&
+ it[IPV6_PROTOCOL_OFFSET].toInt() == IPPROTO_UDP &&
+ Arrays.equals(content, it.copyOfRange(udpStart, udpStart + content.size))
+ }
+ assertNotNull(match, "Did not receive matching packet on ${iface.interfaceName} " +
+ " after ${DEFAULT_TIMEOUT_MS}ms")
+ }
+
+ fun createInterfaceAndReader(): Triple<TestNetworkInterface, PollPacketReader, LinkProperties> {
+ val iface = createTunInterface(listOf(LINK_ADDRESS))
+ val handler = Handler(Looper.getMainLooper())
+ val reader = PollPacketReader(handler, iface.fileDescriptor.fileDescriptor, ETHER_MTU)
+ reader.startAsyncForTest()
+ handler.waitForIdle(DEFAULT_TIMEOUT_MS)
+ val ifName = iface.interfaceName
+ val lp = makeTestLinkProperties(ifName)
+ return Triple(iface, reader, lp)
+ }
+
+ @Test
+ fun testRegisterAfterUnregister() {
+ val (iface, reader, lp) = createInterfaceAndReader()
+
+ // File a request that matches and keeps up the best-scoring test network.
+ val testCallback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+ requestNetwork(makeTestNetworkRequest(), testCallback)
+
+ // Register and unregister networkagents in a loop, checking that every time an agent
+ // connects, the native network is correctly configured and packets can be sent.
+ // Running 10 iterations takes about 1 second on x86 cuttlefish, and detects the race in
+ // b/286649301 most of the time.
+ for (i in 1..10) {
+ val agent1 = createNetworkAgent(realContext, initialLp = lp)
+ agent1.register()
+ agent1.unregister()
+
+ val agent2 = createNetworkAgent(realContext, initialLp = lp)
+ agent2.register()
+ agent2.markConnected()
+ val network2 = agent2.network!!
+
+ testCallback.expectAvailableThenValidatedCallbacks(network2)
+ sendAndExpectUdpPacket(network2, reader, iface)
+ agent2.unregister()
+ testCallback.expect<Lost>(network2)
+ }
+ }
+
@Test
fun testUnregisterAfterReplacement() {
+ val (iface, reader, lp) = createInterfaceAndReader()
+
// Keeps an eye on all test networks.
val matchAllCallback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
registerNetworkCallback(makeTestNetworkRequest(), matchAllCallback)
@@ -1519,14 +1610,13 @@
requestNetwork(makeTestNetworkRequest(), testCallback)
// Connect the first network. This should satisfy the request.
- val (agent1, network1) = connectNetwork()
+ val (agent1, network1) = connectNetwork(lp = lp)
matchAllCallback.expectAvailableThenValidatedCallbacks(network1)
testCallback.expectAvailableThenValidatedCallbacks(network1)
- // Check that network1 exists by binding a socket to it and getting no exceptions.
- network1.bindSocket(DatagramSocket())
+ sendAndExpectUdpPacket(network1, reader, iface)
// Connect a second agent. network1 is preferred because it was already registered, so
- // testCallback will not see any events. agent2 is be torn down because it has no requests.
+ // testCallback will not see any events. agent2 is torn down because it has no requests.
val (agent2, network2) = connectNetwork()
matchAllCallback.expectAvailableThenValidatedCallbacks(network2)
matchAllCallback.expect<Lost>(network2)
@@ -1551,9 +1641,10 @@
// as soon as it validates (until then, it is outscored by network1).
// The fact that the first events seen by matchAllCallback is the connection of network3
// implicitly ensures that no callbacks are sent since network1 was lost.
- val (agent3, network3) = connectNetwork()
+ val (agent3, network3) = connectNetwork(lp = lp)
matchAllCallback.expectAvailableThenValidatedCallbacks(network3)
testCallback.expectAvailableDoubleValidatedCallbacks(network3)
+ sendAndExpectUdpPacket(network3, reader, iface)
// As soon as the replacement arrives, network1 is disconnected.
// Check that this happens before the replacement timeout (5 seconds) fires.
@@ -1573,6 +1664,7 @@
matchAllCallback.expect<Losing>(network3)
testCallback.expectAvailableCallbacks(network4, validated = true)
mCM.unregisterNetworkCallback(agent4callback)
+ sendAndExpectUdpPacket(network3, reader, iface)
agent3.unregisterAfterReplacement(5_000)
agent3.expectCallback<OnNetworkUnwanted>()
matchAllCallback.expect<Lost>(network3, 1000L)
@@ -1588,9 +1680,10 @@
// If a network that is awaiting replacement is unregistered, it disconnects immediately,
// before the replacement timeout fires.
- val (agent5, network5) = connectNetwork()
+ val (agent5, network5) = connectNetwork(lp = lp)
matchAllCallback.expectAvailableThenValidatedCallbacks(network5)
testCallback.expectAvailableThenValidatedCallbacks(network5)
+ sendAndExpectUdpPacket(network5, reader, iface)
agent5.unregisterAfterReplacement(5_000 /* timeoutMillis */)
agent5.unregister()
matchAllCallback.expect<Lost>(network5, 1000L /* timeoutMs */)
@@ -1637,7 +1730,7 @@
matchAllCallback.assertNoCallback(200 /* timeoutMs */)
// If wifi is replaced within the timeout, the device does not switch to cellular.
- val (_, cellNetwork) = connectNetwork(TRANSPORT_CELLULAR)
+ val (cellAgent, cellNetwork) = connectNetwork(TRANSPORT_CELLULAR)
testCallback.expectAvailableThenValidatedCallbacks(cellNetwork)
matchAllCallback.expectAvailableThenValidatedCallbacks(cellNetwork)
@@ -1674,6 +1767,34 @@
matchAllCallback.expectAvailableThenValidatedCallbacks(newWifiNetwork)
matchAllCallback.expect<Lost>(wifiNetwork)
wifiAgent.expectCallback<OnNetworkUnwanted>()
+ testCallback.expect<CapabilitiesChanged>(newWifiNetwork)
+
+ cellAgent.unregister()
+ matchAllCallback.expect<Lost>(cellNetwork)
+ newWifiAgent.unregister()
+ matchAllCallback.expect<Lost>(newWifiNetwork)
+ testCallback.expect<Lost>(newWifiNetwork)
+
+ // Calling unregisterAfterReplacement several times in quick succession works.
+ // These networks are all kept up by testCallback.
+ val agent10 = createNetworkAgent(realContext, initialLp = lp)
+ agent10.register()
+ agent10.unregisterAfterReplacement(5_000)
+
+ val agent11 = createNetworkAgent(realContext, initialLp = lp)
+ agent11.register()
+ agent11.unregisterAfterReplacement(5_000)
+
+ val agent12 = createNetworkAgent(realContext, initialLp = lp)
+ agent12.register()
+ agent12.unregisterAfterReplacement(5_000)
+
+ val agent13 = createNetworkAgent(realContext, initialLp = lp)
+ agent13.register()
+ agent13.markConnected()
+ testCallback.expectAvailableThenValidatedCallbacks(agent13.network!!)
+ sendAndExpectUdpPacket(agent13.network!!, reader, iface)
+ agent13.unregister()
}
@Test
@@ -1706,14 +1827,7 @@
it.underlyingNetworks = listOf()
}
}
- val lp = LinkProperties().apply {
- interfaceName = ifName
- addLinkAddress(LinkAddress("2001:db8::1/64"))
- addRoute(RouteInfo(IpPrefix("2001:db8::/64"), null /* nextHop */, ifName))
- addRoute(RouteInfo(IpPrefix("::/0"),
- InetAddresses.parseNumericAddress("fe80::abcd"),
- ifName))
- }
+ val lp = makeTestLinkProperties(ifName)
// File a request containing the agent's specifier to receive callbacks and to ensure that
// the agent is not torn down due to being unneeded.
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
index 1a48983..10adee0 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
@@ -19,28 +19,28 @@
import static android.os.Process.INVALID_UID;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.INetworkStatsService;
import android.net.TrafficStats;
+import android.net.connectivity.android.net.netstats.StatsResult;
import android.os.Build;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
-import android.test.AndroidTestCase;
-import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.CollectionUtils;
+import com.android.testutils.ConnectivityModuleTest;
import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
-import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -48,37 +48,20 @@
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
-import java.util.function.Function;
import java.util.function.Predicate;
-@RunWith(AndroidJUnit4.class)
+@ConnectivityModuleTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2) // Mainline NetworkStats starts from T.
+@RunWith(DevSdkIgnoreRunner.class)
public class NetworkStatsBinderTest {
- // NOTE: These are shamelessly copied from TrafficStats.
- private static final int TYPE_RX_BYTES = 0;
- private static final int TYPE_RX_PACKETS = 1;
- private static final int TYPE_TX_BYTES = 2;
- private static final int TYPE_TX_PACKETS = 3;
-
- @Rule
- public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(
- Build.VERSION_CODES.Q /* ignoreClassUpTo */);
-
- private final SparseArray<Function<Integer, Long>> mUidStatsQueryOpArray = new SparseArray<>();
-
- @Before
- public void setUp() throws Exception {
- mUidStatsQueryOpArray.put(TYPE_RX_BYTES, uid -> TrafficStats.getUidRxBytes(uid));
- mUidStatsQueryOpArray.put(TYPE_RX_PACKETS, uid -> TrafficStats.getUidRxPackets(uid));
- mUidStatsQueryOpArray.put(TYPE_TX_BYTES, uid -> TrafficStats.getUidTxBytes(uid));
- mUidStatsQueryOpArray.put(TYPE_TX_PACKETS, uid -> TrafficStats.getUidTxPackets(uid));
- }
-
- private long getUidStatsFromBinder(int uid, int type) throws Exception {
- Method getServiceMethod = Class.forName("android.os.ServiceManager")
+ @Nullable
+ private StatsResult getUidStatsFromBinder(int uid) throws Exception {
+ final Method getServiceMethod = Class.forName("android.os.ServiceManager")
.getDeclaredMethod("getService", new Class[]{String.class});
- IBinder binder = (IBinder) getServiceMethod.invoke(null, Context.NETWORK_STATS_SERVICE);
- INetworkStatsService nss = INetworkStatsService.Stub.asInterface(binder);
- return nss.getUidStats(uid, type);
+ final IBinder binder = (IBinder) getServiceMethod.invoke(
+ null, Context.NETWORK_STATS_SERVICE);
+ final INetworkStatsService nss = INetworkStatsService.Stub.asInterface(binder);
+ return nss.getUidStats(uid);
}
private int getFirstAppUidThat(@NonNull Predicate<Integer> predicate) {
@@ -108,38 +91,34 @@
if (notMyUid != INVALID_UID) testUidList.add(notMyUid);
for (final int uid : testUidList) {
- for (int i = 0; i < mUidStatsQueryOpArray.size(); i++) {
- final int type = mUidStatsQueryOpArray.keyAt(i);
- try {
- final long uidStatsFromBinder = getUidStatsFromBinder(uid, type);
- final long uidTrafficStats = mUidStatsQueryOpArray.get(type).apply(uid);
+ try {
+ final StatsResult uidStatsFromBinder = getUidStatsFromBinder(uid);
- // Verify that UNSUPPORTED is returned if the uid is not current app uid.
- if (uid != myUid) {
- assertEquals(uidStatsFromBinder, TrafficStats.UNSUPPORTED);
- }
+ if (uid != myUid) {
+ // Verify that null is returned if the uid is not current app uid.
+ assertNull(uidStatsFromBinder);
+ } else {
// Verify that returned result is the same with the result get from
// TrafficStats.
- // TODO: If the test is flaky then it should instead assert that the values
- // are approximately similar.
- assertEquals("uidStats is not matched for query type " + type
- + ", uid=" + uid + ", myUid=" + myUid, uidTrafficStats,
- uidStatsFromBinder);
- } catch (IllegalAccessException e) {
- /* Java language access prevents exploitation. */
- return;
- } catch (InvocationTargetException e) {
- /* Underlying method has been changed. */
- return;
- } catch (ClassNotFoundException e) {
- /* not vulnerable if hidden API no longer available */
- return;
- } catch (NoSuchMethodException e) {
- /* not vulnerable if hidden API no longer available */
- return;
- } catch (RemoteException e) {
- return;
+ assertEquals(uidStatsFromBinder.rxBytes, TrafficStats.getUidRxBytes(uid));
+ assertEquals(uidStatsFromBinder.rxPackets, TrafficStats.getUidRxPackets(uid));
+ assertEquals(uidStatsFromBinder.txBytes, TrafficStats.getUidTxBytes(uid));
+ assertEquals(uidStatsFromBinder.txPackets, TrafficStats.getUidTxPackets(uid));
}
+ } catch (IllegalAccessException e) {
+ /* Java language access prevents exploitation. */
+ return;
+ } catch (InvocationTargetException e) {
+ /* Underlying method has been changed. */
+ return;
+ } catch (ClassNotFoundException e) {
+ /* not vulnerable if hidden API no longer available */
+ return;
+ } catch (NoSuchMethodException e) {
+ /* not vulnerable if hidden API no longer available */
+ return;
+ } catch (RemoteException e) {
+ return;
}
}
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index bc1a154..e3d7240 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -41,6 +41,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.annotation.NonNull;
import android.app.AppOpsManager;
import android.app.Instrumentation;
import android.app.usage.NetworkStats;
@@ -69,13 +70,16 @@
import android.util.Log;
import androidx.test.InstrumentationRegistry;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.compatibility.common.util.ShellIdentityUtils;
import com.android.compatibility.common.util.SystemUtil;
import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
import com.android.testutils.ConnectivityModuleTest;
import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.RecorderCallback.CallbackEntry;
+import com.android.testutils.TestableNetworkCallback;
import org.junit.After;
import org.junit.Before;
@@ -96,12 +100,18 @@
import java.util.Set;
import java.util.concurrent.TimeUnit;
-@ConnectivityModuleTest
+// TODO: Fix thread leaks in testCallback and annotating with @MonitorThreadLeak.
@AppModeFull(reason = "instant apps cannot be granted USAGE_STATS")
-@RunWith(AndroidJUnit4.class)
+@ConnectivityModuleTest
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+@RunWith(DevSdkIgnoreRunner.class)
public class NetworkStatsManagerTest {
- @Rule
+ @Rule(order = 1)
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(Build.VERSION_CODES.Q);
+ @Rule(order = 2)
+ public final AutoReleaseNetworkCallbackRule
+ networkCallbackRule = new AutoReleaseNetworkCallbackRule();
+
private static final String LOG_TAG = "NetworkStatsManagerTest";
private static final String APPOPS_SET_SHELL_COMMAND = "appops set --user {0} {1} {2} {3}";
@@ -116,14 +126,23 @@
private static final int NETWORK_TAG = 0xf00d;
private static final long THRESHOLD_BYTES = 2 * 1024 * 1024; // 2 MB
+ private static final long SHORT_TOLERANCE = MINUTE / 2;
+ private static final long LONG_TOLERANCE = MINUTE * 120;
private abstract class NetworkInterfaceToTest {
+
+ final TestableNetworkCallback mRequestNetworkCb = new TestableNetworkCallback();
private boolean mMetered;
private boolean mRoaming;
private boolean mIsDefault;
abstract int getNetworkType();
- abstract int getTransportType();
+
+ abstract Network requestNetwork();
+
+ void unrequestNetwork() {
+ networkCallbackRule.unregisterNetworkCallback(mRequestNetworkCb);
+ }
public boolean getMetered() {
return mMetered;
@@ -150,7 +169,13 @@
}
abstract String getSystemFeature();
- abstract String getErrorMessage();
+
+ @NonNull NetworkRequest buildRequestForTransport(int transport) {
+ return new NetworkRequest.Builder()
+ .addTransportType(transport)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ }
}
private final NetworkInterfaceToTest[] mNetworkInterfacesToTest =
@@ -162,19 +187,20 @@
}
@Override
- public int getTransportType() {
- return NetworkCapabilities.TRANSPORT_WIFI;
+ public Network requestNetwork() {
+ networkCallbackRule.requestNetwork(buildRequestForTransport(
+ NetworkCapabilities.TRANSPORT_WIFI),
+ mRequestNetworkCb, TIMEOUT_MILLIS);
+ return mRequestNetworkCb.expect(CallbackEntry.AVAILABLE,
+ "Wifi network not available. "
+ + "Please ensure the device has working wifi."
+ ).getNetwork();
}
@Override
public String getSystemFeature() {
return PackageManager.FEATURE_WIFI;
}
-
- @Override
- public String getErrorMessage() {
- return " Please make sure you are connected to a WiFi access point.";
- }
},
new NetworkInterfaceToTest() {
@Override
@@ -183,22 +209,20 @@
}
@Override
- public int getTransportType() {
- return NetworkCapabilities.TRANSPORT_CELLULAR;
+ public Network requestNetwork() {
+ networkCallbackRule.requestNetwork(buildRequestForTransport(
+ NetworkCapabilities.TRANSPORT_CELLULAR),
+ mRequestNetworkCb, TIMEOUT_MILLIS);
+ return mRequestNetworkCb.expect(CallbackEntry.AVAILABLE,
+ "Cell network not available. "
+ + "Please ensure the device has working mobile data."
+ ).getNetwork();
}
@Override
public String getSystemFeature() {
return PackageManager.FEATURE_TELEPHONY;
}
-
- @Override
- public String getErrorMessage() {
- return " Please make sure you have added a SIM card with data plan to"
- + " your phone, have enabled data over cellular and in case of"
- + " dual SIM devices, have selected the right SIM "
- + "for data connection.";
- }
}
};
@@ -214,7 +238,22 @@
private String mWriteSettingsMode;
private String mUsageStatsMode;
- private void exerciseRemoteHost(Network network, URL url) throws Exception {
+ // The test host only has IPv4. So on a dual-stack network where IPv6 connects before IPv4,
+ // we need to wait until IPv4 is available or the test will spuriously fail.
+ private static void waitForHostResolution(@NonNull Network network, @NonNull URL url) {
+ for (int i = 0; i < HOST_RESOLUTION_RETRIES; i++) {
+ try {
+ network.getAllByName(url.getHost());
+ return;
+ } catch (UnknownHostException e) {
+ SystemClock.sleep(HOST_RESOLUTION_INTERVAL_MS);
+ }
+ }
+ fail(String.format("%s could not be resolved on network %s (%d attempts %dms apart)",
+ url.getHost(), network, HOST_RESOLUTION_RETRIES, HOST_RESOLUTION_INTERVAL_MS));
+ }
+
+ private void exerciseRemoteHost(@NonNull Network network, @NonNull URL url) throws Exception {
NetworkInfo networkInfo = mCm.getNetworkInfo(network);
if (networkInfo == null) {
Log.w(LOG_TAG, "Network info is null");
@@ -312,99 +351,44 @@
return result.contains("FOREGROUND");
}
- private class NetworkCallback extends ConnectivityManager.NetworkCallback {
- private long mTolerance;
- private URL mUrl;
- public boolean success;
- public boolean metered;
- public boolean roaming;
- public boolean isDefault;
-
- NetworkCallback(long tolerance, URL url) {
- mTolerance = tolerance;
- mUrl = url;
- success = false;
- metered = false;
- roaming = false;
- isDefault = false;
- }
-
- // The test host only has IPv4. So on a dual-stack network where IPv6 connects before IPv4,
- // we need to wait until IPv4 is available or the test will spuriously fail.
- private void waitForHostResolution(Network network) {
- for (int i = 0; i < HOST_RESOLUTION_RETRIES; i++) {
- try {
- network.getAllByName(mUrl.getHost());
- return;
- } catch (UnknownHostException e) {
- SystemClock.sleep(HOST_RESOLUTION_INTERVAL_MS);
- }
- }
- fail(String.format("%s could not be resolved on network %s (%d attempts %dms apart)",
- mUrl.getHost(), network, HOST_RESOLUTION_RETRIES, HOST_RESOLUTION_INTERVAL_MS));
- }
-
- @Override
- public void onAvailable(Network network) {
- try {
- mStartTime = System.currentTimeMillis() - mTolerance;
- isDefault = network.equals(mCm.getActiveNetwork());
- waitForHostResolution(network);
- exerciseRemoteHost(network, mUrl);
- mEndTime = System.currentTimeMillis() + mTolerance;
- success = true;
- metered = !mCm.getNetworkCapabilities(network)
- .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
- roaming = !mCm.getNetworkCapabilities(network)
- .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
- synchronized (NetworkStatsManagerTest.this) {
- NetworkStatsManagerTest.this.notify();
- }
- } catch (Exception e) {
- Log.w(LOG_TAG, "exercising remote host failed.", e);
- success = false;
- }
- }
+ private boolean shouldTestThisNetworkType(int networkTypeIndex) {
+ return mPm.hasSystemFeature(mNetworkInterfacesToTest[networkTypeIndex].getSystemFeature());
}
- private boolean shouldTestThisNetworkType(int networkTypeIndex, final long tolerance)
- throws Exception {
- boolean hasFeature = mPm.hasSystemFeature(
- mNetworkInterfacesToTest[networkTypeIndex].getSystemFeature());
- if (!hasFeature) {
- return false;
- }
- NetworkCallback callback = new NetworkCallback(tolerance, new URL(CHECK_CONNECTIVITY_URL));
- mCm.requestNetwork(new NetworkRequest.Builder()
- .addTransportType(mNetworkInterfacesToTest[networkTypeIndex].getTransportType())
- .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
- .build(), callback);
- synchronized (this) {
- long now = System.currentTimeMillis();
- final long deadline = (long) (now + TIMEOUT_MILLIS * 2.4);
- while (!callback.success && now < deadline) {
- try {
- wait(deadline - now);
- } catch (InterruptedException e) {
- }
- now = System.currentTimeMillis();
- }
- }
- mCm.unregisterNetworkCallback(callback);
- if (callback.success) {
- mNetworkInterfacesToTest[networkTypeIndex].setMetered(callback.metered);
- mNetworkInterfacesToTest[networkTypeIndex].setRoaming(callback.roaming);
- mNetworkInterfacesToTest[networkTypeIndex].setIsDefault(callback.isDefault);
- return true;
- }
+ @NonNull
+ private Network requestNetworkAndSetAttributes(
+ @NonNull NetworkInterfaceToTest networkInterface) {
+ final Network network = networkInterface.requestNetwork();
- // This will always fail at this point as we know 'hasFeature' is true.
- assertFalse(mNetworkInterfacesToTest[networkTypeIndex].getSystemFeature()
- + " is a reported system feature, "
- + "however no corresponding connected network interface was found or the attempt "
- + "to connect and read has timed out (timeout = " + (TIMEOUT_MILLIS * 2) + "ms)."
- + mNetworkInterfacesToTest[networkTypeIndex].getErrorMessage(), hasFeature);
- return false;
+ // These attributes are needed when performing NetworkStats queries.
+ // Fetch caps from the first capabilities changed event since the
+ // interested attributes are not mutable, and not expected to be
+ // changed during the test.
+ final NetworkCapabilities caps = networkInterface.mRequestNetworkCb.expect(
+ CallbackEntry.NETWORK_CAPS_UPDATED, network).getCaps();
+ networkInterface.setMetered(!caps.hasCapability(
+ NetworkCapabilities.NET_CAPABILITY_NOT_METERED));
+ networkInterface.setRoaming(!caps.hasCapability(
+ NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING));
+ networkInterface.setIsDefault(network.equals(mCm.getActiveNetwork()));
+
+ return network;
+ }
+
+ private void requestNetworkAndGenerateTraffic(int networkTypeIndex, final long tolerance)
+ throws Exception {
+ final NetworkInterfaceToTest networkInterface = mNetworkInterfacesToTest[networkTypeIndex];
+ final Network network = requestNetworkAndSetAttributes(networkInterface);
+
+ mStartTime = System.currentTimeMillis() - tolerance;
+ waitForHostResolution(network, new URL(CHECK_CONNECTIVITY_URL));
+ exerciseRemoteHost(network, new URL(CHECK_CONNECTIVITY_URL));
+ mEndTime = System.currentTimeMillis() + tolerance;
+
+ // It is fine if the test fails and this line is not reached.
+ // The AutoReleaseNetworkCallbackRule will eventually release
+ // all unwanted callbacks.
+ networkInterface.unrequestNetwork();
}
private String getSubscriberId(int networkIndex) {
@@ -420,9 +404,10 @@
@Test
public void testDeviceSummary() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
- if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
+ if (!shouldTestThisNetworkType(i)) {
continue;
}
+ requestNetworkAndGenerateTraffic(i, SHORT_TOLERANCE);
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow");
NetworkStats.Bucket bucket = null;
try {
@@ -456,9 +441,10 @@
@Test
public void testUserSummary() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
- if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
+ if (!shouldTestThisNetworkType(i)) {
continue;
}
+ requestNetworkAndGenerateTraffic(i, SHORT_TOLERANCE);
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow");
NetworkStats.Bucket bucket = null;
try {
@@ -492,14 +478,15 @@
@Test
public void testAppSummary() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
+ if (!shouldTestThisNetworkType(i)) {
+ continue;
+ }
// Use tolerance value that large enough to make sure stats of at
// least one bucket is included. However, this is possible that
// the test will see data of different app but with the same UID
// that created before testing.
// TODO: Consider query stats before testing and use the difference to verify.
- if (!shouldTestThisNetworkType(i, MINUTE * 120)) {
- continue;
- }
+ requestNetworkAndGenerateTraffic(i, LONG_TOLERANCE);
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow");
NetworkStats result = null;
try {
@@ -568,10 +555,11 @@
@Test
public void testAppDetails() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
- // Relatively large tolerance to accommodate for history bucket size.
- if (!shouldTestThisNetworkType(i, MINUTE * 120)) {
+ if (!shouldTestThisNetworkType(i)) {
continue;
}
+ // Relatively large tolerance to accommodate for history bucket size.
+ requestNetworkAndGenerateTraffic(i, LONG_TOLERANCE);
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow");
NetworkStats result = null;
try {
@@ -612,9 +600,10 @@
public void testUidDetails() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
// Relatively large tolerance to accommodate for history bucket size.
- if (!shouldTestThisNetworkType(i, MINUTE * 120)) {
+ if (!shouldTestThisNetworkType(i)) {
continue;
}
+ requestNetworkAndGenerateTraffic(i, LONG_TOLERANCE);
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow");
NetworkStats result = null;
try {
@@ -666,9 +655,10 @@
public void testTagDetails() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
// Relatively large tolerance to accommodate for history bucket size.
- if (!shouldTestThisNetworkType(i, MINUTE * 120)) {
+ if (!shouldTestThisNetworkType(i)) {
continue;
}
+ requestNetworkAndGenerateTraffic(i, LONG_TOLERANCE);
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow");
NetworkStats result = null;
try {
@@ -772,10 +762,11 @@
@Test
public void testUidTagStateDetails() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
- // Relatively large tolerance to accommodate for history bucket size.
- if (!shouldTestThisNetworkType(i, MINUTE * 120)) {
+ if (!shouldTestThisNetworkType(i)) {
continue;
}
+ // Relatively large tolerance to accommodate for history bucket size.
+ requestNetworkAndGenerateTraffic(i, LONG_TOLERANCE);
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow");
NetworkStats result = null;
try {
@@ -850,9 +841,10 @@
public void testCallback() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
// Relatively large tolerance to accommodate for history bucket size.
- if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
+ if (!shouldTestThisNetworkType(i)) {
continue;
}
+ requestNetworkAndGenerateTraffic(i, SHORT_TOLERANCE);
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow");
TestUsageCallback usageCallback = new TestUsageCallback();
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
index 621af23..aad072c 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
@@ -19,7 +19,6 @@
import android.Manifest.permission.MANAGE_TEST_NETWORKS
import android.Manifest.permission.NETWORK_SETTINGS
import android.content.Context
-import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.EthernetManager
import android.net.InetAddresses
@@ -36,7 +35,6 @@
import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_DISCOVER
import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_REQUEST
import android.net.dhcp.DhcpRequestPacket
-import android.os.Build
import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import androidx.test.platform.app.InstrumentationRegistry
@@ -44,11 +42,11 @@
import com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress
import com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address
import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
-import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.AutoCloseTestInterfaceRule
import com.android.testutils.DhcpClientPacketFilter
import com.android.testutils.DhcpOptionFilter
import com.android.testutils.RecorderCallback.CallbackEntry
-import com.android.testutils.TapPacketReader
+import com.android.testutils.PollPacketReader
import com.android.testutils.TestHttpServer
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.runAsShell
@@ -79,10 +77,6 @@
@AppModeFull(reason = "Instant apps cannot create test networks")
@RunWith(AndroidJUnit4::class)
class NetworkValidationTest {
- @JvmField
- @Rule
- val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
-
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val tnm by lazy { context.assertHasService(TestNetworkManager::class.java) }
private val eth by lazy { context.assertHasService(EthernetManager::class.java) }
@@ -99,31 +93,32 @@
private val ethRequestCb = TestableNetworkCallback()
private lateinit var iface: TestNetworkInterface
- private lateinit var reader: TapPacketReader
+ private lateinit var reader: PollPacketReader
private lateinit var capportUrl: Uri
private var testSkipped = false
+ @get:Rule
+ val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
@Before
fun setUp() {
// This test requires using a tap interface as an ethernet interface.
- val pm = context.getPackageManager()
- testSkipped = !pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET) &&
- context.getSystemService(EthernetManager::class.java) == null
+ testSkipped = context.getSystemService(EthernetManager::class.java) == null
assumeFalse(testSkipped)
// Register a request so the network does not get torn down
cm.requestNetwork(ethRequest, ethRequestCb)
runAsShell(NETWORK_SETTINGS, MANAGE_TEST_NETWORKS) {
eth.setIncludeTestInterfaces(true)
- // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
- // does not go out of scope, which would cause it to close the underlying FileDescriptor
- // in its finalizer.
- iface = tnm.createTapInterface()
}
+ // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
+ // does not go out of scope, which would cause it to close the underlying FileDescriptor
+ // in its finalizer.
+ iface = testInterfaceRule.createTapInterface()
handlerThread.start()
- reader = TapPacketReader(
+ reader = PollPacketReader(
handlerThread.threadHandler,
iface.fileDescriptor.fileDescriptor,
MAX_PACKET_LENGTH)
@@ -147,8 +142,6 @@
handlerThread.threadHandler.post { reader.stop() }
handlerThread.quitSafely()
handlerThread.join()
-
- iface.fileDescriptor.close()
}
@Test
@@ -225,7 +218,7 @@
TEST_MTU, false /* rapidCommit */, capportUrl.toString())
}
-private fun <T : DhcpPacket> TapPacketReader.assertDhcpPacketReceived(
+private fun <T : DhcpPacket> PollPacketReader.assertDhcpPacketReceived(
packetType: Class<T>,
timeoutMs: Long,
type: Byte
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
index a0b40aa..d3d4f4d 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -17,6 +17,7 @@
package android.net.cts
import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
import com.android.net.module.util.NetworkStackConstants
@@ -33,7 +34,7 @@
* Clear the test network validation URLs.
*/
@JvmStatic fun clearValidationTestUrlsDeviceConfig() {
- runAsShell(WRITE_DEVICE_CONFIG) {
+ runAsShell(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG) {
DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL, null, false)
DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index f45f881..24af42b 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -17,19 +17,21 @@
import android.net.EthernetTetheringTestBase
import android.net.LinkAddress
-import android.net.TestNetworkInterface
import android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL
import android.net.TetheringManager.TETHERING_ETHERNET
import android.net.TetheringManager.TetheringRequest
+import android.net.cts.util.EthernetTestInterface
import android.net.nsd.NsdManager
import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import androidx.test.filters.SmallTest
+import com.android.testutils.AutoCloseTestInterfaceRule
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.NsdDiscoveryRecord
-import com.android.testutils.TapPacketReader
import com.android.testutils.pollForQuery
import com.android.testutils.tryTest
import java.util.Random
@@ -38,9 +40,12 @@
import org.junit.After
import org.junit.Assume.assumeFalse
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+private const val TAG = "NsdManagerDownstreamTetheringTest"
+
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@ConnectivityModuleTest
@@ -50,14 +55,29 @@
private val nsdManager by lazy { context.getSystemService(NsdManager::class.java)!! }
private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
+ private val handlerThread = HandlerThread("$TAG thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+ private lateinit var downstreamIface: EthernetTestInterface
+ private var tetheringEventCallback: MyTetheringEventCallback? = null
+
+ @get:Rule
+ val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
@Before
override fun setUp() {
super.setUp()
- setIncludeTestInterfaces(true)
+ val iface = testInterfaceRule.createTapInterface()
+ downstreamIface = EthernetTestInterface(context, handler, iface)
}
@After
override fun tearDown() {
+ if (::downstreamIface.isInitialized) {
+ downstreamIface.destroy()
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ maybeUnregisterTetheringEventCallback(tetheringEventCallback)
super.tearDown()
}
@@ -65,16 +85,11 @@
fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
assumeFalse(isInterfaceForTetheringAvailable())
- var downstreamIface: TestNetworkInterface? = null
- var tetheringEventCallback: MyTetheringEventCallback? = null
- var downstreamReader: TapPacketReader? = null
-
val discoveryRecord = NsdDiscoveryRecord()
tryTest {
- downstreamIface = createTestInterface()
val iface = mTetheredInterfaceRequester.getInterface()
- assertEquals(iface, downstreamIface?.interfaceName)
+ assertEquals(downstreamIface.name, iface)
val request = TetheringRequest.Builder(TETHERING_ETHERNET)
.setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
tetheringEventCallback = enableEthernetTethering(
@@ -83,23 +98,15 @@
).apply {
awaitInterfaceLocalOnly()
}
- // This shouldn't be flaky because the TAP interface will buffer all packets even
- // before the reader is started.
- downstreamReader = makePacketReader(downstreamIface)
+ val downstreamReader = downstreamIface.packetReader
waitForRouterAdvertisement(downstreamReader, iface, WAIT_RA_TIMEOUT_MS)
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
- assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+ assertNotNull(downstreamReader.pollForQuery("$serviceType.local", 12 /* type PTR */))
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
- } cleanupStep {
- maybeStopTapPacketReader(downstreamReader)
- } cleanupStep {
- maybeCloseTestInterface(downstreamIface)
- } cleanup {
- maybeUnregisterTetheringEventCallback(tetheringEventCallback)
}
}
@@ -107,16 +114,11 @@
fun testMdnsDiscoveryWorkOnTetheringInterface() {
assumeFalse(isInterfaceForTetheringAvailable())
- var downstreamIface: TestNetworkInterface? = null
- var tetheringEventCallback: MyTetheringEventCallback? = null
- var downstreamReader: TapPacketReader? = null
-
val discoveryRecord = NsdDiscoveryRecord()
tryTest {
- downstreamIface = createTestInterface()
val iface = mTetheredInterfaceRequester.getInterface()
- assertEquals(iface, downstreamIface?.interfaceName)
+ assertEquals(downstreamIface.name, iface)
val localAddr = LinkAddress("192.0.2.3/28")
val clientAddr = LinkAddress("192.0.2.2/28")
@@ -130,23 +132,14 @@
awaitInterfaceTethered()
}
- val fd = downstreamIface?.fileDescriptor?.fileDescriptor
- assertNotNull(fd)
- downstreamReader = makePacketReader(fd, getMTU(downstreamIface))
-
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
- assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+ val downstreamReader = downstreamIface.packetReader
+ assertNotNull(downstreamReader.pollForQuery("$serviceType.local", 12 /* type PTR */))
// TODO: Add another test to check packet reply can trigger serviceFound.
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
- } cleanupStep {
- maybeStopTapPacketReader(downstreamReader)
- } cleanupStep {
- maybeCloseTestInterface(downstreamIface)
- } cleanup {
- maybeUnregisterTetheringEventCallback(tetheringEventCallback)
}
}
}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index be80787..7fc8863 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -100,12 +100,11 @@
import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
-import com.android.testutils.TapPacketReader
+import com.android.testutils.PollPacketReader
import com.android.testutils.TestDnsPacket
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.assertContainsExactly
import com.android.testutils.assertEmpty
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33
@@ -128,8 +127,10 @@
import java.util.Random
import java.util.concurrent.Executor
import kotlin.math.min
+import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
@@ -142,10 +143,10 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import kotlin.test.assertNotEquals
private const val TAG = "NsdManagerTest"
private const val TIMEOUT_MS = 2000L
+
// Registration may take a long time if there are devices with the same hostname on the network,
// as the device needs to try another name and probe again. This is especially true since when using
// mdnsresponder the usual hostname is "Android", and on conflict "Android-2", "Android-3", ... are
@@ -187,11 +188,12 @@
private val customHostname = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
private val customHostname2 = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
private val publicKey = hexStringToByteArray(
- "0201030dc141d0637960b98cbc12cfca"
- + "221d2879dac26ee5b460e9007c992e19"
- + "02d897c391b03764d448f7d0c772fdb0"
- + "3b1d9d6d52ff8886769e8e2362513565"
- + "270962d3")
+ "0201030dc141d0637960b98cbc12cfca" +
+ "221d2879dac26ee5b460e9007c992e19" +
+ "02d897c391b03764d448f7d0c772fdb0" +
+ "3b1d9d6d52ff8886769e8e2362513565" +
+ "270962d3"
+ )
private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
private val ctsNetUtils by lazy{ CtsNetUtils(context) }
@@ -243,11 +245,14 @@
val iface = tnm.createTapInterface()
val cb = TestableNetworkCallback()
val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
- cm.requestNetwork(NetworkRequest.Builder()
+ cm.requestNetwork(
+ NetworkRequest.Builder()
.removeCapability(NET_CAPABILITY_TRUSTED)
.addTransportType(TRANSPORT_TEST)
.setNetworkSpecifier(testNetworkSpecifier)
- .build(), cb)
+ .build(),
+ cb
+ )
val agent = registerTestNetworkAgent(iface.interfaceName)
val network = agent.network ?: fail("Registered agent should have a network")
@@ -267,12 +272,17 @@
val lp = LinkProperties().apply {
interfaceName = ifaceName
}
- val agent = TestableNetworkAgent(context, handlerThread.looper,
+ val agent = TestableNetworkAgent(
+ context,
+ handlerThread.looper,
NetworkCapabilities().apply {
removeCapability(NET_CAPABILITY_TRUSTED)
addTransportType(TRANSPORT_TEST)
setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
- }, lp, NetworkAgentConfig.Builder().build())
+ },
+ lp,
+ NetworkAgentConfig.Builder().build()
+ )
val network = agent.register()
agent.markConnected()
agent.expectCallback<OnNetworkCreated>()
@@ -316,6 +326,15 @@
it.port = TEST_PORT
}
+ private fun makePacketReader(network: TestTapNetwork = testNetwork1) = PollPacketReader(
+ Handler(handlerThread.looper),
+ network.iface.fileDescriptor.fileDescriptor,
+ 1500 /* maxPacketSize */
+ ).also {
+ it.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+
@After
fun tearDown() {
runAsShell(MANAGE_TEST_NETWORKS) {
@@ -346,15 +365,19 @@
Triple(null, null, "null key"),
Triple("", null, "empty key"),
Triple(string256, null, "key with 256 characters"),
- Triple("key", string256.substring(3),
- "key+value combination with more than 255 characters"),
+ Triple(
+ "key",
+ string256.substring(3),
+ "key+value combination with more than 255 characters"
+ ),
Triple("key", string256.substring(4), "key+value combination with 255 characters"),
Triple("\u0019", null, "key with invalid character"),
Triple("=", null, "key with invalid character"),
Triple("\u007f", null, "key with invalid character")
).forEach {
assertFailsWith<IllegalArgumentException>(
- "Setting invalid ${it.third} unexpectedly succeeded") {
+ "Setting invalid ${it.third} unexpectedly succeeded"
+ ) {
si.setAttribute(it.first, it.second)
}
}
@@ -377,7 +400,8 @@
// Test registering without an Executor
nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
val registeredInfo = registrationRecord.expectCallback<ServiceRegistered>(
- REGISTRATION_TIMEOUT_MS).serviceInfo
+ REGISTRATION_TIMEOUT_MS
+ ).serviceInfo
// Only service name is included in ServiceRegistered callbacks
assertNull(registeredInfo.serviceType)
@@ -392,7 +416,9 @@
// Expect a service record to be discovered
val foundInfo = discoveryRecord.waitForServiceDiscovered(
- registeredInfo.serviceName, serviceType)
+ registeredInfo.serviceName,
+ serviceType
+ )
// Test resolving without an Executor
val resolveRecord = NsdResolveRecord()
@@ -407,8 +433,10 @@
assertNull(resolvedService.attributes["booleanAttr"])
assertEquals("value", resolvedService.attributes["keyValueAttr"].utf8ToString())
assertEquals("=", resolvedService.attributes["keyEqualsAttr"].utf8ToString())
- assertEquals(" value ",
- resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString())
+ assertEquals(
+ " value ",
+ resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString()
+ )
assertEquals(string256.substring(9), resolvedService.attributes["longkey"].utf8ToString())
assertArrayEquals(testByteArray, resolvedService.attributes["binaryDataAttr"])
assertTrue(resolvedService.attributes.containsKey("nullBinaryDataAttr"))
@@ -442,12 +470,15 @@
val registrationRecord2 = NsdRegistrationRecord()
nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, registrationRecord2)
val registeredInfo2 = registrationRecord2.expectCallback<ServiceRegistered>(
- REGISTRATION_TIMEOUT_MS).serviceInfo
+ REGISTRATION_TIMEOUT_MS
+ ).serviceInfo
// Expect a service record to be discovered (and filter the ones
// that are unrelated to this test)
val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
- registeredInfo2.serviceName, serviceType)
+ registeredInfo2.serviceName,
+ serviceType
+ )
// Resolve the service
val resolveRecord2 = NsdResolveRecord()
@@ -950,7 +981,8 @@
// when the compat change is disabled.
// Note that before T the compat constant had a different int value.
assertFalse(CompatChanges.isChangeEnabled(
- ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER))
+ ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER
+ ))
}
@Test
@@ -1023,7 +1055,9 @@
// onStopResolutionFailed on the record directly then verify it is received.
val resolveRecord = NsdResolveRecord()
resolveRecord.onStopResolutionFailed(
- NsdServiceInfo(), NsdManager.FAILURE_OPERATION_NOT_RUNNING)
+ NsdServiceInfo(),
+ NsdManager.FAILURE_OPERATION_NOT_RUNNING
+ )
val failedCb = resolveRecord.expectCallback<StopResolutionFailed>()
assertEquals(NsdManager.FAILURE_OPERATION_NOT_RUNNING, failedCb.errorCode)
}
@@ -1273,16 +1307,16 @@
assumeTrue(TestUtils.shouldTestTApis())
val si = makeTestServiceInfo(testNetwork1.network)
-
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
- nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
- registrationRecord)
+ nsdManager.registerService(
+ si,
+ NsdManager.PROTOCOL_DNS_SD,
+ { it.run() },
+ registrationRecord
+ )
tryTest {
assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1313,15 +1347,16 @@
parseNumericAddress("2001:db8::3"))
}
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
- nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
- registrationRecord)
+ nsdManager.registerService(
+ si,
+ NsdManager.PROTOCOL_DNS_SD,
+ { it.run() },
+ registrationRecord
+ )
tryTest {
assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1352,15 +1387,16 @@
hostname = customHostname
}
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
- nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
- registrationRecord)
+ nsdManager.registerService(
+ si,
+ NsdManager.PROTOCOL_DNS_SD,
+ { it.run() },
+ registrationRecord
+ )
tryTest {
assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1392,10 +1428,7 @@
val registrationRecord = NsdRegistrationRecord()
val discoveryRecord = NsdDiscoveryRecord()
val registeredService = registerService(registrationRecord, si)
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
tryTest {
assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
@@ -1469,11 +1502,7 @@
val registrationRecord = NsdRegistrationRecord()
val discoveryRecord = NsdDiscoveryRecord()
val registeredService = registerService(registrationRecord, si)
- val packetReader = TapPacketReader(
- Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
tryTest {
assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
@@ -1501,11 +1530,11 @@
val newRegistration =
registrationRecord
.expectCallbackEventually<ServiceRegistered>(REGISTRATION_TIMEOUT_MS) {
- it.serviceInfo.serviceName == serviceName
- && it.serviceInfo.hostname.let { hostname ->
- hostname != null
- && hostname.startsWith(customHostname)
- && hostname != customHostname
+ it.serviceInfo.serviceName == serviceName &&
+ it.serviceInfo.hostname.let { hostname ->
+ hostname != null &&
+ hostname.startsWith(customHostname) &&
+ hostname != customHostname
}
}
@@ -1536,10 +1565,7 @@
val registrationRecord = NsdRegistrationRecord()
val discoveryRecord = NsdDiscoveryRecord()
val registeredService = registerService(registrationRecord, si)
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
tryTest {
assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
@@ -1576,13 +1602,15 @@
fun testDiscoveryWithPtrOnlyResponse_ServiceIsFound() {
// Register service on testNetwork1
val discoveryRecord = NsdDiscoveryRecord()
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
- testNetwork1.network, { it.run() }, discoveryRecord)
+ nsdManager.discoverServices(
+ serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network,
+ { it.run() },
+ discoveryRecord
+ )
tryTest {
discoveryRecord.expectCallback<DiscoveryStarted>()
@@ -1613,9 +1641,12 @@
assertEmpty(it.hostAddresses)
assertEquals(0, it.attributes.size)
}
- } cleanup {
+ } cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
}
@@ -1626,69 +1657,77 @@
fun testResolveWhenServerSendsNoAdditionalRecord() {
// Resolve service on testNetwork1
val resolveRecord = NsdResolveRecord()
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */
- )
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
-
+ val packetReader = makePacketReader()
val si = makeTestServiceInfo(testNetwork1.network)
nsdManager.resolveService(si, { it.run() }, resolveRecord)
- val serviceFullName = "$serviceName.$serviceType.local"
- // The query should ask for ANY, since both SRV and TXT are requested. Note legacy
- // mdnsresponder will ask for SRV and TXT separately, and will not proceed to asking for
- // address records without an answer for both.
- val srvTxtQuery = packetReader.pollForQuery(serviceFullName, DnsResolver.TYPE_ANY)
- assertNotNull(srvTxtQuery)
+ tryTest {
+ val serviceFullName = "$serviceName.$serviceType.local"
+ // The query should ask for ANY, since both SRV and TXT are requested. Note legacy
+ // mdnsresponder will ask for SRV and TXT separately, and will not proceed to asking for
+ // address records without an answer for both.
+ val srvTxtQuery = packetReader.pollForQuery(serviceFullName, DnsResolver.TYPE_ANY)
+ assertNotNull(srvTxtQuery)
- /*
- Generated with:
- scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
- scapy.DNSRRSRV(rrname='NsdTest123456789._nmt123456789._tcp.local',
- rclass=0x8001, port=31234, target='testhost.local', ttl=120) /
- scapy.DNSRR(rrname='NsdTest123456789._nmt123456789._tcp.local', type='TXT', ttl=120,
- rdata='testkey=testvalue')
- ))).hex()
- */
- val srvTxtResponsePayload = HexDump.hexStringToByteArray("000084000000000200000000104" +
- "e7364546573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f6" +
- "3616c0000218001000000780011000000007a020874657374686f7374c030c00c00100001000" +
- "00078001211746573746b65793d7465737476616c7565")
- replaceServiceNameAndTypeWithTestSuffix(srvTxtResponsePayload)
- packetReader.sendResponse(buildMdnsPacket(srvTxtResponsePayload))
+ /*
+ Generated with:
+ scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+ scapy.DNSRRSRV(rrname='NsdTest123456789._nmt123456789._tcp.local',
+ rclass=0x8001, port=31234, target='testhost.local', ttl=120) /
+ scapy.DNSRR(rrname='NsdTest123456789._nmt123456789._tcp.local', type='TXT', ttl=120,
+ rdata='testkey=testvalue')
+ ))).hex()
+ */
+ val srvTxtResponsePayload = HexDump.hexStringToByteArray(
+ "000084000000000200000000104" +
+ "e7364546573743132333435363738390d5f6e6d74313233343536373839045f7463" +
+ "70056c6f63616c0000218001000000780011000000007a020874657374686f7374c" +
+ "030c00c0010000100000078001211746573746b65793d7465737476616c7565"
+ )
+ replaceServiceNameAndTypeWithTestSuffix(srvTxtResponsePayload)
+ packetReader.sendResponse(buildMdnsPacket(srvTxtResponsePayload))
- val testHostname = "testhost.local"
- val addressQuery = packetReader.pollForQuery(testHostname,
- DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA)
- assertNotNull(addressQuery)
+ val testHostname = "testhost.local"
+ val addressQuery = packetReader.pollForQuery(
+ testHostname,
+ DnsResolver.TYPE_A,
+ DnsResolver.TYPE_AAAA
+ )
+ assertNotNull(addressQuery)
- /*
- Generated with:
- scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
- scapy.DNSRR(rrname='testhost.local', type='A', ttl=120,
- rdata='192.0.2.123') /
- scapy.DNSRR(rrname='testhost.local', type='AAAA', ttl=120,
- rdata='2001:db8::123')
- ))).hex()
- */
- val addressPayload = HexDump.hexStringToByteArray("0000840000000002000000000874657374" +
- "686f7374056c6f63616c0000010001000000780004c000027bc00c001c000100000078001020" +
- "010db8000000000000000000000123")
- packetReader.sendResponse(buildMdnsPacket(addressPayload))
+ /*
+ Generated with:
+ scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+ scapy.DNSRR(rrname='testhost.local', type='A', ttl=120,
+ rdata='192.0.2.123') /
+ scapy.DNSRR(rrname='testhost.local', type='AAAA', ttl=120,
+ rdata='2001:db8::123')
+ ))).hex()
+ */
+ val addressPayload = HexDump.hexStringToByteArray(
+ "0000840000000002000000000874657374" +
+ "686f7374056c6f63616c0000010001000000780004c000027bc00c001c000100000" +
+ "078001020010db8000000000000000000000123"
+ )
+ packetReader.sendResponse(buildMdnsPacket(addressPayload))
- val serviceResolved = resolveRecord.expectCallback<ServiceResolved>()
- serviceResolved.serviceInfo.let {
- assertEquals(serviceName, it.serviceName)
- assertEquals(".$serviceType", it.serviceType)
- assertEquals(testNetwork1.network, it.network)
- assertEquals(31234, it.port)
- assertEquals(1, it.attributes.size)
- assertArrayEquals("testvalue".encodeToByteArray(), it.attributes["testkey"])
+ val serviceResolved = resolveRecord.expectCallback<ServiceResolved>()
+ serviceResolved.serviceInfo.let {
+ assertEquals(serviceName, it.serviceName)
+ assertEquals(".$serviceType", it.serviceType)
+ assertEquals(testNetwork1.network, it.network)
+ assertEquals(31234, it.port)
+ assertEquals(1, it.attributes.size)
+ assertArrayEquals("testvalue".encodeToByteArray(), it.attributes["testkey"])
+ }
+ assertEquals(
+ setOf(parseNumericAddress("192.0.2.123"), parseNumericAddress("2001:db8::123")),
+ serviceResolved.serviceInfo.hostAddresses.toSet()
+ )
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
- assertEquals(
- setOf(parseNumericAddress("192.0.2.123"), parseNumericAddress("2001:db8::123")),
- serviceResolved.serviceInfo.hostAddresses.toSet())
}
@Test
@@ -1702,20 +1741,16 @@
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
var nsResponder: NSResponder? = null
+ val packetReader = makePacketReader()
tryTest {
registerService(registrationRecord, si)
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
-
- handlerThread.waitForIdle(TIMEOUT_MS)
/*
Send a "query unicast" query.
Generated with:
scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR', qclass=0x8001)
)).hex()
- */
+ */
val mdnsPayload = HexDump.hexStringToByteArray("0000000000010000000000000d5f6e6d74313" +
"233343536373839045f746370056c6f63616c00000c8001")
replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
@@ -1733,10 +1768,13 @@
pkt.dstAddr == testSrcAddr
}
assertNotNull(reply)
- } cleanup {
+ } cleanupStep {
nsResponder?.stop()
nsdManager.unregisterService(registrationRecord)
registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
}
@@ -1752,13 +1790,9 @@
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
var nsResponder: NSResponder? = null
+ val packetReader = makePacketReader()
tryTest {
registerService(registrationRecord, si)
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
-
- handlerThread.waitForIdle(TIMEOUT_MS)
/*
Send a query with a known answer. Expect to receive a response containing TXT record
only.
@@ -1771,7 +1805,7 @@
an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
rdata='NsdTest123456789._nmt123456789._tcp.local')
)).hex()
- */
+ */
val query = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d74313233343" +
"536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
"d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d743132333" +
@@ -1806,7 +1840,7 @@
an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=2150,
rdata='NsdTest123456789._nmt123456789._tcp.local')
)).hex()
- */
+ */
val query2 = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d7431323334" +
"3536373839045f746370056c6f63616c00000c8001104e736454657374313233343536373839" +
"0d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d74313233" +
@@ -1823,10 +1857,13 @@
pkt.dstAddr == testSrcAddr
}
assertNotNull(reply2)
- } cleanup {
+ } cleanupStep {
nsResponder?.stop()
nsdManager.unregisterService(registrationRecord)
registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
}
@@ -1842,13 +1879,9 @@
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
var nsResponder: NSResponder? = null
+ val packetReader = makePacketReader()
tryTest {
registerService(registrationRecord, si)
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
-
- handlerThread.waitForIdle(TIMEOUT_MS)
/*
Send a query with truncated bit set.
Generated with:
@@ -1858,7 +1891,7 @@
scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
qclass=0x8001)
)).hex()
- */
+ */
val query = HexDump.hexStringToByteArray("0000020000020000000000000d5f6e6d74313233343" +
"536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
"d5f6e6d74313233343536373839045f746370056c6f63616c0000108001")
@@ -1870,7 +1903,7 @@
an = scapy.DNSRR(rrname='_test._tcp.local', type='PTR', ttl=4500,
rdata='NsdTest._test._tcp.local')
)).hex()
- */
+ */
val knownAnswer1 = HexDump.hexStringToByteArray("000002000000000100000000055f74657374" +
"045f746370056c6f63616c00000c000100001194001a074e736454657374055f74657374045f" +
"746370056c6f63616c00")
@@ -1882,7 +1915,7 @@
an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
rdata='NsdTest123456789._nmt123456789._tcp.local')
)).hex()
- */
+ */
val knownAnswer2 = HexDump.hexStringToByteArray("0000000000000001000000000d5f6e6d7431" +
"3233343536373839045f746370056c6f63616c00000c000100001194002b104e736454657374" +
"3132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
@@ -1904,10 +1937,13 @@
pkt.dstAddr == testSrcAddr
}
assertNotNull(reply)
- } cleanup {
+ } cleanupStep {
nsResponder?.stop()
nsdManager.unregisterService(registrationRecord)
registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
}
@@ -1919,13 +1955,15 @@
// Register service on testNetwork1
val discoveryRecord = NsdDiscoveryRecord()
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
- testNetwork1.network, { it.run() }, discoveryRecord)
+ nsdManager.discoverServices(
+ serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network,
+ { it.run() },
+ discoveryRecord
+ )
tryTest {
discoveryRecord.expectCallback<DiscoveryStarted>()
@@ -1963,9 +2001,12 @@
pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR)
}
assertNotNull(query)
- } cleanup {
+ } cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
}
@@ -1987,10 +2028,12 @@
val hostAddresses1 = listOf(
parseNumericAddress("192.0.2.23"),
parseNumericAddress("2001:db8::1"),
- parseNumericAddress("2001:db8::2"))
+ parseNumericAddress("2001:db8::2")
+ )
val hostAddresses2 = listOf(
parseNumericAddress("192.0.2.24"),
- parseNumericAddress("2001:db8::3"))
+ parseNumericAddress("2001:db8::3")
+ )
val si1 = NsdServiceInfo().also {
it.network = testNetwork1.network
it.serviceName = serviceName
@@ -2054,10 +2097,12 @@
val hostAddresses1 = listOf(
parseNumericAddress("192.0.2.23"),
parseNumericAddress("2001:db8::1"),
- parseNumericAddress("2001:db8::2"))
+ parseNumericAddress("2001:db8::2")
+ )
val hostAddresses2 = listOf(
parseNumericAddress("192.0.2.24"),
- parseNumericAddress("2001:db8::3"))
+ parseNumericAddress("2001:db8::3")
+ )
val si1 = NsdServiceInfo().also {
it.network = testNetwork1.network
it.hostname = customHostname
@@ -2105,7 +2150,8 @@
val hostAddresses = listOf(
parseNumericAddress("192.0.2.23"),
parseNumericAddress("2001:db8::1"),
- parseNumericAddress("2001:db8::2"))
+ parseNumericAddress("2001:db8::2")
+ )
val si1 = NsdServiceInfo().also {
it.network = testNetwork1.network
it.serviceType = serviceType
@@ -2270,11 +2316,7 @@
it.port = TEST_PORT
it.publicKey = publicKey
}
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
-
+ val packetReader = makePacketReader()
val registrationRecord = NsdRegistrationRecord()
val discoveryRecord = NsdDiscoveryRecord()
tryTest {
@@ -2306,8 +2348,11 @@
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<DiscoveryStopped>()
- } cleanup {
+ } cleanupStep {
nsdManager.unregisterService(registrationRecord)
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
}
@@ -2322,11 +2367,7 @@
parseNumericAddress("2001:db8::2"))
it.publicKey = publicKey
}
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
-
+ val packetReader = makePacketReader()
val registrationRecord = NsdRegistrationRecord()
tryTest {
registerService(registrationRecord, si)
@@ -2348,8 +2389,11 @@
it.nsType == DnsResolver.TYPE_A
}
assertEquals(3, addressRecords.size)
- } cleanup {
+ } cleanupStep {
nsdManager.unregisterService(registrationRecord)
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
}
@@ -2376,11 +2420,7 @@
it.hostAddresses = listOf()
it.publicKey = publicKey
}
- val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
-
+ val packetReader = makePacketReader()
val registrationRecord1 = NsdRegistrationRecord()
val registrationRecord2 = NsdRegistrationRecord()
tryTest {
@@ -2414,9 +2454,12 @@
assertTrue(keyRecords.any { it.dName == "$customHostname.local" })
assertTrue(keyRecords.all { it.ttl == NAME_RECORDS_TTL_MILLIS })
assertTrue(keyRecords.all { it.rr.contentEquals(publicKey) })
- } cleanup {
+ } cleanupStep {
nsdManager.unregisterService(registrationRecord1)
nsdManager.unregisterService(registrationRecord2)
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
}
@@ -2430,10 +2473,20 @@
val discoveryRecord1 = NsdDiscoveryRecord()
val discoveryRecord2 = NsdDiscoveryRecord()
val discoveryRecord3 = NsdDiscoveryRecord()
- nsdManager.discoverServices("_test1._tcp", NsdManager.PROTOCOL_DNS_SD,
- testNetwork1.network, { it.run() }, discoveryRecord1)
- nsdManager.discoverServices("_test2._tcp", NsdManager.PROTOCOL_DNS_SD,
- testNetwork1.network, { it.run() }, discoveryRecord2)
+ nsdManager.discoverServices(
+ "_test1._tcp",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network,
+ { it.run() },
+ discoveryRecord1
+ )
+ nsdManager.discoverServices(
+ "_test2._tcp",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network,
+ { it.run() },
+ discoveryRecord2
+ )
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord3)
tryTest {
@@ -2471,6 +2524,111 @@
}
}
+ @Test
+ fun testAdvertiseServiceWithNoAttributes_TxtRecordIstNotEmpty() {
+ deviceConfigRule.setConfig(
+ NAMESPACE_TETHERING,
+ "test_nsd_avoid_advertising_empty_txt_records",
+ "1"
+ )
+ val packetReader = makePacketReader()
+
+ // Test behavior described in RFC6763 6.1: empty TXT records are not allowed, but TXT
+ // records with a zero length string are equivalent.
+ val si = makeTestServiceInfo(testNetwork1.network)
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ registerService(registrationRecord, si)
+
+ tryTest {
+ val announcement =
+ packetReader.pollForReply("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT)
+ assertNotNull(announcement)
+ val txtRecords = announcement.records[ANSECTION].filter {
+ it.nsType == DnsResolver.TYPE_TXT
+ }
+ assertEquals(1, txtRecords.size)
+ // The TXT record should contain as single zero
+ assertContentEquals(byteArrayOf(0), txtRecords[0].rr)
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
+ private fun verifyCachedServicesRemoval(isCachedServiceRemoved: Boolean) {
+ val si = makeTestServiceInfo(testNetwork1.network)
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ registerService(registrationRecord, si)
+ // Register a discovery request.
+ val discoveryRecord = NsdDiscoveryRecord()
+ val packetReader = makePacketReader()
+
+ tryTest {
+ nsdManager.discoverServices(
+ serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network,
+ { it.run() },
+ discoveryRecord
+ )
+
+ discoveryRecord.expectCallback<DiscoveryStarted>()
+ val foundInfo = discoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ assertEquals(testNetwork1.network, foundInfo.network)
+ // Verify that the service is not in the cache (a query is sent).
+ assertNotNull(packetReader.pollForQuery(
+ "$serviceType.local", DnsResolver.TYPE_PTR, timeoutMs = 0L))
+
+ // Stop discovery to trigger the cached services removal process.
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+
+ val serviceFullName = "$serviceName.$serviceType.local"
+ if (isCachedServiceRemoved) {
+ Thread.sleep(100L)
+ resolveService(foundInfo)
+ // Verify the resolution query will send because cached services are remove after
+ // exceeding the retention time.
+ assertNotNull(packetReader.pollForQuery(
+ serviceFullName, DnsResolver.TYPE_ANY, timeoutMs = 0L))
+ } else {
+ resolveService(foundInfo)
+ // Verify the resolution query will not be sent because services are still cached.
+ assertNull(packetReader.pollForQuery(
+ serviceFullName, DnsResolver.TYPE_ANY, timeoutMs = 0L))
+ }
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
+ @Test
+ fun testRemoveCachedServices() {
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_cached_services_removal", "1")
+ verifyCachedServicesRemoval(isCachedServiceRemoved = false)
+ }
+
+ @Test
+ fun testRemoveCachedServices_ShortRetentionTime() {
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_cached_services_removal", "1")
+ deviceConfigRule.setConfig(
+ NAMESPACE_TETHERING,
+ "test_nsd_cached_services_retention_time",
+ "1"
+ )
+ verifyCachedServicesRemoval(isCachedServiceRemoved = true)
+ }
+
private fun hasServiceTypeClientsForNetwork(clients: List<String>, network: Network): Boolean {
return clients.any { client -> client.substring(
client.indexOf("network=") + "network=".length,
@@ -2489,11 +2647,16 @@
*/
private fun getServiceTypeClients(): List<String> {
return SystemUtil.runShellCommand(
- InstrumentationRegistry.getInstrumentation(), "dumpsys servicediscovery")
+ InstrumentationRegistry.getInstrumentation(),
+ "dumpsys servicediscovery"
+ )
.split("\n").mapNotNull { line ->
line.indexOf("ServiceTypeClient:").let { idx ->
- if (idx == -1) null
- else line.substring(idx)
+ if (idx == -1) {
+ null
+ } else {
+ line.substring(idx)
+ }
}
}
}
@@ -2506,9 +2669,11 @@
rclass=0x8001, port=31234, target='conflict.local', ttl=120)
)).hex()
*/
- val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000104e736454657" +
+ val mdnsPayload = HexDump.hexStringToByteArray(
+ "000084000000000100000000104e736454657" +
"3743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00002" +
- "18001000000780016000000007a0208636f6e666c696374056c6f63616c00")
+ "18001000000780016000000007a0208636f6e666c696374056c6f63616c00"
+ )
replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
return buildMdnsPacket(mdnsPayload)
@@ -2522,9 +2687,11 @@
rdata='2001:db8::321')
)).hex()
*/
- val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000144e7364" +
+ val mdnsPayload = HexDump.hexStringToByteArray(
+ "000084000000000100000000144e7364" +
"54657374486f7374313233343536373839056c6f63616c00001c000100000078001020010db80000" +
- "00000000000000000321")
+ "00000000000000000321"
+ )
replaceCustomHostnameWithTestSuffix(mdnsPayload)
return buildMdnsPacket(mdnsPayload)
@@ -2575,22 +2742,29 @@
mdnsPayload: ByteArray,
srcAddr: Inet6Address = testSrcAddr
): ByteBuffer {
- val packetBuffer = PacketBuilder.allocate(true /* hasEther */, IPPROTO_IPV6,
- IPPROTO_UDP, mdnsPayload.size)
+ val packetBuffer = PacketBuilder.allocate(
+ true /* hasEther */,
+ IPPROTO_IPV6,
+ IPPROTO_UDP,
+ mdnsPayload.size
+ )
val packetBuilder = PacketBuilder(packetBuffer)
// Multicast ethernet address for IPv6 to ff02::fb
val multicastEthAddr = MacAddress.fromBytes(
- byteArrayOf(0x33, 0x33, 0, 0, 0, 0xfb.toByte()))
+ byteArrayOf(0x33, 0x33, 0, 0, 0, 0xfb.toByte())
+ )
packetBuilder.writeL2Header(
MacAddress.fromBytes(byteArrayOf(1, 2, 3, 4, 5, 6)) /* srcMac */,
multicastEthAddr,
- ETH_P_IPV6.toShort())
+ ETH_P_IPV6.toShort()
+ )
packetBuilder.writeIpv6Header(
0x60000000, // version=6, traffic class=0x0, flowlabel=0x0
IPPROTO_UDP.toByte(),
64 /* hop limit */,
srcAddr,
- multicastIpv6Addr /* dstIp */)
+ multicastIpv6Addr /* dstIp */
+ )
packetBuilder.writeUdpHeader(MDNS_PORT /* srcPort */, MDNS_PORT /* dstPort */)
packetBuffer.put(mdnsPayload)
return packetBuilder.finalizePacket()
diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java
index 4d924d1..1588835 100644
--- a/tests/cts/net/src/android/net/cts/PacketUtils.java
+++ b/tests/cts/net/src/android/net/cts/PacketUtils.java
@@ -49,6 +49,7 @@
static final int TCP_HDRLEN = 20;
static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12;
static final int ESP_HDRLEN = 8;
+ static final int ICMP_HDRLEN = 8;
static final int ESP_BLK_SIZE = 4; // ESP has to be 4-byte aligned
static final int ESP_TRAILER_LEN = 2;
diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java
index 268d8d2..8bf4998 100644
--- a/tests/cts/net/src/android/net/cts/TunUtils.java
+++ b/tests/cts/net/src/android/net/cts/TunUtils.java
@@ -47,6 +47,8 @@
protected static final int IP4_PROTO_OFFSET = 9;
protected static final int IP6_PROTO_OFFSET = 6;
+ private static final int SEQ_NUM_MATCH_NOT_REQUIRED = -1;
+
private static final int DATA_BUFFER_LEN = 4096;
private static final int TIMEOUT = 2000;
@@ -146,16 +148,30 @@
return espPkt; // We've found the packet we're looking for.
}
+ /** Await the expected ESP packet */
public byte[] awaitEspPacket(int spi, boolean useEncap) throws Exception {
- return awaitPacket((pkt) -> isEsp(pkt, spi, useEncap));
+ return awaitEspPacket(spi, useEncap, SEQ_NUM_MATCH_NOT_REQUIRED);
}
- private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) {
+ /** Await the expected ESP packet with a matching sequence number */
+ public byte[] awaitEspPacket(int spi, boolean useEncap, int seqNum) throws Exception {
+ return awaitPacket((pkt) -> isEsp(pkt, spi, seqNum, useEncap));
+ }
+
+ private static boolean isMatchingEspPacket(byte[] pkt, int espOffset, int spi, int seqNum) {
ByteBuffer buffer = ByteBuffer.wrap(pkt);
buffer.get(new byte[espOffset]); // Skip IP, UDP header
int actualSpi = buffer.getInt();
+ int actualSeqNum = buffer.getInt();
- return actualSpi == spi;
+ if (actualSeqNum < 0) {
+ throw new UnsupportedOperationException(
+ "actualSeqNum overflowed and needs to be converted to an unsigned integer");
+ }
+
+ boolean isSeqNumMatched = (seqNum == SEQ_NUM_MATCH_NOT_REQUIRED || seqNum == actualSeqNum);
+
+ return actualSpi == spi && isSeqNumMatched;
}
/**
@@ -173,29 +189,32 @@
fail("Banned plaintext packet found");
}
- return isEsp(pkt, spi, encap);
+ return isEsp(pkt, spi, SEQ_NUM_MATCH_NOT_REQUIRED, encap);
}
- private static boolean isEsp(byte[] pkt, int spi, boolean encap) {
+ private static boolean isEsp(byte[] pkt, int spi, int seqNum, boolean encap) {
if (isIpv6(pkt)) {
if (encap) {
return pkt[IP6_PROTO_OFFSET] == IPPROTO_UDP
- && isSpiEqual(pkt, IP6_HDRLEN + UDP_HDRLEN, spi);
+ && isMatchingEspPacket(pkt, IP6_HDRLEN + UDP_HDRLEN, spi, seqNum);
} else {
- return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP6_HDRLEN, spi);
+ return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP
+ && isMatchingEspPacket(pkt, IP6_HDRLEN, spi, seqNum);
}
} else {
// Use default IPv4 header length (assuming no options)
if (encap) {
return pkt[IP4_PROTO_OFFSET] == IPPROTO_UDP
- && isSpiEqual(pkt, IP4_HDRLEN + UDP_HDRLEN, spi);
+ && isMatchingEspPacket(pkt, IP4_HDRLEN + UDP_HDRLEN, spi, seqNum);
} else {
- return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP4_HDRLEN, spi);
+ return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP
+ && isMatchingEspPacket(pkt, IP4_HDRLEN, spi, seqNum);
}
}
}
+
public static boolean isIpv6(byte[] pkt) {
// First nibble shows IP version. 0x60 for IPv6
return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
index dffd9d5..243cd27 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
@@ -46,6 +46,7 @@
import android.net.TetheringManager.TetheringEventCallback;
import android.net.TetheringManager.TetheringInterfaceRegexps;
import android.net.TetheringManager.TetheringRequest;
+import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiClient;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.SoftApCallback;
@@ -491,13 +492,29 @@
}
}
+ /**
+ * Starts Wi-Fi tethering.
+ */
public TetheringInterface startWifiTethering(final TestTetheringEventCallback callback)
throws InterruptedException {
+ return startWifiTethering(callback, null);
+ }
+
+ /**
+ * Starts Wi-Fi tethering with the specified SoftApConfiguration.
+ */
+ public TetheringInterface startWifiTethering(final TestTetheringEventCallback callback,
+ final SoftApConfiguration softApConfiguration)
+ throws InterruptedException {
final List<String> wifiRegexs = getWifiTetherableInterfaceRegexps(callback);
final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
- final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
- .setShouldShowEntitlementUi(false).build();
+ TetheringRequest.Builder builder = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setShouldShowEntitlementUi(false);
+ if (softApConfiguration != null) {
+ builder.setSoftApConfiguration(softApConfiguration);
+ }
+ final TetheringRequest request = builder.build();
return runAsShell(TETHER_PRIVILEGED, () -> {
mTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
diff --git a/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
new file mode 100644
index 0000000..20cfa1d
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
@@ -0,0 +1,128 @@
+/*
+ * 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 android.net.cts.util
+
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.EthernetManager
+import android.net.EthernetManager.InterfaceStateListener
+import android.net.EthernetManager.STATE_ABSENT
+import android.net.EthernetManager.STATE_LINK_UP
+import android.net.IpConfiguration
+import android.net.TestNetworkInterface
+import android.net.cts.util.EthernetTestInterface.EthernetStateListener.CallbackEntry.InterfaceStateChanged
+import android.os.Handler
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.PollPacketReader
+import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
+import java.net.NetworkInterface
+import kotlin.concurrent.Volatile
+import kotlin.test.assertNotNull
+
+private const val TAG = "EthernetTestInterface"
+private const val TIMEOUT_MS = 5_000L
+
+/**
+ * A class to manage the lifecycle of an ethernet interface.
+ *
+ * This class encapsulates creating new tun/tap interfaces and registering them with ethernet
+ * service.
+ */
+class EthernetTestInterface(
+ private val context: Context,
+ private val handler: Handler,
+ val testIface: TestNetworkInterface
+) {
+ private class EthernetStateListener(private val trackedIface: String) : InterfaceStateListener {
+ val events = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+ sealed class CallbackEntry {
+ data class InterfaceStateChanged(
+ val iface: String,
+ val state: Int,
+ val role: Int,
+ val cfg: IpConfiguration?
+ ) : CallbackEntry()
+ }
+
+ override fun onInterfaceStateChanged(
+ iface: String,
+ state: Int,
+ role: Int,
+ cfg: IpConfiguration?
+ ) {
+ // filter out callbacks for other interfaces
+ if (iface != trackedIface) return
+ events.add(InterfaceStateChanged(iface, state, role, cfg))
+ }
+
+ fun eventuallyExpect(state: Int) {
+ val cb = events.poll(TIMEOUT_MS) { it is InterfaceStateChanged && it.state == state }
+ assertNotNull(cb, "Never received state $state. Got: ${events.backtrace()}")
+ }
+ }
+
+ val name get() = testIface.interfaceName
+ val mtu: Int
+ get() {
+ val nif = NetworkInterface.getByName(name)
+ assertNotNull(nif)
+ return nif.mtu
+ }
+ val packetReader = PollPacketReader(handler, testIface.fileDescriptor.fileDescriptor, mtu)
+ private val listener = EthernetStateListener(name)
+ private val em = context.getSystemService(EthernetManager::class.java)!!
+ @Volatile private var cleanedUp = false
+
+ init{
+ em.addInterfaceStateListener(handler::post, listener)
+ runAsShell(NETWORK_SETTINGS) {
+ em.setIncludeTestInterfaces(true)
+ }
+ // Wait for link up to be processed in EthernetManager before returning.
+ listener.eventuallyExpect(STATE_LINK_UP)
+ handler.post { packetReader.start() }
+ handler.waitForIdle(TIMEOUT_MS)
+ }
+
+ fun destroy() {
+ // packetReader.stop() closes the test interface.
+ handler.post { packetReader.stop() }
+ handler.waitForIdle(TIMEOUT_MS)
+ listener.eventuallyExpect(STATE_ABSENT)
+
+ // setIncludeTestInterfaces() posts on the handler and does not run synchronously. However,
+ // there should be no need for a synchronization mechanism here. If the next test is
+ // bringing up its interface, a RTM_NEWLINK will be put on the back of the handler and is
+ // guaranteed to be in order with (i.e. after) this call, so there is no race here.
+ runAsShell(NETWORK_SETTINGS) {
+ em.setIncludeTestInterfaces(false)
+ }
+ em.removeInterfaceStateListener(listener)
+
+ cleanedUp = true
+ }
+
+ protected fun finalize() {
+ if (!cleanedUp) {
+ Log.wtf(TAG, "destroy() was not called for interface $name.")
+ destroy()
+ }
+ }
+}
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 1023173..d9bc7f7 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -19,10 +19,13 @@
java_defaults {
name: "CtsTetheringTestDefaults",
- defaults: ["cts_defaults"],
+ defaults: [
+ "cts_defaults",
+ "framework-connectivity-test-defaults",
+ ],
libs: [
- "android.test.base",
+ "android.test.base.stubs.system",
],
srcs: [
@@ -94,14 +97,8 @@
// Tag this module as a cts test artifact
test_suites: [
"cts",
- "mts-dnsresolver",
- "mts-networking",
"mts-tethering",
- "mts-wifi",
- "mcts-dnsresolver",
- "mcts-networking",
"mcts-tethering",
- "mcts-wifi",
"general-tests",
],
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 8794847..b294d63 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -22,6 +22,11 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
@@ -29,11 +34,13 @@
import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
import static android.net.cts.util.CtsTetheringUtils.isAnyIfaceMatch;
+import static android.os.Process.INVALID_UID;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -60,7 +67,9 @@
import android.net.cts.util.CtsTetheringUtils;
import android.net.cts.util.CtsTetheringUtils.StartTetheringCallback;
import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
+import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
+import android.net.wifi.WifiSsid;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.ResultReceiver;
@@ -68,9 +77,11 @@
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.modules.utils.build.SdkLevel;
import com.android.testutils.ParcelUtils;
import org.junit.After;
@@ -78,6 +89,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -214,45 +226,117 @@
}
+ private SoftApConfiguration createSoftApConfiguration(@NonNull String ssid) {
+ SoftApConfiguration config;
+ if (SdkLevel.isAtLeastT()) {
+ config = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(ssid.getBytes(StandardCharsets.UTF_8)))
+ .build();
+ } else {
+ config = new SoftApConfiguration.Builder()
+ .setSsid(ssid)
+ .build();
+ }
+ return config;
+ }
+
@Test
public void testTetheringRequest() {
- final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ SoftApConfiguration softApConfiguration = createSoftApConfiguration("SSID");
+ final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfiguration)
+ .build();
assertEquals(TETHERING_WIFI, tr.getTetheringType());
assertNull(tr.getLocalIpv4Address());
assertNull(tr.getClientStaticIpv4Address());
assertFalse(tr.isExemptFromEntitlementCheck());
assertTrue(tr.getShouldShowEntitlementUi());
+ assertEquals(CONNECTIVITY_SCOPE_GLOBAL, tr.getConnectivityScope());
+ assertEquals(softApConfiguration, tr.getSoftApConfiguration());
+ assertEquals(INVALID_UID, tr.getUid());
+ assertNull(tr.getPackageName());
+ assertEquals(tr.toString(), "TetheringRequest[ "
+ + "TETHERING_WIFI, "
+ + "showProvisioningUi, "
+ + "CONNECTIVITY_SCOPE_GLOBAL, "
+ + "softApConfig=" + softApConfiguration.toString()
+ + " ]");
final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
final TetheringRequest tr2 = new TetheringRequest.Builder(TETHERING_USB)
.setStaticIpv4Addresses(localAddr, clientAddr)
.setExemptFromEntitlementCheck(true)
- .setShouldShowEntitlementUi(false).build();
+ .setShouldShowEntitlementUi(false)
+ .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL)
+ .build();
+ int uid = 1000;
+ String packageName = "package";
+ tr2.setUid(uid);
+ tr2.setPackageName(packageName);
assertEquals(localAddr, tr2.getLocalIpv4Address());
assertEquals(clientAddr, tr2.getClientStaticIpv4Address());
assertEquals(TETHERING_USB, tr2.getTetheringType());
assertTrue(tr2.isExemptFromEntitlementCheck());
assertFalse(tr2.getShouldShowEntitlementUi());
+ assertEquals(CONNECTIVITY_SCOPE_LOCAL, tr2.getConnectivityScope());
+ assertNull(tr2.getSoftApConfiguration());
+ assertEquals(uid, tr2.getUid());
+ assertEquals(packageName, tr2.getPackageName());
+ assertEquals(tr2.toString(), "TetheringRequest[ "
+ + "TETHERING_USB, "
+ + "localIpv4Address=" + localAddr + ", "
+ + "staticClientAddress=" + clientAddr + ", "
+ + "exemptFromEntitlementCheck, "
+ + "CONNECTIVITY_SCOPE_LOCAL, "
+ + "uid=1000, "
+ + "packageName=package"
+ + " ]");
final TetheringRequest tr3 = new TetheringRequest.Builder(TETHERING_USB)
.setStaticIpv4Addresses(localAddr, clientAddr)
.setExemptFromEntitlementCheck(true)
- .setShouldShowEntitlementUi(false).build();
+ .setShouldShowEntitlementUi(false)
+ .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL)
+ .build();
+ tr3.setUid(uid);
+ tr3.setPackageName(packageName);
assertEquals(tr2, tr3);
}
@Test
+ public void testTetheringRequestSetSoftApConfigurationFailsWhenNotWifi() {
+ final SoftApConfiguration softApConfiguration = createSoftApConfiguration("SSID");
+ for (int type : List.of(TETHERING_USB, TETHERING_BLUETOOTH, TETHERING_WIFI_P2P,
+ TETHERING_NCM, TETHERING_ETHERNET)) {
+ try {
+ new TetheringRequest.Builder(type).setSoftApConfiguration(softApConfiguration);
+ fail("Was able to set SoftApConfiguration for tethering type " + type);
+ } catch (IllegalArgumentException e) {
+ // Success
+ }
+ }
+ }
+
+ @Test
public void testTetheringRequestParcelable() {
+ final SoftApConfiguration softApConfiguration = createSoftApConfiguration("SSID");
final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
- final TetheringRequest unparceled = new TetheringRequest.Builder(TETHERING_USB)
+ final TetheringRequest withConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfiguration)
.setStaticIpv4Addresses(localAddr, clientAddr)
.setExemptFromEntitlementCheck(true)
.setShouldShowEntitlementUi(false).build();
- final TetheringRequest parceled = ParcelUtils.parcelingRoundTrip(unparceled);
- assertEquals(unparceled, parceled);
+ final TetheringRequest withoutConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setStaticIpv4Addresses(localAddr, clientAddr)
+ .setExemptFromEntitlementCheck(true)
+ .setShouldShowEntitlementUi(false).build();
+ assertEquals(withConfig, ParcelUtils.parcelingRoundTrip(withConfig));
+ assertEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
+ assertNotEquals(withConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
+ assertNotEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withConfig));
}
@Test
@@ -264,10 +348,12 @@
tetherEventCallback.assumeWifiTetheringSupported(mContext);
tetherEventCallback.expectNoTetheringActive();
+ SoftApConfiguration softApConfig = createSoftApConfiguration("SSID");
final TetheringInterface tetheredIface =
- mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
assertNotNull(tetheredIface);
+ assertEquals(softApConfig, tetheredIface.getSoftApConfiguration());
final String wifiTetheringIface = tetheredIface.getInterface();
mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp
index 726e504..70a3655 100644
--- a/tests/deflake/Android.bp
+++ b/tests/deflake/Android.bp
@@ -40,7 +40,7 @@
"kotlin-test",
"net-host-tests-utils",
],
- data: [":FrameworksNetTests"],
+ device_common_data: [":FrameworksNetTests"],
test_suites: ["device-tests"],
// It will get build error if just set enabled to true. It fails with "windows_common"
// depends on some disabled modules that are used by this test and it looks like set
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 349529dd..6c3b7a0 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -33,7 +33,7 @@
"src/**/*.aidl",
],
libs: [
- "android.test.mock",
+ "android.test.mock.stubs",
"ServiceConnectivityResources",
],
static_libs: [
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index c5088c6..39a08fa 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -21,6 +21,7 @@
stl: "libc++_static",
shared_libs: [
"libbinder_ndk",
+ "libcom.android.tethering.connectivity_native",
"liblog",
"libnetutils",
"libprocessgroup",
@@ -32,7 +33,7 @@
"libmodules-utils-build",
"libutils",
],
- compile_multilib: "first",
+ compile_multilib: "both",
}
filegroup {
diff --git a/tests/native/connectivity_native_test/AndroidTestTemplate.xml b/tests/native/connectivity_native_test/AndroidTestTemplate.xml
index 44e35a9..4ea114c 100644
--- a/tests/native/connectivity_native_test/AndroidTestTemplate.xml
+++ b/tests/native/connectivity_native_test/AndroidTestTemplate.xml
@@ -15,8 +15,10 @@
<configuration description="Configuration for connectivity {MODULE} tests">
<option name="test-suite-tag" value="mts" />
<option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
- <!-- The tested code is only part of a SDK 30+ module (Tethering) -->
- <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+ <!-- The tested library is only usable on U+ -->
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+ <!-- The tested library is only available with the primary abi -->
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
<target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
<target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
diff --git a/tests/native/connectivity_native_test/connectivity_native_test.cpp b/tests/native/connectivity_native_test/connectivity_native_test.cpp
index f62a30b..faaf150 100644
--- a/tests/native/connectivity_native_test/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test/connectivity_native_test.cpp
@@ -14,25 +14,17 @@
* limitations under the License.
*/
+#include <android-modules-utils/sdk_level.h>
#include <android/binder_manager.h>
#include <android/binder_process.h>
-#include <android-modules-utils/sdk_level.h>
-#include <cutils/misc.h> // FIRST_APPLICATION_UID
+#include <connectivity_native.h>
+#include <cutils/misc.h> // FIRST_APPLICATION_UID
#include <dlfcn.h>
#include <gtest/gtest.h>
#include <netinet/in.h>
#include "bpf/BpfUtils.h"
-typedef int (*GetPortsBlockedForBind)(in_port_t*, size_t*);
-GetPortsBlockedForBind getPortsBlockedForBind;
-typedef int (*BlockPortForBind)(in_port_t);
-BlockPortForBind blockPortForBind;
-typedef int (*UnblockPortForBind)(in_port_t);
-UnblockPortForBind unblockPortForBind;
-typedef int (*UnblockAllPortsForBind)();
-UnblockAllPortsForBind unblockAllPortsForBind;
-
class ConnectivityNativeBinderTest : public ::testing::Test {
public:
in_port_t mActualBlockedPorts[65535];
@@ -50,29 +42,12 @@
if (!android::bpf::isAtLeastKernelVersion(5, 4, 0))
GTEST_SKIP() << "Kernel should be at least 5.4.";
- // Necessary to use dlopen/dlsym since the lib is only available on U and there
- // is no Sdk34ModuleController in tradefed yet.
- // TODO: link against the library directly and add Sdk34ModuleController to
- // AndroidTest.txml when available.
- void* nativeLib = dlopen("libcom.android.tethering.connectivity_native.so", RTLD_NOW);
- ASSERT_NE(nullptr, nativeLib);
- getPortsBlockedForBind = reinterpret_cast<GetPortsBlockedForBind>(
- dlsym(nativeLib, "AConnectivityNative_getPortsBlockedForBind"));
- ASSERT_NE(nullptr, getPortsBlockedForBind);
- blockPortForBind = reinterpret_cast<BlockPortForBind>(
- dlsym(nativeLib, "AConnectivityNative_blockPortForBind"));
- ASSERT_NE(nullptr, blockPortForBind);
- unblockPortForBind = reinterpret_cast<UnblockPortForBind>(
- dlsym(nativeLib, "AConnectivityNative_unblockPortForBind"));
- ASSERT_NE(nullptr, unblockPortForBind);
- unblockAllPortsForBind = reinterpret_cast<UnblockAllPortsForBind>(
- dlsym(nativeLib, "AConnectivityNative_unblockAllPortsForBind"));
- ASSERT_NE(nullptr, unblockAllPortsForBind);
-
- // If there are already ports being blocked on device unblockAllPortsForBind() store
- // the currently blocked ports and add them back at the end of the test. Do this for
- // every test case so additional test cases do not forget to add ports back.
- int err = getPortsBlockedForBind(mActualBlockedPorts, &mActualBlockedPortsCount);
+ // If there are already ports being blocked on device store the
+ // currently blocked ports and add them back at the end of the test. Do
+ // this for every test case so additional test cases do not forget to
+ // add ports back.
+ int err = AConnectivityNative_getPortsBlockedForBind(
+ mActualBlockedPorts, &mActualBlockedPortsCount);
EXPECT_EQ(err, 0);
restoreBlockedPorts = true;
}
@@ -81,8 +56,9 @@
int err;
if (mActualBlockedPortsCount > 0 && restoreBlockedPorts) {
for (int i=0; i < mActualBlockedPortsCount; i++) {
- err = blockPortForBind(mActualBlockedPorts[i]);
- EXPECT_EQ(err, 0);
+ err =
+ AConnectivityNative_blockPortForBind(mActualBlockedPorts[i]);
+ EXPECT_EQ(err, 0);
}
}
}
@@ -99,7 +75,7 @@
int blockedPort = 0;
if (blockPort) {
blockedPort = ntohs(port);
- err = blockPortForBind(blockedPort);
+ err = AConnectivityNative_blockPortForBind(blockedPort);
EXPECT_EQ(err, 0);
}
@@ -107,7 +83,7 @@
if (blockPort) {
EXPECT_EQ(-1, sock3);
- err = unblockPortForBind(blockedPort);
+ err = AConnectivityNative_unblockPortForBind(blockedPort);
EXPECT_EQ(err, 0);
} else {
EXPECT_NE(-1, sock3);
@@ -197,11 +173,11 @@
}
TEST_F(ConnectivityNativeBinderTest, BlockPortTwice) {
- int err = blockPortForBind(5555);
+ int err = AConnectivityNative_blockPortForBind(5555);
EXPECT_EQ(err, 0);
- err = blockPortForBind(5555);
+ err = AConnectivityNative_blockPortForBind(5555);
EXPECT_EQ(err, 0);
- err = unblockPortForBind(5555);
+ err = AConnectivityNative_unblockPortForBind(5555);
EXPECT_EQ(err, 0);
}
@@ -210,16 +186,17 @@
in_port_t blockedPorts[8] = {1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
if (mActualBlockedPortsCount > 0) {
- err = unblockAllPortsForBind();
+ err = AConnectivityNative_unblockAllPortsForBind();
}
for (int i : blockedPorts) {
- err = blockPortForBind(i);
+ err = AConnectivityNative_blockPortForBind(i);
EXPECT_EQ(err, 0);
}
size_t actualBlockedPortsCount = 8;
in_port_t actualBlockedPorts[actualBlockedPortsCount];
- err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+ err = AConnectivityNative_getPortsBlockedForBind(
+ (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
EXPECT_EQ(err, 0);
EXPECT_NE(actualBlockedPortsCount, 0);
for (int i=0; i < actualBlockedPortsCount; i++) {
@@ -227,9 +204,10 @@
}
// Remove the ports we added.
- err = unblockAllPortsForBind();
+ err = AConnectivityNative_unblockAllPortsForBind();
EXPECT_EQ(err, 0);
- err = getPortsBlockedForBind(actualBlockedPorts, &actualBlockedPortsCount);
+ err = AConnectivityNative_getPortsBlockedForBind(actualBlockedPorts,
+ &actualBlockedPortsCount);
EXPECT_EQ(err, 0);
EXPECT_EQ(actualBlockedPortsCount, 0);
}
@@ -239,23 +217,25 @@
in_port_t blockedPorts[8] = {1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
if (mActualBlockedPortsCount > 0) {
- err = unblockAllPortsForBind();
+ err = AConnectivityNative_unblockAllPortsForBind();
}
for (int i : blockedPorts) {
- err = blockPortForBind(i);
+ err = AConnectivityNative_blockPortForBind(i);
EXPECT_EQ(err, 0);
}
size_t actualBlockedPortsCount = 8;
in_port_t actualBlockedPorts[actualBlockedPortsCount];
- err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+ err = AConnectivityNative_getPortsBlockedForBind(
+ (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
EXPECT_EQ(err, 0);
EXPECT_EQ(actualBlockedPortsCount, 8);
- err = unblockAllPortsForBind();
+ err = AConnectivityNative_unblockAllPortsForBind();
EXPECT_EQ(err, 0);
- err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+ err = AConnectivityNative_getPortsBlockedForBind(
+ (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
EXPECT_EQ(err, 0);
EXPECT_EQ(actualBlockedPortsCount, 0);
// If mActualBlockedPorts is not empty, ports will be added back in teardown.
@@ -264,7 +244,7 @@
TEST_F(ConnectivityNativeBinderTest, CheckPermission) {
int curUid = getuid();
EXPECT_EQ(0, seteuid(FIRST_APPLICATION_UID + 2000)) << "seteuid failed: " << strerror(errno);
- int err = blockPortForBind((in_port_t) 5555);
+ int err = AConnectivityNative_blockPortForBind((in_port_t)5555);
EXPECT_EQ(EPERM, err);
EXPECT_EQ(0, seteuid(curUid)) << "seteuid failed: " << strerror(errno);
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index ef3ebb0..9edf9bd 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -72,8 +72,8 @@
],
}
-java_defaults {
- name: "FrameworksNetTestsDefaults",
+android_library {
+ name: "ConnectivityUnitTestsLib",
min_sdk_version: "30",
defaults: [
"framework-connectivity-internal-test-defaults",
@@ -82,6 +82,7 @@
"java/**/*.java",
"java/**/*.kt",
],
+ exclude_srcs: [":non-connectivity-module-test"],
static_libs: [
"androidx.test.rules",
"androidx.test.uiautomator_uiautomator",
@@ -104,24 +105,16 @@
],
libs: [
"android.net.ipsec.ike.stubs.module_lib",
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
"ServiceConnectivityResources",
],
exclude_kotlinc_generated_files: false,
-}
-
-android_library {
- name: "FrameworksNetTestsLib",
- defaults: [
- "FrameworksNetTestsDefaults",
- ],
- exclude_srcs: [":non-connectivity-module-test"],
visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
}
-genrule {
+java_genrule {
name: "frameworks-net-tests-jarjar-rules",
defaults: ["jarjar-rules-combine-defaults"],
srcs: [
@@ -137,7 +130,7 @@
java_genrule {
name: "frameworks-net-tests-lib-jarjar-gen",
tool_files: [
- ":FrameworksNetTestsLib{.jar}",
+ ":ConnectivityUnitTestsLib{.jar}",
"jarjar-excludes.txt",
],
tools: [
@@ -145,7 +138,7 @@
],
out: ["frameworks-net-tests-lib-jarjar-rules.txt"],
cmd: "$(location jarjar-rules-generator) " +
- "$(location :FrameworksNetTestsLib{.jar}) " +
+ "$(location :ConnectivityUnitTestsLib{.jar}) " +
"--prefix android.net.connectivity " +
"--excludes $(location jarjar-excludes.txt) " +
"--output $(out)",
@@ -156,14 +149,25 @@
name: "FrameworksNetTests",
enabled: enable_frameworks_net_tests,
defaults: [
- "FrameworksNetTestsDefaults",
+ "framework-connectivity-internal-test-defaults",
"FrameworksNetTests-jni-defaults",
],
jarjar_rules: ":frameworks-net-tests-jarjar-rules",
+ srcs: [":non-connectivity-module-test"],
test_suites: ["device-tests"],
static_libs: [
+ "frameworks-base-testutils",
"services.core",
"services.net",
+ "androidx.test.rules",
+ "framework-protos",
+ "mockito-target-minus-junit4",
+ "net-tests-utils",
+ "service-connectivity-pre-jarjar",
+ "service-connectivity-tiramisu-pre-jarjar",
+ ],
+ libs: [
+ "android.test.mock.stubs",
],
jni_libs: [
"libandroid_net_connectivity_com_android_net_module_util_jni",
diff --git a/tests/unit/java/android/net/IpMemoryStoreTest.java b/tests/unit/java/android/net/IpMemoryStoreTest.java
index 0b82759..e8f91e6 100644
--- a/tests/unit/java/android/net/IpMemoryStoreTest.java
+++ b/tests/unit/java/android/net/IpMemoryStoreTest.java
@@ -16,6 +16,11 @@
package android.net;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_CONFIRM;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@@ -68,6 +73,14 @@
-128, 0, 89, 112, 91, -34 };
private static final NetworkAttributes TEST_NETWORK_ATTRIBUTES = buildTestNetworkAttributes(
"hint", 219);
+ private static final long ONE_WEEK_IN_MS = 7 * 24 * 3600 * 1000;
+ private static final long ONE_DAY_IN_MS = 24 * 3600 * 1000;
+ private static final int[] NETWORK_EVENT_NUD_FAILURES = new int[] {
+ NETWORK_EVENT_NUD_FAILURE_ROAM,
+ NETWORK_EVENT_NUD_FAILURE_CONFIRM,
+ NETWORK_EVENT_NUD_FAILURE_ORGANIC,
+ NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED
+ };
@Mock
Context mMockContext;
@@ -333,4 +346,31 @@
mStore.factoryReset();
verify(mMockService, times(1)).factoryReset();
}
+
+ @Test
+ public void testNetworkEvents() throws Exception {
+ startIpMemoryStore(true /* supplyService */);
+ final String cluster = "cluster";
+
+ final long now = System.currentTimeMillis();
+ final long expiry = now + ONE_WEEK_IN_MS;
+ mStore.storeNetworkEvent(cluster, now, expiry, NETWORK_EVENT_NUD_FAILURE_ROAM,
+ status -> assertTrue("Store not successful : " + status.resultCode,
+ status.isSuccess()));
+ verify(mMockService, times(1)).storeNetworkEvent(eq(cluster),
+ eq(now), eq(expiry), eq(NETWORK_EVENT_NUD_FAILURE_ROAM), any());
+
+ final long[] sinceTimes = new long[2];
+ sinceTimes[0] = now - ONE_WEEK_IN_MS;
+ sinceTimes[1] = now - ONE_DAY_IN_MS;
+ mStore.retrieveNetworkEventCount(cluster, sinceTimes, NETWORK_EVENT_NUD_FAILURES,
+ (status, counts) -> {
+ assertTrue("Retrieve network event counts not successful : "
+ + status.resultCode, status.isSuccess());
+ assertEquals(new int[0], counts);
+ });
+
+ verify(mMockService, times(1)).retrieveNetworkEventCount(eq(cluster), eq(sinceTimes),
+ eq(NETWORK_EVENT_NUD_FAILURES), any());
+ }
}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 859c54a..c1c15ca 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -41,6 +41,7 @@
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_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;
@@ -136,6 +137,12 @@
private static final int TEST_UID = 10086;
private static final int[] TEST_UIDS = {10002, 10003};
+ private static final int[] CORE_AIDS = {
+ Process.ROOT_UID,
+ Process.SYSTEM_UID,
+ Process.FIRST_APPLICATION_UID - 10,
+ Process.FIRST_APPLICATION_UID - 1,
+ };
private static final String TEST_IF_NAME = "wlan0";
private static final int TEST_IF_INDEX = 7;
private static final int NO_IIF = 0;
@@ -1261,15 +1268,9 @@
assertTrue(BpfNetMapsUtils.isUidNetworkingBlocked(TEST_UID, false, mConfigurationMap,
mUidOwnerMap, mDataSaverEnabledMap));
- final int[] coreAids = new int[] {
- Process.ROOT_UID,
- Process.SYSTEM_UID,
- Process.FIRST_APPLICATION_UID - 10,
- Process.FIRST_APPLICATION_UID - 1,
- };
// Core appIds are not on the chain but should still be allowed on any user.
for (int userId = 0; userId < 20; userId++) {
- for (final int aid : coreAids) {
+ for (final int aid : CORE_AIDS) {
final int uid = UserHandle.getUid(userId, aid);
assertFalse(BpfNetMapsUtils.isUidNetworkingBlocked(uid, false, mConfigurationMap,
mUidOwnerMap, mDataSaverEnabledMap));
@@ -1277,6 +1278,26 @@
}
}
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testGetUidNetworkingBlockedReasonsForCoreUids() throws Exception {
+ // Enable BACKGROUND_MATCH that is an allowlist match.
+ mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(BACKGROUND_MATCH));
+
+ // Non-core uid that is not on this chain is blocked by BLOCKED_REASON_APP_BACKGROUND.
+ assertEquals(BLOCKED_REASON_APP_BACKGROUND, BpfNetMapsUtils.getUidNetworkingBlockedReasons(
+ TEST_UID, mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap));
+
+ // Core appIds are not on the chain but should not be blocked on any users.
+ for (int userId = 0; userId < 20; userId++) {
+ for (final int aid : CORE_AIDS) {
+ final int uid = UserHandle.getUid(userId, aid);
+ assertEquals(BLOCKED_REASON_NONE, BpfNetMapsUtils.getUidNetworkingBlockedReasons(
+ uid, mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap));
+ }
+ }
+ }
+
private void doTestIsUidRestrictedOnMeteredNetworks(
final long enabledMatches,
final long uidRules,
diff --git a/tests/unit/java/com/android/server/CallbackQueueTest.kt b/tests/unit/java/com/android/server/CallbackQueueTest.kt
index a6dd5c3..d8d35c1 100644
--- a/tests/unit/java/com/android/server/CallbackQueueTest.kt
+++ b/tests/unit/java/com/android/server/CallbackQueueTest.kt
@@ -116,7 +116,7 @@
addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
}
- val queue2 = CallbackQueue(queue1.shrinkedBackingArray)
+ val queue2 = CallbackQueue(queue1.minimizedBackingArray)
assertQueueEquals(listOf(
TEST_NETID_1 to CALLBACK_AVAILABLE,
TEST_NETID_2 to CALLBACK_AVAILABLE
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 999d17d..f7d7c87 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2369,6 +2369,18 @@
mScheduledEvaluationTimeouts.add(new Pair<>(network.netId, delayMs));
super.scheduleEvaluationTimeout(handler, network, delayMs);
}
+
+ @Override
+ public int getDefaultCellularDataInactivityTimeout() {
+ // Needed to mock out the dependency on DeviceConfig
+ return 10;
+ }
+
+ @Override
+ public int getDefaultWifiDataInactivityTimeout() {
+ // Needed to mock out the dependency on DeviceConfig
+ return 15;
+ }
}
private class AutomaticOnOffKeepaliveTrackerDependencies
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index ea3d2dd..fb3004a3 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -54,6 +54,7 @@
import android.net.ResolverOptionsParcel;
import android.net.ResolverParamsParcel;
import android.net.RouteInfo;
+import android.net.resolv.aidl.DohParamsParcel;
import android.net.shared.PrivateDnsConfig;
import android.os.Build;
import android.provider.Settings;
@@ -324,11 +325,10 @@
assertEquals(new InetAddress[0], cfgStrict.ips);
}
- @Test
- public void testSendDnsConfiguration() throws Exception {
+ private void doTestSendDnsConfiguration(PrivateDnsConfig cfg, DohParamsParcel expectedDohParams)
+ throws Exception {
reset(mMockDnsResolver);
- mDnsManager.updatePrivateDns(new Network(TEST_NETID),
- mDnsManager.getPrivateDnsConfig());
+ mDnsManager.updatePrivateDns(new Network(TEST_NETID), cfg);
final LinkProperties lp = new LinkProperties();
lp.setInterfaceName(TEST_IFACENAME);
lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
@@ -352,12 +352,63 @@
expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
expectedParams.resolverOptions = null;
expectedParams.meteredNetwork = true;
- expectedParams.dohParams = null;
+ expectedParams.dohParams = expectedDohParams;
expectedParams.interfaceNames = new String[]{TEST_IFACENAME};
verify(mMockDnsResolver, times(1)).setResolverConfiguration(eq(expectedParams));
}
@Test
+ public void testSendDnsConfiguration_ddrDisabled() throws Exception {
+ final PrivateDnsConfig cfg = new PrivateDnsConfig(
+ PRIVATE_DNS_MODE_OPPORTUNISTIC /* mode */,
+ null /* hostname */,
+ null /* ips */,
+ false /* ddrEnabled */,
+ null /* dohName */,
+ null /* dohIps */,
+ null /* dohPath */,
+ -1 /* dohPort */);
+ doTestSendDnsConfiguration(cfg, null /* expectedDohParams */);
+ }
+
+ @Test
+ public void testSendDnsConfiguration_ddrEnabledEmpty() throws Exception {
+ final PrivateDnsConfig cfg = new PrivateDnsConfig(
+ PRIVATE_DNS_MODE_OPPORTUNISTIC /* mode */,
+ null /* hostname */,
+ null /* ips */,
+ true /* ddrEnabled */,
+ null /* dohName */,
+ null /* dohIps */,
+ null /* dohPath */,
+ -1 /* dohPort */);
+
+ final DohParamsParcel params = new DohParamsParcel.Builder().build();
+ doTestSendDnsConfiguration(cfg, params);
+ }
+
+ @Test
+ public void testSendDnsConfiguration_ddrEnabled() throws Exception {
+ final PrivateDnsConfig cfg = new PrivateDnsConfig(
+ PRIVATE_DNS_MODE_OPPORTUNISTIC /* mode */,
+ null /* hostname */,
+ null /* ips */,
+ true /* ddrEnabled */,
+ "doh.com" /* dohName */,
+ null /* dohIps */,
+ "/some-path{?dns}" /* dohPath */,
+ 5353 /* dohPort */);
+
+ final DohParamsParcel params = new DohParamsParcel.Builder()
+ .setName("doh.com")
+ .setDohpath("/some-path{?dns}")
+ .setPort(5353)
+ .build();
+
+ doTestSendDnsConfiguration(cfg, params);
+ }
+
+ @Test
public void testTransportTypesEqual() throws Exception {
SparseArray<String> ncTransTypes = MessageUtils.findMessageNames(
new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" });
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index b5c0132..ab2fb99 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -19,11 +19,14 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,10 +35,12 @@
import android.net.Network;
import android.os.Handler;
import android.os.HandlerThread;
+import android.testing.TestableLooper;
import android.text.TextUtils;
import android.util.Pair;
import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.MdnsDiscoveryManager.DiscoveryExecutor;
import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -55,7 +60,9 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
/** Tests for {@link MdnsDiscoveryManager}. */
@DevSdkIgnoreRunner.MonitorThreadLeak
@@ -96,6 +103,7 @@
@Mock MdnsServiceBrowserListener mockListenerOne;
@Mock MdnsServiceBrowserListener mockListenerTwo;
@Mock SharedLog sharedLog;
+ @Mock MdnsServiceCache mockServiceCache;
private MdnsDiscoveryManager discoveryManager;
private HandlerThread thread;
private Handler handler;
@@ -139,7 +147,9 @@
return null;
}
};
+ discoveryManager = makeDiscoveryManager(MdnsFeatureFlags.newBuilder().build());
doReturn(mockExecutorService).when(mockServiceTypeClientType1NullNetwork).getExecutor();
+ doReturn(mockExecutorService).when(mockServiceTypeClientType1Network1).getExecutor();
}
@After
@@ -150,6 +160,40 @@
}
}
+ private MdnsDiscoveryManager makeDiscoveryManager(@NonNull MdnsFeatureFlags featureFlags) {
+ return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog, featureFlags) {
+ @Override
+ MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
+ @NonNull SocketKey socketKey) {
+ createdServiceTypeClientCount++;
+ final Pair<String, SocketKey> perSocketServiceType =
+ Pair.create(serviceType, socketKey);
+ if (perSocketServiceType.equals(PER_SOCKET_SERVICE_TYPE_1_NULL_NETWORK)) {
+ return mockServiceTypeClientType1NullNetwork;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_1_NETWORK_1)) {
+ return mockServiceTypeClientType1Network1;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_2_NULL_NETWORK)) {
+ return mockServiceTypeClientType2NullNetwork;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_2_NETWORK_1)) {
+ return mockServiceTypeClientType2Network1;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_2_NETWORK_2)) {
+ return mockServiceTypeClientType2Network2;
+ }
+ fail("Unexpected perSocketServiceType: " + perSocketServiceType);
+ return null;
+ }
+
+ @Override
+ MdnsServiceCache getServiceCache() {
+ return mockServiceCache;
+ }
+ };
+ }
+
private void runOnHandler(Runnable r) {
handler.post(r);
HandlerUtils.waitForIdle(handler, DEFAULT_TIMEOUT);
@@ -390,6 +434,99 @@
verify(mockServiceTypeClientType1NullNetwork).notifySocketDestroyed();
}
+ @Test
+ public void testDiscoveryExecutor() throws Exception {
+ final TestableLooper testableLooper = new TestableLooper(thread.getLooper());
+ final DiscoveryExecutor executor = new DiscoveryExecutor(testableLooper.getLooper());
+ try {
+ // Verify the checkAndRunOnHandlerThread method
+ final CompletableFuture<Boolean> future1 = new CompletableFuture<>();
+ executor.checkAndRunOnHandlerThread(()-> future1.complete(true));
+ assertTrue(future1.isDone());
+ assertTrue(future1.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
+
+ // Verify the execute method
+ final CompletableFuture<Boolean> future2 = new CompletableFuture<>();
+ executor.execute(()-> future2.complete(true));
+ testableLooper.processAllMessages();
+ assertTrue(future2.isDone());
+ assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
+
+ // Verify the executeDelayed method
+ final CompletableFuture<Boolean> future3 = new CompletableFuture<>();
+ // Schedule a task with 999 ms delay
+ executor.executeDelayed(()-> future3.complete(true), 999L);
+ testableLooper.processAllMessages();
+ assertFalse(future3.isDone());
+
+ // 500 ms have elapsed but do not exceed the target time (999 ms)
+ // The function should not be executed.
+ testableLooper.moveTimeForward(500L);
+ testableLooper.processAllMessages();
+ assertFalse(future3.isDone());
+
+ // 500 ms have elapsed again and have exceeded the target time (999 ms).
+ // The function should be executed.
+ testableLooper.moveTimeForward(500L);
+ testableLooper.processAllMessages();
+ assertTrue(future3.isDone());
+ assertTrue(future3.get(500L, TimeUnit.MILLISECONDS));
+ } finally {
+ testableLooper.destroy();
+ }
+ }
+
+ @Test
+ public void testRemoveServicesAfterAllListenersUnregistered() throws IOException {
+ final MdnsFeatureFlags mdnsFeatureFlags = MdnsFeatureFlags.newBuilder()
+ .setIsCachedServicesRemovalEnabled(true)
+ .setCachedServicesRetentionTime(0L)
+ .build();
+ discoveryManager = makeDiscoveryManager(mdnsFeatureFlags);
+
+ final MdnsSearchOptions options =
+ MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, options);
+ runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options);
+
+ final MdnsServiceCache.CacheKey cacheKey =
+ new MdnsServiceCache.CacheKey(SERVICE_TYPE_1, SOCKET_KEY_NETWORK_1);
+ doReturn(cacheKey).when(mockServiceTypeClientType1Network1).getCacheKey();
+ doReturn(true).when(mockServiceTypeClientType1Network1)
+ .stopSendAndReceive(mockListenerOne);
+ runOnHandler(() -> discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne));
+ verify(executorProvider).shutdownExecutorService(mockExecutorService);
+ verify(mockServiceTypeClientType1Network1).stopSendAndReceive(mockListenerOne);
+ verify(socketClient).stopDiscovery();
+ verify(mockServiceCache, timeout(DEFAULT_TIMEOUT)).removeServices(cacheKey);
+ }
+
+ @Test
+ public void testRemoveServicesAfterSocketDestroyed() throws IOException {
+ final MdnsFeatureFlags mdnsFeatureFlags = MdnsFeatureFlags.newBuilder()
+ .setIsCachedServicesRemovalEnabled(true)
+ .setCachedServicesRetentionTime(0L)
+ .build();
+ discoveryManager = makeDiscoveryManager(mdnsFeatureFlags);
+
+ final MdnsSearchOptions options =
+ MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, options);
+ runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options);
+
+ final MdnsServiceCache.CacheKey cacheKey =
+ new MdnsServiceCache.CacheKey(SERVICE_TYPE_1, SOCKET_KEY_NETWORK_1);
+ doReturn(cacheKey).when(mockServiceTypeClientType1Network1).getCacheKey();
+ runOnHandler(() -> callback.onSocketDestroyed(SOCKET_KEY_NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).notifySocketDestroyed();
+ verify(executorProvider).shutdownExecutorService(mockExecutorService);
+ verify(mockServiceCache, timeout(DEFAULT_TIMEOUT)).removeServices(cacheKey);
+ }
+
private MdnsPacket createMdnsPacket(String serviceType) {
final String[] type = TextUtils.split(serviceType, "\\.");
final ArrayList<String> name = new ArrayList<>(type.length + 1);
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 2cb97c9..9674da3 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -123,18 +123,18 @@
}
private val TEST_PUBLIC_KEY = hexStringToByteArray(
- "0201030dc141d0637960b98cbc12cfca"
- + "221d2879dac26ee5b460e9007c992e19"
- + "02d897c391b03764d448f7d0c772fdb0"
- + "3b1d9d6d52ff8886769e8e2362513565"
- + "270962d3")
+ "0201030dc141d0637960b98cbc12cfca" +
+ "221d2879dac26ee5b460e9007c992e19" +
+ "02d897c391b03764d448f7d0c772fdb0" +
+ "3b1d9d6d52ff8886769e8e2362513565" +
+ "270962d3")
private val TEST_PUBLIC_KEY_2 = hexStringToByteArray(
- "0201030dc141d0637960b98cbc12cfca"
- + "221d2879dac26ee5b460e9007c992e19"
- + "02d897c391b03764d448f7d0c772fdb0"
- + "3b1d9d6d52ff8886769e8e2362513565"
- + "270962d4")
+ "0201030dc141d0637960b98cbc12cfca" +
+ "221d2879dac26ee5b460e9007c992e19" +
+ "02d897c391b03764d448f7d0c772fdb0" +
+ "3b1d9d6d52ff8886769e8e2362513565" +
+ "270962d4")
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -159,7 +159,7 @@
@Before
fun setUp() {
- deps.resetElapsedRealTime();
+ deps.resetElapsedRealTime()
thread.start()
}
@@ -172,11 +172,13 @@
private fun makeFlags(
includeInetAddressesInProbing: Boolean = false,
isKnownAnswerSuppressionEnabled: Boolean = false,
- unicastReplyEnabled: Boolean = true
+ unicastReplyEnabled: Boolean = true,
+ avoidAdvertisingEmptyTxtRecords: Boolean = true
) = MdnsFeatureFlags.Builder()
.setIncludeInetAddressRecordsInProbing(includeInetAddressesInProbing)
.setIsKnownAnswerSuppressionEnabled(isKnownAnswerSuppressionEnabled)
.setIsUnicastReplyEnabled(unicastReplyEnabled)
+ .setAvoidAdvertisingEmptyTxtRecords(avoidAdvertisingEmptyTxtRecords)
.build()
@Test
@@ -1721,6 +1723,30 @@
}
@Test
+ fun testGetConflictingServices_ZeroLengthTxtRecord_NoConflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ val packet = MdnsPacket(
+ 0 /* flags */,
+ emptyList() /* questions */,
+ listOf(
+ MdnsTextRecord(
+ arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ listOf(TextEntry("", null as ByteArray?))
+ ),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */
+ )
+
+ assertEquals(emptyMap(), repository.getConflictingServices(packet))
+ }
+
+ @Test
fun testGetServiceRepliedRequestsCount() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
@@ -2168,6 +2194,46 @@
assertEquals(knownAnswers, reply.knownAnswers)
}
+ private fun doAddServiceWithEmptyTxtRecordTest(flags: MdnsFeatureFlags): MdnsTextRecord {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ val questions = listOf(MdnsTextRecord(
+ arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+ true /* isUnicast */
+ ))
+ val query = MdnsPacket(
+ 0 /* flags */,
+ questions,
+ emptyList() /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */
+ )
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(1, reply.answers.size)
+ assertTrue(reply.answers[0] is MdnsTextRecord)
+ return reply.answers[0] as MdnsTextRecord
+ }
+
+ @Test
+ fun testAddService_AvoidEmptyTxtRecords_HasTxtRecordWithEmptyString() {
+ val answerRecord = doAddServiceWithEmptyTxtRecordTest(makeFlags())
+ assertEquals(1, answerRecord.entries.size)
+ assertEquals(0, answerRecord.entries[0].key.length)
+ assertNull(answerRecord.entries[0].value)
+ }
+
+ @Test
+ fun testAddService_UsesEmptyTxtRecords_HasEmptyTxtRecord() {
+ val answerRecord = doAddServiceWithEmptyTxtRecordTest(makeFlags(
+ avoidAdvertisingEmptyTxtRecords = false
+ ))
+ assertEquals(0, answerRecord.entries.size)
+ }
+
@Test
fun testRestartProbingForHostname() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
index 63548c1..784c502 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -28,6 +28,8 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import static java.util.Collections.emptyList;
+
import android.util.Log;
import com.android.net.module.util.HexDump;
@@ -372,6 +374,30 @@
assertEquals(dataInText, dataOutText);
}
+ private static MdnsTextRecord makeTextRecordWithEntries(List<TextEntry> entries) {
+ return new MdnsTextRecord(new String[] { "test", "record" }, 0L /* receiptTimeMillis */,
+ true /* cacheFlush */, 120_000L /* ttlMillis */, entries);
+ }
+
+ @Test
+ public void testTextRecord_EmptyRecordsAreEquivalent() {
+ final MdnsTextRecord record1 = makeTextRecordWithEntries(emptyList());
+ final MdnsTextRecord record2 = makeTextRecordWithEntries(
+ List.of(new TextEntry("", (byte[]) null)));
+ final MdnsTextRecord record3 = makeTextRecordWithEntries(
+ List.of(new TextEntry(null, (byte[]) null)));
+ final MdnsTextRecord nonEmptyRecord = makeTextRecordWithEntries(
+ List.of(new TextEntry("a", (byte[]) null)));
+
+ assertEquals(record1, record1);
+ assertEquals(record1, record2);
+ assertEquals(record1, record3);
+
+ assertNotEquals(nonEmptyRecord, record1);
+ assertNotEquals(nonEmptyRecord, record2);
+ assertNotEquals(nonEmptyRecord, record3);
+ }
+
private static String toHex(MdnsRecord record) throws IOException {
MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
record.write(writer, record.getReceiptTime());
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index b040ab6..0a8f108 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -154,6 +154,11 @@
serviceCache.registerServiceExpiredCallback(cacheKey, callback)
}
+ private fun removeServices(
+ serviceCache: MdnsServiceCache,
+ cacheKey: CacheKey
+ ): Unit = runningOnHandlerAndReturn { serviceCache.removeServices(cacheKey) }
+
@Test
fun testAddAndRemoveService() {
val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
@@ -291,6 +296,37 @@
assertEquals(response4, responses[3])
}
+ @Test
+ fun testRemoveServices() {
+ val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
+ addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_1, SERVICE_TYPE_2))
+ val responses1 = getServices(serviceCache, cacheKey1)
+ assertEquals(2, responses1.size)
+ assertTrue(responses1.stream().anyMatch { response ->
+ response.serviceInstanceName == SERVICE_NAME_1
+ })
+ assertTrue(responses1.any { response ->
+ response.serviceInstanceName == SERVICE_NAME_2
+ })
+ val responses2 = getServices(serviceCache, cacheKey2)
+ assertEquals(1, responses2.size)
+ assertTrue(responses2.stream().anyMatch { response ->
+ response.serviceInstanceName == SERVICE_NAME_1
+ })
+
+ removeServices(serviceCache, cacheKey1)
+ val responses3 = getServices(serviceCache, cacheKey1)
+ assertEquals(0, responses3.size)
+ val responses4 = getServices(serviceCache, cacheKey2)
+ assertEquals(1, responses4.size)
+
+ removeServices(serviceCache, cacheKey2)
+ val responses5 = getServices(serviceCache, cacheKey2)
+ assertEquals(0, responses5.size)
+ }
+
private fun createResponse(
serviceInstanceName: String,
serviceType: String,
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 44fa55c..da0bc88 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -562,10 +562,7 @@
//MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
.addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
- QueryTaskConfig config = new QueryTaskConfig(
- searchOptions.getQueryMode(),
- false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
- socketKey);
+ QueryTaskConfig config = new QueryTaskConfig(searchOptions.getQueryMode());
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
@@ -574,14 +571,14 @@
// For the rest of queries in this burst, we will NOT ask for unicast response.
for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
int oldTransactionId = config.transactionId;
- config = config.getConfigForNextRun();
+ config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
assertEquals(config.transactionId, oldTransactionId + 1);
}
// This is the first query of a new burst. We will ask for unicast response.
int oldTransactionId = config.transactionId;
- config = config.getConfigForNextRun();
+ config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertTrue(config.expectUnicastResponse);
assertEquals(config.transactionId, oldTransactionId + 1);
}
@@ -590,10 +587,7 @@
public void testQueryTaskConfig_askForUnicastInFirstQuery() {
MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
.addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
- QueryTaskConfig config = new QueryTaskConfig(
- searchOptions.getQueryMode(),
- false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
- socketKey);
+ QueryTaskConfig config = new QueryTaskConfig(searchOptions.getQueryMode());
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
@@ -602,14 +596,14 @@
// For the rest of queries in this burst, we will NOT ask for unicast response.
for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
int oldTransactionId = config.transactionId;
- config = config.getConfigForNextRun();
+ config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
assertEquals(config.transactionId, oldTransactionId + 1);
}
// This is the first query of a new burst. We will NOT ask for unicast response.
int oldTransactionId = config.transactionId;
- config = config.getConfigForNextRun();
+ config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
assertEquals(config.transactionId, oldTransactionId + 1);
}
@@ -2062,6 +2056,64 @@
assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
}
+ @Test
+ public void sendQueries_AggressiveQueryMode_ServiceInCache() {
+ final int numOfQueriesBeforeBackoff = 11;
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .setQueryMode(AGGRESSIVE_QUERY_MODE)
+ .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+ .build();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ int burstCounter = 0;
+ int betweenBurstTime = 0;
+ for (int i = 0; i < numOfQueriesBeforeBackoff; i += 3) {
+ verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ /* expectsUnicastResponse= */ false);
+ betweenBurstTime = Math.min(
+ INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS * (int) Math.pow(2, burstCounter),
+ MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+ burstCounter++;
+ }
+ // In backoff mode, the current scheduled task will be canceled and reschedule if the
+ // 0.8 * smallestRemainingTtl is larger than time to next run.
+ long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ doReturn(true).when(mockDeps).hasMessages(any(), eq(EVENT_START_QUERYTASK));
+ processResponse(createResponse(
+ "service-instance-1", "192.0.2.123", 5353,
+ SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), socketKey);
+ verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ assertNotNull(delayMessage);
+ assertEquals((long) (TEST_TTL / 2 * 0.8), latestDelayMs);
+
+ // Register another listener. There is a service in cache, the query time should be
+ // rescheduled with previous run.
+ currentTime += (long) ((TEST_TTL / 2 * 0.8) - 500L);
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ startSendAndReceive(mockListenerTwo, searchOptions);
+ verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ assertNotNull(delayMessage);
+ assertEquals(500L, latestDelayMs);
+
+ // Stop all listeners
+ stopSendAndReceive(mockListenerOne);
+ stopSendAndReceive(mockListenerTwo);
+ verify(mockDeps, times(4)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ // Register a new listener. There is a service in cache, the query time should be
+ // rescheduled with remaining ttl.
+ currentTime += 400L;
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ assertNotNull(delayMessage);
+ assertEquals(9680L, latestDelayMs);
+ }
+
private static MdnsServiceInfo matchServiceName(String name) {
return argThat(info -> info.getServiceInstanceName().equals(name));
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
index cf88d05..efae244 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
@@ -22,83 +22,31 @@
import com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED
import com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR
import com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR
+import com.android.server.connectivity.mdns.MdnsInetAddressRecord
import com.android.server.connectivity.mdns.MdnsPacket
import com.android.server.connectivity.mdns.MdnsPacketReader
import com.android.server.connectivity.mdns.MdnsPointerRecord
import com.android.server.connectivity.mdns.MdnsRecord
+import com.android.server.connectivity.mdns.MdnsResponse
+import com.android.server.connectivity.mdns.MdnsServiceInfo
+import com.android.server.connectivity.mdns.MdnsServiceRecord
+import com.android.server.connectivity.mdns.MdnsTextRecord
import com.android.server.connectivity.mdns.util.MdnsUtils.createQueryDatagramPackets
-import com.android.server.connectivity.mdns.util.MdnsUtils.equalsDnsLabelIgnoreDnsCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLabelsLowerCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase
import com.android.server.connectivity.mdns.util.MdnsUtils.truncateServiceName
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
-import java.net.DatagramPacket
-import kotlin.test.assertContentEquals
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
+import java.net.DatagramPacket
+import kotlin.test.assertContentEquals
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsUtilsTest {
- @Test
- fun testToDnsLowerCase() {
- assertEquals("test", toDnsLowerCase("TEST"))
- assertEquals("test", toDnsLowerCase("TeSt"))
- assertEquals("test", toDnsLowerCase("test"))
- assertEquals("tÉst", toDnsLowerCase("TÉST"))
- assertEquals("ţést", toDnsLowerCase("ţést"))
- // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
- // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
- assertEquals(
- "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
- toDnsLowerCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ")
- )
- // Also test some characters where the first surrogate is not \ud800
- assertEquals(
- "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
- "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
- toDnsLowerCase(
- "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
- "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
- )
- )
- }
-
- @Test
- fun testToDnsLabelsLowerCase() {
- assertArrayEquals(
- arrayOf("test", "tÉst", "ţést"),
- toDnsLabelsLowerCase(arrayOf("TeSt", "TÉST", "ţést"))
- )
- }
-
- @Test
- fun testEqualsIgnoreDnsCase() {
- assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
- assertTrue(equalsIgnoreDnsCase("TEST", "test"))
- assertTrue(equalsIgnoreDnsCase("test", "TeSt"))
- assertTrue(equalsIgnoreDnsCase("Tést", "tést"))
- assertFalse(equalsIgnoreDnsCase("ŢÉST", "ţést"))
- // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
- // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
- assertTrue(equalsIgnoreDnsCase(
- "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
- "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "
- ))
- // Also test some characters where the first surrogate is not \ud800
- assertTrue(equalsIgnoreDnsCase(
- "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
- "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
- "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
- "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
- ))
- }
@Test
fun testTruncateServiceName() {
@@ -107,14 +55,6 @@
}
@Test
- fun testEqualsLabelIgnoreDnsCase() {
- assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
- assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
- assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
- assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
- }
-
- @Test
fun testTypeEqualsOrIsSubtype() {
assertTrue(MdnsUtils.typeEqualsOrIsSubtype(
arrayOf("_type", "_tcp", "local"),
@@ -223,4 +163,54 @@
assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet, otherV6Packet)))
assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, v6Packet)))
}
+
+ @Test
+ fun testBuildMdnsServiceInfoFromResponse() {
+ val serviceInstanceName = "MyTestService"
+ val serviceType = "_testservice._tcp.local"
+ val hostName = "Android_000102030405060708090A0B0C0D0E0F.local"
+ val port = 12345
+ val ttlTime = 120000L
+ val testElapsedRealtime = 123L
+ val serviceName = "$serviceInstanceName.$serviceType".split(".").toTypedArray()
+ val v4Address = "192.0.2.1"
+ val v6Address = "2001:db8::1"
+ val interfaceIndex = 99
+ val response = MdnsResponse(0 /* now */, serviceName, interfaceIndex, null /* network */)
+ // Set PTR record
+ response.addPointerRecord(MdnsPointerRecord(serviceType.split(".").toTypedArray(),
+ testElapsedRealtime, false /* cacheFlush */, ttlTime, serviceName))
+ // Set SRV record.
+ response.serviceRecord = MdnsServiceRecord(serviceName, testElapsedRealtime,
+ false /* cacheFlush */, ttlTime, 0 /* servicePriority */, 0 /* serviceWeight */,
+ port, hostName.split(".").toTypedArray())
+ // Set TXT record.
+ response.textRecord = MdnsTextRecord(serviceName,
+ testElapsedRealtime, true /* cacheFlush */, 0L /* ttlMillis */,
+ listOf(MdnsServiceInfo.TextEntry.fromString("somedifferent=entry")))
+ // Set InetAddress record.
+ response.addInet4AddressRecord(MdnsInetAddressRecord(hostName.split(".").toTypedArray(),
+ testElapsedRealtime, true /* cacheFlush */,
+ 0L /* ttlMillis */, InetAddresses.parseNumericAddress(v4Address)))
+ response.addInet6AddressRecord(MdnsInetAddressRecord(hostName.split(".").toTypedArray(),
+ testElapsedRealtime, true /* cacheFlush */,
+ 0L /* ttlMillis */, InetAddresses.parseNumericAddress(v6Address)))
+
+ // Convert a MdnsResponse to a MdnsServiceInfo
+ val serviceInfo = MdnsUtils.buildMdnsServiceInfoFromResponse(
+ response, serviceType.split(".").toTypedArray(), testElapsedRealtime)
+
+ assertEquals(serviceInstanceName, serviceInfo.serviceInstanceName)
+ assertArrayEquals(serviceType.split(".").toTypedArray(), serviceInfo.serviceType)
+ assertArrayEquals(hostName.split(".").toTypedArray(), serviceInfo.hostName)
+ assertEquals(port, serviceInfo.port)
+ assertEquals(1, serviceInfo.ipv4Addresses.size)
+ assertEquals(v4Address, serviceInfo.ipv4Addresses[0])
+ assertEquals(1, serviceInfo.ipv6Addresses.size)
+ assertEquals(v6Address, serviceInfo.ipv6Addresses[0])
+ assertEquals(interfaceIndex, serviceInfo.interfaceIndex)
+ assertEquals(null, serviceInfo.network)
+ assertEquals(mapOf("somedifferent" to "entry"),
+ serviceInfo.attributes)
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
index 5c29e3a..b824531 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
@@ -27,9 +27,26 @@
import com.android.testutils.TestableNetworkCallback
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
private const val LONG_TIMEOUT_MS = 5_000
+private val CAPABILITIES = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build()
+
+private val REQUEST = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_WIFI)
+ .build()
+
+
@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@@ -37,29 +54,53 @@
class CSDestroyedNetworkTests : CSTest() {
@Test
fun testDestroyNetworkNotKeptWhenUnvalidated() {
- val nc = NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_WIFI)
- .build()
-
- val nr = NetworkRequest.Builder()
- .clearCapabilities()
- .addTransportType(TRANSPORT_WIFI)
- .build()
val cbRequest = TestableNetworkCallback()
val cbCallback = TestableNetworkCallback()
- cm.requestNetwork(nr, cbRequest)
- cm.registerNetworkCallback(nr, cbCallback)
+ cm.requestNetwork(REQUEST, cbRequest)
+ cm.registerNetworkCallback(REQUEST, cbCallback)
- val firstAgent = Agent(nc = nc)
+ val firstAgent = Agent(nc = CAPABILITIES)
firstAgent.connect()
cbCallback.expectAvailableCallbacks(firstAgent.network, validated = false)
firstAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
- val secondAgent = Agent(nc = nc)
+ val secondAgent = Agent(nc = CAPABILITIES)
secondAgent.connect()
cbCallback.expectAvailableCallbacks(secondAgent.network, validated = false)
cbCallback.expect<Lost>(timeoutMs = 500) { it.network == firstAgent.network }
}
+
+ @Test
+ fun testDestroyNetworkWithDelayedTeardown() {
+ val cbRequest = TestableNetworkCallback()
+ val cbCallback = TestableNetworkCallback()
+ cm.requestNetwork(REQUEST, cbRequest)
+ cm.registerNetworkCallback(REQUEST, cbCallback)
+
+ val firstAgent = Agent(nc = CAPABILITIES)
+ firstAgent.connect()
+ firstAgent.setTeardownDelayMillis(1)
+ cbCallback.expectAvailableCallbacks(firstAgent.network, validated = false)
+
+ clearInvocations(netd)
+ val inOrder = inOrder(netd)
+ firstAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+
+ val secondAgent = Agent(nc = CAPABILITIES)
+ secondAgent.connect()
+ cbCallback.expectAvailableCallbacks(secondAgent.network, validated = false)
+ secondAgent.disconnect()
+
+ cbCallback.expect<Lost>(timeoutMs = 500) { it.network == firstAgent.network }
+ cbCallback.expect<Lost>(timeoutMs = 500) { it.network == secondAgent.network }
+ // onLost is fired before the network is destroyed.
+ waitForIdle()
+
+ inOrder.verify(netd).networkDestroy(eq(firstAgent.network.netId))
+ inOrder.verify(netd).networkCreate(argThat{ it.netId == secondAgent.network.netId })
+ inOrder.verify(netd).networkDestroy(eq(secondAgent.network.netId))
+ verify(netd, never()).networkSetPermissionForNetwork(anyInt(), anyInt())
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
index 93f6e81..77b06b2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
@@ -26,7 +26,9 @@
import android.net.NetworkCapabilities.TRANSPORT_VPN
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
+import android.net.VpnManager.TYPE_VPN_OEM
import android.net.VpnManager.TYPE_VPN_SERVICE
+import android.net.VpnManager.TYPE_VPN_LEGACY
import android.net.VpnTransportInfo
import android.os.Build
import androidx.test.filters.SmallTest
@@ -49,19 +51,19 @@
private const val TIMEOUT_MS = 1_000L
private const val LONG_TIMEOUT_MS = 5_000
-private fun vpnNc() = NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_VPN)
- .removeCapability(NET_CAPABILITY_NOT_VPN)
- .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
- .setTransportInfo(
- VpnTransportInfo(
- TYPE_VPN_SERVICE,
- "MySession12345",
- false /* bypassable */,
- false /* longLivedTcpConnectionsExpensive */
- )
- )
- .build()
+private fun vpnNc(vpnType: Int = TYPE_VPN_SERVICE) = NetworkCapabilities.Builder().apply {
+ addTransportType(TRANSPORT_VPN)
+ removeCapability(NET_CAPABILITY_NOT_VPN)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ setTransportInfo(
+ VpnTransportInfo(
+ vpnType,
+ "MySession12345",
+ false /* bypassable */,
+ false /* longLivedTcpConnectionsExpensive */
+ )
+ )
+}.build()
private fun wifiNc() = NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_WIFI)
@@ -310,4 +312,38 @@
// IngressDiscardRule should not be added since feature is disabled
verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
}
+
+ fun doTestVpnIngressDiscardRule_VpnType(vpnType: Int, expectAddRule: Boolean) {
+ val nr = nr(TRANSPORT_VPN)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, cb)
+ val nc = vpnNc(vpnType)
+ val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val agent = Agent(nc = nc, lp = lp)
+ agent.connect()
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+
+ if (expectAddRule) {
+ verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+ } else {
+ verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+ }
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_ServiceVpn() {
+ doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_SERVICE, expectAddRule = true)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_LegacyVpn() {
+ // IngressDiscardRule should not be added to Legacy VPN
+ doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_LEGACY, expectAddRule = false)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_OemVpn() {
+ // IngressDiscardRule should not be added to OEM VPN
+ doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_OEM, expectAddRule = false)
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
index df0a2cc..ccbd6b3 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
@@ -21,6 +21,7 @@
import android.net.ConnectivityManager.EXTRA_DEVICE_TYPE
import android.net.ConnectivityManager.EXTRA_IS_ACTIVE
import android.net.ConnectivityManager.EXTRA_REALTIME_NS
+import android.net.ConnectivitySettingsManager
import android.net.LinkProperties
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_IMS
@@ -41,12 +42,14 @@
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.TestableNetworkCallback
+import java.time.Duration
import kotlin.test.assertNotNull
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyLong
@@ -69,6 +72,18 @@
@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class CSNetworkActivityTest : CSTest() {
+ private fun setMobileDataActivityTimeout(timeoutSeconds: Int) {
+ ConnectivitySettingsManager.setMobileDataActivityTimeout(
+ context, Duration.ofSeconds(timeoutSeconds.toLong())
+ )
+ }
+
+ private fun setWifiDataActivityTimeout(timeoutSeconds: Int) {
+ ConnectivitySettingsManager.setWifiDataActivityTimeout(
+ context, Duration.ofSeconds(timeoutSeconds.toLong())
+ )
+ }
+
private fun getRegisteredNetdUnsolicitedEventListener(): BaseNetdUnsolicitedEventListener {
val captor = ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener::class.java)
verify(netd).registerUnsolicitedEventListener(captor.capture())
@@ -252,8 +267,122 @@
cm.unregisterNetworkCallback(dataNetworkCb)
cm.unregisterNetworkCallback(imsNetworkCb)
}
+
+ @Test
+ fun testCellularIdleTimerSettingsTimeout() {
+ val cellNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val cellLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+
+ val settingsTimeout: Int = deps.defaultCellDataInactivityTimeoutForTest + 432
+ // DATA_ACTIVITY_TIMEOUT_MOBILE is set, so the default should be ignored.
+ setMobileDataActivityTimeout(settingsTimeout)
+ val cellAgent = Agent(nc = cellNc, lp = cellLp)
+ cellAgent.connect()
+
+ verify(netd).idletimerAddInterface(eq(DATA_CELL_IFNAME), eq(settingsTimeout), anyString())
+ }
+
+ @Test
+ fun testCellularIdleTimerDefaultTimeout() {
+ val cellNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val cellLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+
+ val testTimeout: Int = deps.defaultCellDataInactivityTimeoutForTest
+ // DATA_ACTIVITY_TIMEOUT_MOBILE is not set, so the default should be used.
+ val cellAgent = Agent(nc = cellNc, lp = cellLp)
+ cellAgent.connect()
+
+ verify(netd).idletimerAddInterface(eq(DATA_CELL_IFNAME), eq(testTimeout), anyString())
+ }
+
+ @Test
+ fun testCellularIdleTimerDisabled() {
+ val cellNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val cellLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+ setMobileDataActivityTimeout(0)
+ val cellAgent = Agent(nc = cellNc, lp = cellLp)
+ cellAgent.connect()
+
+ verify(netd, never()).idletimerAddInterface(eq(DATA_CELL_IFNAME), anyInt(), anyString())
+ }
+
+ @Test
+ fun testWifiIdleTimerSettingsTimeout() {
+ val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val wifiLp = LinkProperties().apply {
+ interfaceName = WIFI_IFNAME
+ }
+ val settingsTimeout: Int = deps.defaultWifiDataInactivityTimeout + 435
+ setWifiDataActivityTimeout(settingsTimeout)
+ // DATA_ACTIVITY_TIMEOUT_MOBILE is set, so the default should be ignored.
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+
+ verify(netd).idletimerAddInterface(eq(WIFI_IFNAME), eq(settingsTimeout), anyString())
+ }
+
+ @Test
+ fun testWifiIdleTimerDefaultTimeout() {
+ val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val wifiLp = LinkProperties().apply {
+ interfaceName = WIFI_IFNAME
+ }
+ val testTimeout: Int = deps.defaultWifiDataInactivityTimeoutForTest
+ // DATA_ACTIVITY_TIMEOUT_WIFI is not set, so the default should be used.
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+
+ verify(netd).idletimerAddInterface(eq(WIFI_IFNAME), eq(testTimeout), anyString())
+ }
+
+ @Test
+ fun testWifiIdleTimerDisabled() {
+ val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val wifiLp = LinkProperties().apply {
+ interfaceName = WIFI_IFNAME
+ }
+ setWifiDataActivityTimeout(0)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+
+ verify(netd, never()).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(), anyString())
+ }
}
+
internal fun CSContext.expectDataActivityBroadcast(
deviceType: Int,
isActive: Boolean,
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
new file mode 100644
index 0000000..fc2a06c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
@@ -0,0 +1,636 @@
+/*
+ * 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.connectivityservice
+
+import android.app.ActivityManager.UidFrozenStateChangedCallback
+import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN
+import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN
+import android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER
+import android.net.ConnectivityManager.BLOCKED_REASON_NONE
+import android.net.ConnectivitySettingsManager
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.LocalNetworkConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkPolicyManager.NetworkPolicyCallback
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.server.CALLING_UID_UNMOCKED
+import com.android.server.CSAgentWrapper
+import com.android.server.CSTest
+import com.android.server.FromS
+import com.android.server.HANDLER_TIMEOUT_MS
+import com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS
+import com.android.server.defaultLp
+import com.android.server.defaultNc
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
+import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.visibleOnHandlerThread
+import com.android.testutils.waitForIdleSerialExecutor
+import java.util.Collections
+import kotlin.test.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+
+private const val TEST_UID = 42
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class CSQueuedCallbacksTest(freezingBehavior: FreezingBehavior) : CSTest() {
+ companion object {
+ enum class FreezingBehavior {
+ UID_FROZEN,
+ UID_NOT_FROZEN,
+ UID_FROZEN_FEATURE_DISABLED
+ }
+
+ // Use a parameterized test with / without freezing to make it easy to compare and make sure
+ // freezing behavior (which callbacks are sent in which order) stays close to what happens
+ // without freezing.
+ @JvmStatic
+ @Parameterized.Parameters(name = "freezingBehavior={0}")
+ fun freezingBehavior() = listOf(
+ FreezingBehavior.UID_FROZEN,
+ FreezingBehavior.UID_NOT_FROZEN,
+ FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+ )
+
+ private val TAG = CSQueuedCallbacksTest::class.simpleName
+ ?: fail("Could not get test class name")
+ }
+
+ @get:Rule
+ val ignoreRule = DevSdkIgnoreRule()
+
+ private val mockedBlockedReasonsPerUid = Collections.synchronizedMap(mutableMapOf(
+ Process.myUid() to BLOCKED_REASON_NONE,
+ TEST_UID to BLOCKED_REASON_NONE
+ ))
+
+ private val freezeUids = freezingBehavior != FreezingBehavior.UID_NOT_FROZEN
+ private val expectAllCallbacks = freezingBehavior == FreezingBehavior.UID_NOT_FROZEN ||
+ freezingBehavior == FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+ init {
+ setFeatureEnabled(
+ QUEUE_CALLBACKS_FOR_FROZEN_APPS,
+ freezingBehavior != FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+ )
+ }
+
+ @Before
+ fun subclassSetUp() {
+ // Ensure cellular stays up. CS is recreated for each test so no cleanup is necessary.
+// cm.requestNetwork(
+// NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build(),
+// TestableNetworkCallback()
+// )
+ }
+
+ @Test
+ fun testFrozenWhileNetworkConnects_UpdatesAreReceived() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ val lpChangeOnConnect = agent.sendLpChange { setLinkAddresses("fe80:db8::123/64") }
+ val ncChangeOnConnect = agent.sendNcChange {
+ addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ }
+
+ maybeSetUidsFrozen(true, TEST_UID)
+
+ val lpChange1WhileFrozen = agent.sendLpChange {
+ setLinkAddresses("fe80:db8::126/64")
+ }
+ val ncChange1WhileFrozen = agent.sendNcChange {
+ removeCapability(NET_CAPABILITY_NOT_ROAMING)
+ }
+ val ncChange2WhileFrozen = agent.sendNcChange {
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ }
+ val lpChange2WhileFrozen = agent.sendLpChange {
+ setLinkAddresses("fe80:db8::125/64")
+ }
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ // Verify callbacks that are sent before freezing
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ cb.expectLpWith(agent, lpChangeOnConnect)
+ cb.expectNcWith(agent, ncChangeOnConnect)
+
+ // Below callbacks should be skipped if the processes were frozen, since a single callback
+ // will be sent with the latest state after unfreezing
+ if (expectAllCallbacks) {
+ cb.expectLpWith(agent, lpChange1WhileFrozen)
+ cb.expectNcWith(agent, ncChange1WhileFrozen)
+ }
+
+ cb.expectNcWith(agent, ncChange2WhileFrozen)
+ cb.expectLpWith(agent, lpChange2WhileFrozen)
+
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testFrozenWhileNetworkConnects_SuspendedUnsuspendedWhileFrozen() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val rmCap = agent.sendNcChange { removeCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+ val addCap = agent.sendNcChange { addCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expectNcWith(agent, rmCap)
+ cb.expect<Suspended>(agent)
+ cb.expectNcWith(agent, addCap)
+ cb.expect<Resumed>(agent)
+ } else {
+ // When frozen, a single NetworkCapabilitiesChange will be sent at unfreezing time,
+ // with nc actually identical to the original ones. This is because NetworkCapabilities
+ // callbacks were sent, but CS does not keep initial NetworkCapabilities in memory, so
+ // it cannot detect A->B->A.
+ cb.expect<CapabilitiesChanged>(agent) {
+ it.caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ }
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testFrozenWhileNetworkConnects_UnsuspendedWhileFrozen_GetResumedCallbackWhenUnfrozen() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ val rmCap = agent.sendNcChange { removeCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val addCap = agent.sendNcChange { addCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ cb.expectNcWith(agent, rmCap)
+ cb.expect<Suspended>(agent)
+ cb.expectNcWith(agent, addCap)
+ cb.expect<Resumed>(agent)
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testFrozenWhileNetworkConnects_BlockedUnblockedWhileFrozen_SingleCallbackIfFrozen() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+
+ maybeSetUidsFrozen(true, TEST_UID)
+ setUidsBlockedForDataSaver(true, TEST_UID)
+ setUidsBlockedForDataSaver(false, TEST_UID)
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expect<BlockedStatus>(agent) { it.blocked }
+ }
+ // The unblocked callback is sent in any case (with the latest blocked reason), as the
+ // blocked reason may have changed, and ConnectivityService cannot know that it is the same
+ // as the original reason as it does not keep pre-freeze blocked reasons in memory.
+ cb.expect<BlockedStatus>(agent) { !it.blocked }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testFrozenWhileNetworkConnects_BlockedWhileFrozen_GetLastBlockedCallbackOnlyIfFrozen() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+
+ maybeSetUidsFrozen(true, TEST_UID)
+ setUidsBlockedForDataSaver(true, TEST_UID)
+ setUidsBlockedForDataSaver(false, TEST_UID)
+ setUidsBlockedForDataSaver(true, TEST_UID)
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expect<BlockedStatus>(agent) { it.blocked }
+ cb.expect<BlockedStatus>(agent) { !it.blocked }
+ }
+ cb.expect<BlockedStatus>(agent) { it.blocked }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testNetworkCallback_NetworkToggledWhileFrozen_NotSeen() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+ val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+ wifiAgent.disconnect()
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ cb.expect<Lost>(wifiAgent)
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testNetworkCallback_NetworkAppearedWhileFrozen_ReceiveLatestInfoInOnAvailable() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+ }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ waitForIdle()
+ agent.makeValidationSuccess()
+ val lpChange = agent.sendLpChange {
+ setLinkAddresses("fe80:db8::123/64")
+ }
+ val suspendedChange = agent.sendNcChange {
+ removeCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ }
+ setUidsBlockedForDataSaver(true, TEST_UID)
+
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ val expectLatestStatusInOnAvailable = !expectAllCallbacks
+ cb.expectAvailableCallbacks(
+ agent.network,
+ suspended = expectLatestStatusInOnAvailable,
+ validated = expectLatestStatusInOnAvailable,
+ blocked = expectLatestStatusInOnAvailable
+ )
+ if (expectAllCallbacks) {
+ cb.expectNcWith(agent) { addCapability(NET_CAPABILITY_VALIDATED) }
+ cb.expectLpWith(agent, lpChange)
+ cb.expectNcWith(agent, suspendedChange)
+ cb.expect<Suspended>(agent)
+ cb.expect<BlockedStatus>(agent) { it.blocked }
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testNetworkCallback_LocalNetworkAppearedWhileFrozen_ReceiveLatestInfoInOnAvailable() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerNetworkCallback(
+ NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build(),
+ cb
+ )
+ }
+ val upstreamAgent = Agent(
+ nc = defaultNc()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET),
+ lp = defaultLp().apply { interfaceName = "wlan0" }
+ ).apply { connect() }
+ maybeSetUidsFrozen(true, TEST_UID)
+
+ val lnc = LocalNetworkConfig.Builder().build()
+ val localAgent = Agent(
+ nc = defaultNc()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .removeCapability(NET_CAPABILITY_INTERNET),
+ lp = defaultLp().apply { interfaceName = "local42" },
+ lnc = FromS(lnc)
+ ).apply { connect() }
+ localAgent.sendLocalNetworkConfig(
+ LocalNetworkConfig.Builder()
+ .setUpstreamSelector(
+ NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ )
+ .build()
+ )
+
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(
+ localAgent.network,
+ validated = false,
+ upstream = if (expectAllCallbacks) null else upstreamAgent.network
+ )
+ if (expectAllCallbacks) {
+ cb.expect<LocalInfoChanged>(localAgent) {
+ it.info.upstreamNetwork == upstreamAgent.network
+ }
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testNetworkRequest_NetworkSwitchesWhileFrozen_ReceiveLastNetworkUpdatesOnly() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.requestNetwork(NetworkRequest.Builder().build(), cb)
+ }
+ val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+ val ethAgent = Agent(TRANSPORT_ETHERNET).apply { connect() }
+ waitForIdle()
+ ethAgent.makeValidationSuccess()
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ cb.expectAvailableCallbacks(ethAgent.network, validated = false)
+ cb.expectNcWith(ethAgent) { addCapability(NET_CAPABILITY_VALIDATED) }
+ } else {
+ cb.expectAvailableCallbacks(ethAgent.network, validated = true)
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testNetworkRequest_NetworkSwitchesBackWhileFrozen_ReceiveNoAvailableCallback() {
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.requestNetwork(NetworkRequest.Builder().build(), cb)
+ }
+ val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+ maybeSetUidsFrozen(true, TEST_UID)
+ val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+ waitForIdle()
+
+ // CS switches back to validated cell over non-validated Wi-Fi
+ cellAgent.makeValidationSuccess()
+ val cellLpChange = cellAgent.sendLpChange {
+ setLinkAddresses("fe80:db8::123/64")
+ }
+ setUidsBlockedForDataSaver(true, TEST_UID)
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ // There is an extra "double validated" CapabilitiesChange callback (b/245893397), so
+ // callbacks are (AVAIL, NC, LP), extra NC, then further updates (LP and BLK here).
+ cb.expectAvailableDoubleValidatedCallbacks(cellAgent.network)
+ cb.expectLpWith(cellAgent, cellLpChange)
+ cb.expect<BlockedStatus>(cellAgent) { it.blocked }
+ } else {
+ cb.expectNcWith(cellAgent) {
+ addCapability(NET_CAPABILITY_VALIDATED)
+ }
+ cb.expectLpWith(cellAgent, cellLpChange)
+ cb.expect<BlockedStatus>(cellAgent) { it.blocked }
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testTrackDefaultRequest_AppFrozenWhilePerAppDefaultRequestFiled_ReceiveChangeCallbacks() {
+ val cellAgent = Agent(TRANSPORT_CELLULAR, baseNc = makeInternetNc()).apply { connect() }
+ waitForIdle()
+
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerDefaultNetworkCallback(cb)
+ }
+ maybeSetUidsFrozen(true, TEST_UID)
+
+ // Change LinkProperties twice before the per-app network request is applied
+ val lpChange1 = cellAgent.sendLpChange {
+ setLinkAddresses("fe80:db8::123/64")
+ }
+ val lpChange2 = cellAgent.sendLpChange {
+ setLinkAddresses("fe80:db8::124/64")
+ }
+ setMobileDataPreferredUids(setOf(TEST_UID))
+
+ // Change NetworkCapabilities after the per-app network request is applied
+ val ncChange = cellAgent.sendNcChange {
+ addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ }
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ // Even if a per-app network request was filed to replace the default network request for
+ // the app, all network change callbacks are received
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ if (expectAllCallbacks) {
+ cb.expectLpWith(cellAgent, lpChange1)
+ }
+ cb.expectLpWith(cellAgent, lpChange2)
+ cb.expectNcWith(cellAgent, ncChange)
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ @Test
+ fun testTrackDefaultRequest_AppFrozenWhilePerAppDefaultToggled_GetStatusUpdateCallbacksOnly() {
+ // Add validated Wi-Fi and non-validated cell, expect Wi-Fi is preferred by default
+ val wifiAgent = Agent(TRANSPORT_WIFI, baseNc = makeInternetNc()).apply { connect() }
+ wifiAgent.makeValidationSuccess()
+ val cellAgent = Agent(TRANSPORT_CELLULAR, baseNc = makeInternetNc()).apply { connect() }
+ waitForIdle()
+
+ val cb = TestableNetworkCallback(logTag = TAG)
+ withCallingUid(TEST_UID) {
+ cm.registerDefaultNetworkCallback(cb)
+ }
+ maybeSetUidsFrozen(true, TEST_UID)
+
+ // LP change on the original Wi-Fi network
+ val lpChange = wifiAgent.sendLpChange {
+ setLinkAddresses("fe80:db8::123/64")
+ }
+ // Set per-app default to cell, then unset it
+ setMobileDataPreferredUids(setOf(TEST_UID))
+ setMobileDataPreferredUids(emptySet())
+
+ maybeSetUidsFrozen(false, TEST_UID)
+
+ cb.expectAvailableCallbacks(wifiAgent.network)
+ if (expectAllCallbacks) {
+ cb.expectLpWith(wifiAgent, lpChange)
+ cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+ // Cellular stops being foreground since it is now matched for this app
+ cb.expect<CapabilitiesChanged> { it.caps.hasCapability(NET_CAPABILITY_FOREGROUND) }
+ cb.expectAvailableCallbacks(wifiAgent.network)
+ } else {
+ // After switching to cell and back while frozen, only network attribute update
+ // callbacks (and not AVAILABLE) for the original Wi-Fi network should be sent
+ cb.expect<CapabilitiesChanged>(wifiAgent)
+ cb.expectLpWith(wifiAgent, lpChange)
+ cb.expect<BlockedStatus> { !it.blocked }
+ }
+ cb.assertNoCallback(timeoutMs = 0L)
+ }
+
+ private fun setUidsBlockedForDataSaver(blocked: Boolean, vararg uid: Int) {
+ val reason = if (blocked) {
+ BLOCKED_METERED_REASON_DATA_SAVER
+ } else {
+ BLOCKED_REASON_NONE
+ }
+ if (deps.isAtLeastV) {
+ visibleOnHandlerThread(csHandler) {
+ service.handleBlockedReasonsChanged(uid.map { android.util.Pair(it, reason) })
+ }
+ } else {
+ notifyLegacyBlockedReasonChanged(reason, uid)
+ waitForIdle()
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ private fun notifyLegacyBlockedReasonChanged(reason: Int, uids: IntArray) {
+ val cbCaptor = ArgumentCaptor.forClass(NetworkPolicyCallback::class.java)
+ verify(context.networkPolicyManager).registerNetworkPolicyCallback(
+ any(),
+ cbCaptor.capture()
+ )
+ uids.forEach {
+ cbCaptor.value.onUidBlockedReasonChanged(it, reason)
+ }
+ }
+
+ private fun withCallingUid(uid: Int, action: () -> Unit) {
+ deps.callingUid = uid
+ action()
+ deps.callingUid = CALLING_UID_UNMOCKED
+ }
+
+ private fun getUidFrozenStateChangedCallback(): UidFrozenStateChangedCallback {
+ val captor = ArgumentCaptor.forClass(UidFrozenStateChangedCallback::class.java)
+ verify(activityManager).registerUidFrozenStateChangedCallback(any(), captor.capture())
+ return captor.value
+ }
+
+ private fun maybeSetUidsFrozen(frozen: Boolean, vararg uids: Int) {
+ if (!freezeUids) return
+ val state = if (frozen) UID_FROZEN_STATE_FROZEN else UID_FROZEN_STATE_UNFROZEN
+ getUidFrozenStateChangedCallback()
+ .onUidFrozenStateChanged(uids, IntArray(uids.size) { state })
+ waitForIdle()
+ }
+
+ private fun CSAgentWrapper.makeValidationSuccess() {
+ setValidationResult(
+ NETWORK_VALIDATION_RESULT_VALID,
+ probesCompleted = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTPS,
+ probesSucceeded = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTPS
+ )
+ cm.reportNetworkConnectivity(network, true)
+ // Ensure validation is scheduled
+ waitForIdle()
+ // Ensure validation completes on mock executor
+ waitForIdleSerialExecutor(CSTestExecutor, HANDLER_TIMEOUT_MS)
+ // Ensure validation results are processed
+ waitForIdle()
+ }
+
+ private fun setMobileDataPreferredUids(uids: Set<Int>) {
+ ConnectivitySettingsManager.setMobileDataPreferredUids(context, uids)
+ service.updateMobileDataPreferredUids()
+ waitForIdle()
+ }
+}
+
+private fun makeInternetNc() = NetworkCapabilities.Builder(defaultNc())
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+
+private fun CSAgentWrapper.sendLpChange(
+ mutator: LinkProperties.() -> Unit
+): LinkProperties.() -> Unit {
+ lp.mutator()
+ sendLinkProperties(lp)
+ return mutator
+}
+
+private fun CSAgentWrapper.sendNcChange(
+ mutator: NetworkCapabilities.() -> Unit
+): NetworkCapabilities.() -> Unit {
+ nc.mutator()
+ sendNetworkCapabilities(nc)
+ return mutator
+}
+
+private fun TestableNetworkCallback.expectLpWith(
+ agent: CSAgentWrapper,
+ change: LinkProperties.() -> Unit
+) = expect<LinkPropertiesChanged>(agent) {
+ // This test uses changes that are no-op when already applied (idempotent): verify that the
+ // change is already applied.
+ it.lp == LinkProperties(it.lp).apply(change)
+}
+
+private fun TestableNetworkCallback.expectNcWith(
+ agent: CSAgentWrapper,
+ change: NetworkCapabilities.() -> Unit
+) = expect<CapabilitiesChanged>(agent) {
+ it.caps == NetworkCapabilities(it.caps).apply(change)
+}
+
+private fun LinkProperties.setLinkAddresses(vararg addrs: String) {
+ setLinkAddresses(addrs.map { LinkAddress(it) })
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
index 88c2738..58420c0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -54,6 +54,7 @@
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@@ -162,6 +163,65 @@
doTestSatelliteNeverBecomeDefaultNetwork(restricted = false)
}
+ private fun doTestUnregisterAfterReplacementSatisfier(destroyBeforeRequest: Boolean = false,
+ destroyAfterRequest: Boolean = false) {
+ val satelliteAgent = createSatelliteAgent("satellite0")
+ satelliteAgent.connect()
+
+ if (destroyBeforeRequest) {
+ satelliteAgent.unregisterAfterReplacement(timeoutMs = 5000)
+ }
+
+ val uids = setOf(TEST_PACKAGE_UID)
+ updateSatelliteNetworkFallbackUids(uids)
+
+ if (destroyBeforeRequest) {
+ verify(netd, never()).networkAddUidRangesParcel(any())
+ } else {
+ verify(netd).networkAddUidRangesParcel(
+ NativeUidRangeConfig(
+ satelliteAgent.network.netId,
+ toUidRangeStableParcels(uidRangesForUids(uids)),
+ PREFERENCE_ORDER_SATELLITE_FALLBACK
+ )
+ )
+ }
+
+ if (destroyAfterRequest) {
+ satelliteAgent.unregisterAfterReplacement(timeoutMs = 5000)
+ }
+
+ updateSatelliteNetworkFallbackUids(setOf())
+ if (destroyBeforeRequest || destroyAfterRequest) {
+ // If the network is already destroyed, networkRemoveUidRangesParcel should not be
+ // called.
+ verify(netd, never()).networkRemoveUidRangesParcel(any())
+ } else {
+ verify(netd).networkRemoveUidRangesParcel(
+ NativeUidRangeConfig(
+ satelliteAgent.network.netId,
+ toUidRangeStableParcels(uidRangesForUids(uids)),
+ PREFERENCE_ORDER_SATELLITE_FALLBACK
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testUnregisterAfterReplacementSatisfier_destroyBeforeRequest() {
+ doTestUnregisterAfterReplacementSatisfier(destroyBeforeRequest = true)
+ }
+
+ @Test
+ fun testUnregisterAfterReplacementSatisfier_destroyAfterRequest() {
+ doTestUnregisterAfterReplacementSatisfier(destroyAfterRequest = true)
+ }
+
+ @Test
+ fun testUnregisterAfterReplacementSatisfier_notDestroyed() {
+ doTestUnregisterAfterReplacementSatisfier()
+ }
+
private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
val nris: Set<ConnectivityService.NetworkRequestInfo> =
service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(uids)
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index 13c5cbc..9be7d11 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -181,6 +181,7 @@
cb.eventuallyExpect<Lost> { it.network == agent.network }
}
+ fun setTeardownDelayMillis(delayMillis: Int) = agent.setTeardownDelayMillis(delayMillis)
fun unregisterAfterReplacement(timeoutMs: Int) = agent.unregisterAfterReplacement(timeoutMs)
fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
@@ -192,7 +193,8 @@
connect()
}
- fun setProbesStatus(probesCompleted: Int, probesSucceeded: Int) {
+ fun setValidationResult(result: Int, probesCompleted: Int, probesSucceeded: Int) {
+ nmValidationResult = result
nmProbesCompleted = probesCompleted
nmProbesSucceeded = probesSucceeded
}
@@ -204,8 +206,10 @@
// in the beginning. Because NETWORK_VALIDATION_PROBE_HTTP is the decisive probe for captive
// portal, considering the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet and set only
// DNS and HTTP probes completed.
- setProbesStatus(
- NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP /* probesCompleted */,
- VALIDATION_RESULT_INVALID /* probesSucceeded */)
+ setValidationResult(
+ VALIDATION_RESULT_INVALID,
+ probesCompleted = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP,
+ probesSucceeded = NO_PROBE_RESULT
+ )
}
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index de56ae5..ae196a6 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -341,6 +341,18 @@
}
}
+ // Need a non-zero value to avoid disarming the timer.
+ val defaultCellDataInactivityTimeoutForTest: Int = 81
+ override fun getDefaultCellularDataInactivityTimeout(): Int {
+ return defaultCellDataInactivityTimeoutForTest
+ }
+
+ // Need a non-zero value to avoid disarming the timer.
+ val defaultWifiDataInactivityTimeoutForTest: Int = 121
+ override fun getDefaultWifiDataInactivityTimeout(): Int {
+ return defaultWifiDataInactivityTimeoutForTest
+ }
+
override fun isChangeEnabled(changeId: Long, pkg: String, user: UserHandle) =
changeId in enabledChangeIds
override fun isChangeEnabled(changeId: Long, uid: Int) =
@@ -538,8 +550,12 @@
provider: NetworkProvider? = null
) = CSAgentWrapper(context, deps, csHandlerThread, networkStack,
nac, nc, lp, lnc, score, provider)
- fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
- val nc = NetworkCapabilities.Builder().apply {
+ fun Agent(
+ vararg transports: Int,
+ baseNc: NetworkCapabilities = defaultNc(),
+ lp: LinkProperties = defaultLp()
+ ): CSAgentWrapper {
+ val nc = NetworkCapabilities.Builder(baseNc).apply {
transports.forEach {
addTransportType(it)
}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 7e0a225..b528480 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -69,19 +69,23 @@
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_UPDATED;
+import static com.android.server.net.NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG;
import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
-import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
+import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES;
import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME;
-import static com.android.server.net.NetworkStatsService.TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG;
+import static com.android.server.net.NetworkStatsService.TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG;
+import static com.android.server.net.NetworkStatsService.TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.aryEq;
@@ -101,8 +105,10 @@
import android.annotation.NonNull;
import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.ContentObserver;
@@ -122,10 +128,12 @@
import android.net.TestNetworkSpecifier;
import android.net.TetherStatsParcel;
import android.net.TetheringManager;
-import android.net.TrafficStats;
import android.net.UnderlyingNetworkInfo;
+import android.net.netstats.StatsResult;
+import android.net.netstats.TrafficStatsRateLimitCacheConfig;
import android.net.netstats.provider.INetworkStatsProviderCallback;
import android.net.wifi.WifiInfo;
+import android.os.Build;
import android.os.DropBoxManager;
import android.os.Handler;
import android.os.HandlerThread;
@@ -138,6 +146,7 @@
import android.provider.Settings;
import android.system.ErrnoException;
import android.telephony.TelephonyManager;
+import android.testing.TestableLooper;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
@@ -150,6 +159,7 @@
import com.android.connectivity.resources.R;
import com.android.internal.util.FileRotator;
import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.net.module.util.ArrayTrackRecord;
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.LocationPermissionChecker;
@@ -201,7 +211,6 @@
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Function;
/**
* Tests for {@link NetworkStatsService}.
@@ -215,6 +224,8 @@
// NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
private static final String TAG = "NetworkStatsServiceTest";
@@ -613,8 +624,16 @@
}
@Override
- public boolean alwaysUseTrafficStatsRateLimitCache(Context ctx) {
- return mFeatureFlags.getOrDefault(TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
+ public boolean isTrafficStatsServiceRateLimitCacheEnabled(Context ctx,
+ boolean isClientCacheEnabled) {
+ return !isClientCacheEnabled && mFeatureFlags.getOrDefault(
+ TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
+ }
+
+ @Override
+ public boolean enabledBroadcastNetworkStatsUpdatedRateLimiting(Context ctx) {
+ return mFeatureFlags.getOrDefault(
+ BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG, true);
}
@Override
@@ -623,8 +642,21 @@
}
@Override
- public int getTrafficStatsRateLimitCacheMaxEntries() {
- return DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
+ public int getTrafficStatsServiceRateLimitCacheMaxEntries() {
+ return DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES;
+ }
+
+ @Override
+ public TrafficStatsRateLimitCacheConfig getTrafficStatsRateLimitCacheClientSideConfig(
+ @NonNull Context ctx) {
+ final TrafficStatsRateLimitCacheConfig config =
+ new TrafficStatsRateLimitCacheConfig.Builder()
+ .setIsCacheEnabled(mFeatureFlags.getOrDefault(
+ TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, false))
+ .setExpiryDurationMs(DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS)
+ .setMaxEntries(DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES)
+ .build();
+ return config;
}
@Override
@@ -2438,30 +2470,79 @@
assertUidTotal(sTemplateWifi, UID_GREEN, 64L, 3L, 1024L, 8L, 0);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @Test
+ public void testGetRateLimitCacheConfig_featureDisabled() {
+ mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
+ assertFalse(mService.getRateLimitCacheConfig().isCacheEnabled);
+ mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
+ assertFalse(mService.getRateLimitCacheConfig().isCacheEnabled);
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ public void testGetRateLimitCacheConfig_vOrAbove() {
+ mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
+ assertTrue(mService.getRateLimitCacheConfig().isCacheEnabled);
+ mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
+ assertTrue(mService.getRateLimitCacheConfig().isCacheEnabled);
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ public void testGetRateLimitCacheConfig_belowV() {
+ mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
+ assertFalse(mService.getRateLimitCacheConfig().isCacheEnabled);
+ mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
+ assertTrue(mService.getRateLimitCacheConfig().isCacheEnabled);
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @Test
+ public void testTrafficStatsRateLimitCache_clientCacheEnabledDisableServiceCache()
+ throws Exception {
+ mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
+ doTestTrafficStatsRateLimitCache(false /* expectCached */);
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
@Test
public void testTrafficStatsRateLimitCache_disabledWithCompatChangeEnabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
- doTestTrafficStatsRateLimitCache(true /* expectCached */);
+ doTestTrafficStatsRateLimitCache(false /* expectCached */);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG)
@Test
public void testTrafficStatsRateLimitCache_enabledWithCompatChangeEnabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
doTestTrafficStatsRateLimitCache(true /* expectCached */);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
@Test
public void testTrafficStatsRateLimitCache_disabledWithCompatChangeDisabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
doTestTrafficStatsRateLimitCache(false /* expectCached */);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
- public void testTrafficStatsRateLimitCache_enabledWithCompatChangeDisabled() throws Exception {
+ public void testTrafficStatsRateLimitCache_enabledWithCompatChangeDisabled_belowV()
+ throws Exception {
+ mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
+ doTestTrafficStatsRateLimitCache(false /* expectCached */);
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ public void testTrafficStatsRateLimitCache_enabledWithCompatChangeDisabled_vOrAbove()
+ throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
doTestTrafficStatsRateLimitCache(true /* expectCached */);
}
@@ -2500,20 +2581,18 @@
// Assert for 3 different API return values respectively.
private void assertTrafficStatsValues(String iface, int uid, long rxBytes, long rxPackets,
long txBytes, long txPackets) {
- assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> mService.getTotalStats(type));
- assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> mService.getIfaceStats(iface, type));
- assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> mService.getUidStats(uid, type));
+ assertStatsResultEquals(mService.getTotalStats(), rxBytes, rxPackets, txBytes, txPackets);
+ assertStatsResultEquals(mService.getIfaceStats(iface), rxBytes, rxPackets, txBytes,
+ txPackets);
+ assertStatsResultEquals(mService.getUidStats(uid), rxBytes, rxPackets, txBytes, txPackets);
}
- private void assertTrafficStatsValuesThat(long rxBytes, long rxPackets, long txBytes,
- long txPackets, Function<Integer, Long> fetcher) {
- assertEquals(rxBytes, (long) fetcher.apply(TrafficStats.TYPE_RX_BYTES));
- assertEquals(rxPackets, (long) fetcher.apply(TrafficStats.TYPE_RX_PACKETS));
- assertEquals(txBytes, (long) fetcher.apply(TrafficStats.TYPE_TX_BYTES));
- assertEquals(txPackets, (long) fetcher.apply(TrafficStats.TYPE_TX_PACKETS));
+ private void assertStatsResultEquals(StatsResult stats, long rxBytes, long rxPackets,
+ long txBytes, long txPackets) {
+ assertEquals(rxBytes, stats.rxBytes);
+ assertEquals(rxPackets, stats.rxPackets);
+ assertEquals(txBytes, stats.txBytes);
+ assertEquals(txPackets, stats.txPackets);
}
private void assertShouldRunComparison(boolean expected, boolean isDebuggable) {
@@ -2617,6 +2696,8 @@
private void mockDefaultSettings() throws Exception {
mockSettings(HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+ mSettings.setBroadcastNetworkStatsUpdateDelayMs(
+ NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS);
}
private void mockSettings(long bucketDuration, long deleteAge) {
@@ -2631,6 +2712,8 @@
@NonNull
private volatile Config mConfig;
private final AtomicBoolean mCombineSubtypeEnabled = new AtomicBoolean();
+ private long mBroadcastNetworkStatsUpdateDelayMs =
+ NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
TestNetworkStatsSettings(long bucketDuration, long deleteAge) {
mConfig = new Config(bucketDuration, deleteAge, deleteAge);
@@ -2693,6 +2776,15 @@
public boolean getAugmentEnabled() {
return false;
}
+
+ @Override
+ public long getBroadcastNetworkStatsUpdateDelayMs() {
+ return mBroadcastNetworkStatsUpdateDelayMs;
+ }
+
+ public void setBroadcastNetworkStatsUpdateDelayMs(long broadcastDelay) {
+ mBroadcastNetworkStatsUpdateDelayMs = broadcastDelay;
+ }
}
private void assertStatsFilesExist(boolean exist) {
@@ -3064,4 +3156,91 @@
final String dump = getDump();
assertDumpContains(dump, "Log for testing");
}
+
+ private static class TestNetworkStatsUpdatedReceiver extends BroadcastReceiver {
+ private final ArrayTrackRecord<Intent>.ReadHead mHistory;
+
+ TestNetworkStatsUpdatedReceiver() {
+ mHistory = (new ArrayTrackRecord<Intent>()).newReadHead();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mHistory.add(intent);
+ }
+
+ /**
+ * Assert no broadcast intent is received in blocking manner
+ */
+ public void assertNoBroadcastIntentReceived() {
+ assertNull(mHistory.peek());
+ }
+
+ /**
+ * Assert an intent is received and remove it from queue
+ */
+ public void assertBroadcastIntentReceived() {
+ assertNotNull(mHistory.poll(WAIT_TIMEOUT, number -> true));
+ }
+ }
+
+ @FeatureFlag(name = BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG)
+ @Test
+ public void testNetworkStatsUpdatedIntentSpam_rateLimitOn() throws Exception {
+ // Set the update delay long enough that messages won't be processed before unblocked
+ // Set a short time to test the behavior before reaching delay.
+ // Constraint: test running time < toleranceMs < update delay time
+ mSettings.setBroadcastNetworkStatsUpdateDelayMs(100_000L);
+ final long toleranceMs = 5000;
+
+ final TestableLooper mTestableLooper = new TestableLooper(mHandlerThread.getLooper());
+ final TestNetworkStatsUpdatedReceiver receiver = new TestNetworkStatsUpdatedReceiver();
+ mServiceContext.registerReceiver(receiver, new IntentFilter(ACTION_NETWORK_STATS_UPDATED));
+
+ try {
+ // Test that before anything, the intent is delivered immediately
+ mService.forceUpdate();
+ mTestableLooper.processAllMessages();
+ receiver.assertBroadcastIntentReceived();
+ receiver.assertNoBroadcastIntentReceived();
+
+ // Test that the next two intents results in exactly one intent delivered
+ for (int i = 0; i < 2; i++) {
+ mService.forceUpdate();
+ }
+ // Test that the delay depends on our set value
+ mTestableLooper.moveTimeForward(mSettings.getBroadcastNetworkStatsUpdateDelayMs()
+ - toleranceMs);
+ mTestableLooper.processAllMessages();
+ receiver.assertNoBroadcastIntentReceived();
+
+ // Unblock messages and test that the second and third update
+ // is broadcasted right after the delay
+ mTestableLooper.moveTimeForward(toleranceMs);
+ mTestableLooper.processAllMessages();
+ receiver.assertBroadcastIntentReceived();
+ receiver.assertNoBroadcastIntentReceived();
+
+ } finally {
+ mTestableLooper.destroy();
+ }
+ }
+
+ @FeatureFlag(name = BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG, enabled = false)
+ @Test
+ public void testNetworkStatsUpdatedIntentSpam_rateLimitOff() throws Exception {
+ // Set the update delay long enough to ensure that messages are processed
+ // despite the rate limit.
+ mSettings.setBroadcastNetworkStatsUpdateDelayMs(100_000L);
+
+ final TestNetworkStatsUpdatedReceiver receiver = new TestNetworkStatsUpdatedReceiver();
+ mServiceContext.registerReceiver(receiver, new IntentFilter(ACTION_NETWORK_STATS_UPDATED));
+
+ for (int i = 0; i < 2; i++) {
+ mService.forceUpdate();
+ waitForIdle();
+ receiver.assertBroadcastIntentReceived();
+ }
+ receiver.assertNoBroadcastIntentReceived();
+ }
}
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 57a157d..50971e7 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -42,6 +42,7 @@
],
static_libs: [
"libnet_utils_device_common_bpfjni",
+ "libnet_utils_device_common_timerfdjni",
"libtcutils",
],
shared_libs: [
diff --git a/tests/unit/jni/android_net_frameworktests_util/onload.cpp b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
index 06a3986..a0ce4f8 100644
--- a/tests/unit/jni/android_net_frameworktests_util/onload.cpp
+++ b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
@@ -24,6 +24,8 @@
int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
+int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+ char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv *env;
@@ -38,6 +40,10 @@
if (register_com_android_net_module_util_TcUtils(env,
"android/net/frameworktests/util/TcUtils") < 0) return JNI_ERR;
+ if (register_com_android_net_module_util_TimerFdUtils(
+ env, "android/net/frameworktests/util/TimerFdUtils") < 0)
+ return JNI_ERR;
+
return JNI_VERSION_1_6;
}
diff --git a/thread/README.md b/thread/README.md
index 41b73ac..f2bd3b2 100644
--- a/thread/README.md
+++ b/thread/README.md
@@ -16,3 +16,7 @@
Open `https://localhost:8443/` in your web browser, you can find the Thread
demoapp (with the Thread logo) in the cuttlefish instance. Open it and have fun with Thread!
+
+## More docs
+
+- [Make your Android Border Router](./docs/make-your-android-border-router.md)
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 34d67bb..40842f1 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -13,6 +13,9 @@
"postsubmit": [
{
"name": "ThreadNetworkMultiDeviceTests"
+ },
+ {
+ "name": "ThreadNetworkTrelDisabledTests"
}
]
}
diff --git a/thread/apex/Android.bp b/thread/apex/Android.bp
index edf000a..838c0d9 100644
--- a/thread/apex/Android.bp
+++ b/thread/apex/Android.bp
@@ -23,8 +23,8 @@
// See https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
// for details of versioned rc files.
prebuilt_etc {
- name: "ot-daemon.init.34rc",
+ name: "ot-daemon.34rc",
src: "ot-daemon.34rc",
- filename: "init.34rc",
+ filename: "ot-daemon.34rc",
installable: false,
}
diff --git a/thread/demoapp/Android.bp b/thread/demoapp/Android.bp
index 117b4f9..a786639 100644
--- a/thread/demoapp/Android.bp
+++ b/thread/demoapp/Android.bp
@@ -32,7 +32,7 @@
"guava",
],
libs: [
- "framework-connectivity-t",
+ "framework-connectivity-t.stubs.module_lib",
],
required: [
"privapp-permissions-com.android.threadnetwork.demoapp",
diff --git a/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java b/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java
index e95feaf..ea30e26 100644
--- a/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java
+++ b/thread/demoapp/java/com/android/threadnetwork/demoapp/ThreadNetworkSettingsFragment.java
@@ -28,6 +28,7 @@
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
@@ -45,8 +46,13 @@
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
+import com.google.android.material.switchmaterial.SwitchMaterial;
+
import java.time.Duration;
import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Timer;
+import java.util.TimerTask;
import java.util.concurrent.Executor;
public final class ThreadNetworkSettingsFragment extends Fragment {
@@ -59,11 +65,18 @@
private TextView mTextState;
private TextView mTextNetworkInfo;
private TextView mMigrateNetworkState;
+ private TextView mEphemeralKeyStateText;
+ private SwitchMaterial mNat64Switch;
private Executor mMainExecutor;
private int mDeviceRole;
private long mPartitionId;
private ActiveOperationalDataset mActiveDataset;
+ private int mEphemeralKeyState;
+ private String mEphemeralKey;
+ private Instant mEphemeralKeyExpiry;
+ private Timer mEphemeralKeyLifetimeTimer;
+ private ThreadConfiguration mThreadConfiguration;
private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS =
base16().lowerCase()
@@ -89,6 +102,23 @@
}
}
+ private static String ephemeralKeyStateToString(int ephemeralKeyState) {
+ switch (ephemeralKeyState) {
+ case ThreadNetworkController.EPHEMERAL_KEY_DISABLED:
+ return "Disabled";
+ case ThreadNetworkController.EPHEMERAL_KEY_ENABLED:
+ return "Enabled";
+ case ThreadNetworkController.EPHEMERAL_KEY_IN_USE:
+ return "Connected";
+ default:
+ return "Unknown";
+ }
+ }
+
+ private static String booleanToEnabledOrDisabled(boolean enabled) {
+ return enabled ? "Enabled" : "Disabled";
+ }
+
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -144,6 +174,15 @@
ThreadNetworkSettingsFragment.this.mPartitionId = mPartitionId;
updateState();
}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ int state, String ephemeralKey, Instant expiry) {
+ ThreadNetworkSettingsFragment.this.mEphemeralKeyState = state;
+ ThreadNetworkSettingsFragment.this.mEphemeralKey = ephemeralKey;
+ ThreadNetworkSettingsFragment.this.mEphemeralKeyExpiry = expiry;
+ updateState();
+ }
});
mThreadController.registerOperationalDatasetCallback(
mMainExecutor,
@@ -151,10 +190,16 @@
this.mActiveDataset = newActiveDataset;
updateState();
});
+ mThreadController.registerConfigurationCallback(
+ mMainExecutor, this::updateConfiguration);
}
mTextState = (TextView) view.findViewById(R.id.text_state);
mTextNetworkInfo = (TextView) view.findViewById(R.id.text_network_info);
+ mEphemeralKeyStateText = (TextView) view.findViewById(R.id.text_ephemeral_key_state);
+ mNat64Switch = (SwitchMaterial) view.findViewById(R.id.switch_nat64);
+ mNat64Switch.setOnCheckedChangeListener(
+ (buttonView, isChecked) -> doSetNat64Enabled(isChecked));
if (mThreadController == null) {
mTextState.setText("Thread not supported!");
@@ -168,6 +213,11 @@
((Button) view.findViewById(R.id.button_migrate_network))
.setOnClickListener(v -> doMigration());
+ ((Button) view.findViewById(R.id.button_activate_ephemeral_key_mode))
+ .setOnClickListener(v -> doActivateEphemeralKeyMode());
+ ((Button) view.findViewById(R.id.button_deactivate_ephemeral_key_mode))
+ .setOnClickListener(v -> doDeactivateEphemeralKeyMode());
+
updateState();
}
@@ -234,12 +284,74 @@
});
}
+ private void doActivateEphemeralKeyMode() {
+ mThreadController.activateEphemeralKeyMode(
+ Duration.ofMinutes(2),
+ mMainExecutor,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onError(ThreadNetworkException error) {
+ Log.e(TAG, "Failed to activate ephemeral key", error);
+ }
+
+ @Override
+ public void onResult(Void v) {
+ Log.i(TAG, "Successfully activated ephemeral key mode");
+ }
+ });
+ }
+
+ private void doDeactivateEphemeralKeyMode() {
+ mThreadController.deactivateEphemeralKeyMode(
+ mMainExecutor,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onError(ThreadNetworkException error) {
+ Log.e(TAG, "Failed to deactivate ephemeral key", error);
+ }
+
+ @Override
+ public void onResult(Void v) {
+ Log.i(TAG, "Successfully deactivated ephemeral key mode");
+ }
+ });
+ }
+
+ private void doSetNat64Enabled(boolean enabled) {
+ if (mThreadConfiguration == null) {
+ Log.e(TAG, "Thread configuration is not available");
+ return;
+ }
+ final ThreadConfiguration config =
+ new ThreadConfiguration.Builder(mThreadConfiguration)
+ .setNat64Enabled(enabled)
+ .build();
+ mThreadController.setConfiguration(
+ config,
+ mMainExecutor,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onError(ThreadNetworkException error) {
+ Log.e(
+ TAG,
+ "Failed to set NAT64 " + booleanToEnabledOrDisabled(enabled),
+ error);
+ }
+
+ @Override
+ public void onResult(Void v) {
+ Log.i(TAG, "Successfully set NAT64 " + booleanToEnabledOrDisabled(enabled));
+ }
+ });
+ }
+
private void updateState() {
Log.i(
TAG,
String.format(
- "Updating Thread states (mDeviceRole: %s)",
- deviceRoleToString(mDeviceRole)));
+ "Updating Thread states (mDeviceRole: %s, mEphemeralKeyState: %s)",
+ deviceRoleToString(mDeviceRole),
+ ephemeralKeyStateToString(mEphemeralKeyState)));
String state =
String.format(
@@ -254,6 +366,30 @@
? base16().encode(mActiveDataset.getExtendedPanId())
: null);
mTextState.setText(state);
+
+ updateEphemeralKeyStatus();
+ }
+
+ private void updateEphemeralKeyStatus() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(ephemeralKeyStateToString(mEphemeralKeyState));
+ if (mEphemeralKeyState != ThreadNetworkController.EPHEMERAL_KEY_DISABLED) {
+ sb.append("\nPasscode: ");
+ sb.append(mEphemeralKey);
+ sb.append("\nRemaining lifetime: ");
+ sb.append(Instant.now().until(mEphemeralKeyExpiry, ChronoUnit.SECONDS));
+ sb.append(" seconds");
+ mEphemeralKeyLifetimeTimer = new Timer();
+ mEphemeralKeyLifetimeTimer.schedule(
+ new TimerTask() {
+ @Override
+ public void run() {
+ mMainExecutor.execute(() -> updateEphemeralKeyStatus());
+ }
+ },
+ 1000L /* delay in millis */);
+ }
+ mEphemeralKeyStateText.setText(sb.toString());
}
private void updateNetworkInfo(LinkProperties linProperties) {
@@ -274,4 +410,11 @@
}
mTextNetworkInfo.setText(sb.toString());
}
+
+ private void updateConfiguration(ThreadConfiguration config) {
+ Log.i(TAG, "Updating configuration: " + config);
+
+ mThreadConfiguration = config;
+ mNat64Switch.setChecked(config.isNat64Enabled());
+ }
}
diff --git a/thread/demoapp/res/layout/main_activity.xml b/thread/demoapp/res/layout/main_activity.xml
index 12072e5..d874db1 100644
--- a/thread/demoapp/res/layout/main_activity.xml
+++ b/thread/demoapp/res/layout/main_activity.xml
@@ -21,6 +21,7 @@
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
tools:context=".MainActivity">
<LinearLayout
diff --git a/thread/demoapp/res/layout/thread_network_settings_fragment.xml b/thread/demoapp/res/layout/thread_network_settings_fragment.xml
index cae46a3..47ce62a 100644
--- a/thread/demoapp/res/layout/thread_network_settings_fragment.xml
+++ b/thread/demoapp/res/layout/thread_network_settings_fragment.xml
@@ -14,58 +14,99 @@
limitations under the License.
-->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:padding="8dp"
- android:orientation="vertical"
- tools:context=".ThreadNetworkSettingsFragment" >
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <LinearLayout
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:orientation="vertical"
+ tools:context=".ThreadNetworkSettingsFragment" >
- <Button android:id="@+id/button_join_network"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Join Network" />
- <Button android:id="@+id/button_leave_network"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Leave Network" />
+ <Button android:id="@+id/button_join_network"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Join Network" />
+ <Button android:id="@+id/button_leave_network"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Leave Network" />
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="16dp"
- android:textStyle="bold"
- android:text="State" />
- <TextView
- android:id="@+id/text_state"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="12dp"
- android:typeface="monospace" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="16sp"
+ android:textStyle="bold"
+ android:text="State" />
+ <TextView
+ android:id="@+id/text_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:typeface="monospace" />
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="10dp"
- android:textSize="16dp"
- android:textStyle="bold"
- android:text="Network Info" />
- <TextView
- android:id="@+id/text_network_info"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="12dp" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:textSize="16sp"
+ android:textStyle="bold"
+ android:text="Network Info" />
+ <TextView
+ android:id="@+id/text_network_info"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
- <Button android:id="@+id/button_migrate_network"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Migrate Network" />
- <TextView
- android:id="@+id/text_migrate_network_state"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="12dp" />
-</LinearLayout>
+ <Button android:id="@+id/button_migrate_network"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Migrate Network" />
+ <TextView
+ android:id="@+id/text_migrate_network_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+
+ <Button android:id="@+id/button_activate_ephemeral_key_mode"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Activate Ephemeral Key Mode" />
+ <Button android:id="@+id/button_deactivate_ephemeral_key_mode"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Deactivate Ephemeral Key Mode" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:textSize="16sp"
+ android:textStyle="bold"
+ android:text="Ephemeral Key State" />
+ <TextView
+ android:id="@+id/text_ephemeral_key_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+
+ <TextView
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="Configuration"
+ android:textSize="16sp"
+ android:textStyle="bold" />
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/switch_nat64"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:checked="false"
+ android:text="NAT64" />
+
+ </LinearLayout>
+</ScrollView>
diff --git a/thread/docs/android-thread-arch.png b/thread/docs/android-thread-arch.png
new file mode 100644
index 0000000..ea408fa
--- /dev/null
+++ b/thread/docs/android-thread-arch.png
Binary files differ
diff --git a/thread/docs/build-an-android-border-router.md b/thread/docs/build-an-android-border-router.md
new file mode 100644
index 0000000..f90a23b
--- /dev/null
+++ b/thread/docs/build-an-android-border-router.md
@@ -0,0 +1,526 @@
+# Build an Android Border Router
+
+If you are not an Android device or Thread chip vendor, you can stop reading
+now.
+
+This document walks you through the steps to build a new Android-based Thread
+Border Router device with the latest AOSP source code. By following this
+document, you will learn:
+
+1. [the overall architecture and status of Thread support in Android](#architecture)
+2. [how to create your own Thread HAL service](#build-your-thread-hal-service)
+3. [how to make your device compatible with Google Home](#be-compatible-with-google-home)
+4. [how to test your Thread Border Router](#testing)
+
+If you need support, file an issue in
+[GitHub](https://github.com/openthread/ot-br-posix/issues) or open a
+[Dicussion](https://github.com/orgs/openthread/discussions) if you have any
+questions.
+
+Note: Before creating an issue or discussion, search to see if it has already
+been reported or asked.
+
+## Overview
+
+The Android Thread stack is based on OpenThread and `ot-br-posix` which are
+open-sourced by Google in GitHub. The same way OpenThread is developed in a
+public GitHub repository, so the Android Thread stack is developed in the
+public [AOSP codebase](https://cs.android.com/). All features and bug fixes
+are submitted first in AOSP. This allows vendors to start adopting the latest
+Thread versions without waiting for regular Android releases.
+
+### Architecture
+
+The whole Android Thread stack consists of two major components: the core
+Thread stack in a generic system partition and the Thread HAL service in a
+vendor partition. Device vendors typically need only to take care and build the
+HAL service.
+
+
+
+Here is a brief summary of how the Android Thread stack works:
+- There is a Java Thread system service in the system server which manages the
+ whole stack - provides the Thread system API, creates the `thread-wpan`
+ tunnel interface, registers the Thread network to the
+ [Connectivity service](https://developer.android.com/reference/android/content/Context#CONNECTIVITY_SERVICE)
+ and implements the Border Routing and Advertising Proxy functionalities.
+- The core Thread / OpenThread stack is hosted in a non-privileged standalone
+ native process which is named `ot-daemon`. `ot-daemon` is directly managed
+ by the Java system service via private AIDL APIs and it accesses the Thread
+ hardware radio through the Thread HAL API.
+- A vendor-provided Thread HAL service MUST implement the Thread HAL API. It
+ typically works as an
+ [RCP](https://openthread.io/platforms/co-processor#radio_co-processor_rcp)
+ and implements the
+ [spinel](https://openthread.io/platforms/co-processor#spinel_protocol)
+ protocol.
+
+Note: Both the Java system service and ot-daemon are delivered in a
+[Tethering](https://source.android.com/docs/core/ota/modular-system/tethering#overview)
+mainline module. For mobile devices, the binary is managed by Google and
+delivered to devices via Google Play monthly. For non-mobile devices such as
+TV, vendors are free to build from source or use a prebuilt.
+
+### Where is the code?
+
+- The Android Thread framework / API and service: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/thread/
+- The Thread HAL API and default service implementation: https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/threadnetwork/
+- Imported OpenThread repo: https://cs.android.com/android/platform/superproject/main/+/main:external/openthread/
+- Imported ot-br-posix repo: https://cs.android.com/android/platform/superproject/main/+/main:external/ot-br-posix/
+
+## Set up development environment
+
+Android device vendors who have already established an Android development
+environment for the device can skip this section.
+
+If you are new to Android ecosystem or you are a silicon vendor who wants to
+make your Thread chip compatible with Android and provide support for device
+vendors, keep reading.
+
+### Follow the Android developer codelab
+
+To set up your Android development environment for the first time, use the
+following codelab: https://source.android.com/docs/setup/start. At the end of
+this codelab, you will be able to build and run a simulated Cuttlefish device
+from source code.
+
+## Build your Thread HAL service
+
+### Try Thread in Cuttlefish
+
+[Cuttlefish](https://source.android.com/docs/devices/Cuttlefish) is the virtual
+Android device. Before starting building your own HAL service, it's better to
+try Thread in Cuttlefish to understand how HAL works.
+
+A default Thread HAL service is provided in Cuttlefish and it's implemented
+with the [simulated RCP](https://github.com/openthread/openthread/tree/main/examples/platforms/simulation)
+which transceives packets via UDP socket to and from a simulated
+Thread (802.15.4) radio.
+
+In the Cuttlefish instance, a "ThreadNetworkDemoApp" is pre-installed. Open
+that app to join the Cuttlefish device into a default Thread network.
+
+
+
+Note: The Border Router functionalities will be started (and OMR address will
+be created) only when the Cuttlefish device is connected to a virtual Wi-Fi
+network. You can connect the Wi-Fi network in Settings.
+
+There are also the `ot-ctl` and `ot-cli-ftd` command line tools provided to
+configure your Thread network for testing. Those tools support all the
+OpenThread CLI commands that you may be familiar with already.
+
+You can grep for logs of the Cuttlefish Thread HAL service by:
+
+```
+$ adb logcat | egrep -i threadnetwork-service
+
+07-21 10:43:05.048 0 0 I init : Parsing file /apex/com.android.hardware.threadnetwork/etc/threadnetwork-service.rc...
+07-21 10:59:27.233 580 580 W android.hardware.threadnetwork-service: ThreadChip binder is unlinked
+07-21 10:59:27.233 580 580 I android.hardware.threadnetwork-service: Close IThreadChip successfully
+07-21 10:59:27.385 580 580 I android.hardware.threadnetwork-service: Open IThreadChip successfully
+```
+
+Or grep for ot-daemon logs by:
+
+```
+$ adb logcat | egrep -i ot-daemon
+07-21 10:43:48.741 0 0 I init : starting service 'ot-daemon'...
+07-21 10:43:48.742 0 0 I init : Created socket '/dev/socket/ot-daemon/thread-wpan.sock', mode 660, user 1084, group 1084
+07-21 10:43:48.762 0 0 I init : ... started service 'ot-daemon' has pid 2473
+07-21 10:46:26.320 2473 2473 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-21 10:46:30.290 2473 2473 W ot-daemon: [W] P-Daemon------: Daemon read: Connection reset by peer
+07-21 10:48:07.264 2473 2473 I ot-daemon: [INFO]-BINDER--: Start joining...
+07-21 10:48:07.267 2473 2473 I ot-daemon: [I] Settings------: Saved ActiveDataset
+07-21 10:48:07.267 2473 2473 I ot-daemon: [I] DatasetManager: Active dataset set
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] DnssdServer---: Started
+07-21 10:48:07.273 2473 2473 I ot-daemon: [N] Mle-----------: Role disabled -> detached
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Mle-----------: AttachState Idle -> Start
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Notifier------: StateChanged (0x111fd11d) [Ip6+ Role LLAddr MLAddr KeySeqCntr Ip6Mult+ Channel PanId NetName ExtPanId ...
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Notifier------: StateChanged (0x111fd11d) ... NetworkKey PSKc SecPolicy NetifState ActDset]
+```
+
+Note: You can also capture Thread system server log with the tag
+"ThreadNetworkService".
+
+The Cuttlefish Thread HAL service uses the default Thread HAL service plus the
+OpenThread simulated RCP binary, see the next section for how it works.
+
+### The default HAL service
+
+A [default HAL service](https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/threadnetwork/aidl/default/)
+is included along with the Thread HAL API. The default HAL service supports
+both simulated and real RCP devices. It receives an optional RCP device URL and
+if the URL is not provided, it defaults to the simulated RCP device.
+
+In file `hardware/interfaces/threadnetwork/aidl/default/threadnetwork-service.rc`:
+
+```
+service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service
+ class hal
+ user thread_network
+```
+
+This is equivalent to:
+
+```
+service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service spinel+hdlc+forkpty:///apex/com.android.hardware.threadnetwork/bin/ot-rcp?forkpty-arg=1
+ class hal
+ user thread_network
+```
+
+For real RCP devices, it supports both SPI and UART interfaces and you can
+specify the device with the schema `spinel+spi://`, `spinel+hdlc+uart://` and
+`spinel+socket://` respectively.
+
+Note: `spinel+socket://` is a new spinel interface added in the default Thread
+HAL, it supports transmitting spinel frame via an Unix socket. A full
+socket-based radio URL may be like
+`spinel+socket:///data/vendor/threadnetwork/thread_spinel_socket`.
+
+#### Understand the vendor APEX
+
+Similar to the Thread stack in the Tethering mainline module, the default Thread
+HAL service in Cuttlefish is packaged in an APEX module as well. But it's a
+vendor APEX module which will be installed to `/vendor/apex/` (The artifacts in
+the module will be unzipped to `/apex/com.android.hardware.threadnetwork/`).
+
+```aidl
+apex {
+ name: "com.android.hardware.threadnetwork",
+ manifest: "manifest.json",
+ file_contexts: "file_contexts",
+ key: "com.android.hardware.key",
+ certificate: ":com.android.hardware.certificate",
+ updatable: false,
+ vendor: true,
+
+ binaries: [
+ "android.hardware.threadnetwork-service",
+ "ot-rcp",
+ ],
+
+ prebuilts: [
+ "threadnetwork-default.xml", // vintf_fragment
+ "threadnetwork-service.rc", // init_rc
+ "android.hardware.thread_network.prebuilt.xml", // permission
+ ],
+}
+```
+
+There are a few important configurations that you will need to pay attention or
+make changes to when building your own HAL APEX module:
+
+- `file_contexts`: This describes the binary / data files delivered in this
+ APEX module or files the HAL service need to access (for example, the RCP
+ device). This allows you to specify specific sepolicy rules for your HAL
+ service to access the hardware RCP device.
+- `binaries`: The binary file delivered in this APEX module
+- `threadnetwork-service.rc`: How the HAL service will be started. You need to
+ specify the RCP device path here.
+- `android.hardware.thread_network.prebuilt.xml`: Defines the
+ `android.hardware.thread_network` hardware feature. This is required for the
+ Android system to know that your device does have Thread hardware support.
+ Otherwise, the Android Thread stack won't be enabled.
+
+### Create your HAL service
+
+Whether you are an Android device developer or a silicon vendor, you should be
+familiar with building OT RCP firmware for your Thread chip. The following
+instructions assume that the hardware chip is correctly wired and
+validated.
+
+The simplest way to build your HAL APEX is to create a new APEX with the
+binaries and prebuilts of the default HAL APEX. For example, if your company is
+Banana and the RCP device on your device is `/dev/ttyACM0`, your Thread HAL
+APEX will look like this:
+
+- `Android.bp`:
+ ```
+ prebuilt_etc {
+ name: "banana-threadnetwork-service.rc",
+ src: "banana-threadnetwork-service.rc",
+ installable: false,
+ }
+
+ apex {
+ name: "com.banana.android.hardware.threadnetwork",
+ manifest: "manifest.json",
+ file_contexts: "file_contexts",
+ key: "com.android.hardware.key",
+ certificate: ":com.android.hardware.certificate",
+ updatable: false,
+ vendor: true,
+
+ binaries: [
+ "android.hardware.threadnetwork-service",
+ ],
+
+ prebuilts: [
+ "banana-threadnetwork-service.rc",
+ "threadnetwork-default.xml",
+ "android.hardware.thread_network.prebuilt.xml",
+ ],
+ }
+ ```
+- `file_contexts`:
+ ```
+ (/.*)? u:object_r:vendor_file:s0
+ /etc(/.*)? u:object_r:vendor_configs_file:s0
+ /bin/hw/android\.hardware\.threadnetwork-service u:object_r:hal_threadnetwork_default_exec:s0
+ /dev/ttyACM0 u:object_r:threadnetwork_rcp_device:s0
+ ```
+ The file paths in the first column are related to `/apex/com.android.hardware.threadnetwork/`.
+- `threadnetwork-service.rc`:
+ ```
+ service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=115200
+ class hal
+ user root
+ ```
+- `manifest.json`:
+ ```
+ {
+ "name": "com.android.hardware.threadnetwork",
+ "version": 1
+ }
+ ```
+
+Note: The default Thread HAL service is not designed to be a generic system
+component which works for all Android devices. If the default implementation
+can't support your device, you are free to make a copy and change it for your
+needs. In this case, it's just simpler to create a new APEX module without
+overriding the default one.
+
+Assuming you are making a new device named Orange, your device specific
+configuration directory will be like:
+
+```
+device/banana/orange/threadnetwork/
+ sepolicy/
+ Android.bp
+ file_contexts
+ manifest.json
+ threadnetwork-default.xml
+ threadnetwork-service.rc
+```
+
+See the next section for what sepolicy rules should be added in the `sepolicy/`
+sub-directory.
+
+#### Sepolicy rules for RCP device
+
+By default, your Thread HAL service doesn't have access to the RCP device (for
+example `/dev/ttyACM0`), custom sepolicy rules need to be added to the
+`sepolicy/` directory.
+
+Create a new `sepolicy/threadnetwork_hal.te` file with below content:
+
+```
+type threadnetwork_rcp_device, dev_type;
+
+# Allows the Thread HAL service to read / write the Thread RCP device
+allow hal_threadnetwork_default threadnetwork_rcp_device:chr_file rw_file_perms;
+```
+
+#### Put together
+
+Now you have finished almost all the code needs for adding Thread, the last
+step is to add the Thread HAL APEX and sepolicy rules to your device's image.
+
+You can do this by adding below code to your device's `Makefile` (for example,
+`device.mk`):
+
+```
+PRODUCT_PACKAGES += com.banana.hardware.threadnetwork
+BOARD_SEPOLICY_DIRS += device/banana/orange/threadnetwork/sepolicy
+```
+
+Note: Unfortunately, APEX module doesn't support sepolicy rules, so you need
+to explicitly specify the sepolicy directory separately.
+
+If everything works, now you will be able to see the Thread HAL service log similar to:
+
+```
+$ adb logcat | egrep -i threadnetwork-service
+08-13 13:26:41.751 477 477 I android.hardware.threadnetwork-service: ServiceName: android.hardware.threadnetwork.IThreadChip/chip0, Url: spinel+spi
+08-13 13:26:41.751 477 477 I android.hardware.threadnetwork-service: Thread Network HAL is running
+08-13 13:26:55.165 477 477 I android.hardware.threadnetwork-service: Open IThreadChip successfully
+```
+
+And the `ot-daemon` log will be like:
+
+```
+$ adb logcat -s ot-daemon
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Running OTBR_AGENT/Unknown
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Thread version: 1.3.0
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Thread interface: thread-wpan
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Backbone interface is not specified
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Radio URL: threadnetwork_hal://binder?none
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-ILS-----: Infra link selected:
+08-13 13:26:55.160 1019 1019 I ot-daemon: [I] Platform------: [HAL] Wait for getting the service android.hardware.threadnetwork.IThreadChip/chip0 ...
+08-13 13:26:55.165 1019 1019 I ot-daemon: [I] Platform------: [HAL] Successfully got the service android.hardware.threadnetwork.IThreadChip/chip0
+08-13 13:26:55.275 1019 1019 I ot-daemon: [I] P-RadioSpinel-: RCP reset: RESET_UNKNOWN
+08-13 13:26:55.276 1019 1019 I ot-daemon: [I] P-RadioSpinel-: Software reset RCP successfully
+08-13 13:26:55.277 1019 1019 I ot-daemon: [I] P-RadioSpinel-: RCP reset: RESET_POWER_ON
+08-13 13:26:55.322 1019 1019 I ot-daemon: [I] ChildSupervsn-: Timeout: 0 -> 190
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Initializing - InfraIfIndex:0
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] InfraIf-------: Init infra netif 0
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] Settings------: Read BrUlaPrefix fd7b:cc45:ff06::/48
+08-13 13:26:55.324 1019 1019 I ot-daemon: [N] RoutingManager: BR ULA prefix: fd7b:cc45:ff06::/48 (loaded)
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Generated local OMR prefix: fd7b:cc45:ff06:1::/64
+08-13 13:26:55.324 1019 1019 I ot-daemon: [N] RoutingManager: Local on-link prefix: fdde:ad00:beef:cafe::/64
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Enabling
+```
+
+## Customization
+
+The Thread mainline module (it's actually a part of the "Tethering" module)
+provides a few [overlayable configurations](https://source.android.com/docs/core/runtime/rros)
+which can be specified by vendors to customize the stack behavior. See
+[config_thread.xml](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/service/ServiceConnectivityResources/res/values/config_thread.xml)
+for the full list.
+
+Typically, you must change the `config_thread_vendor_name`,
+`config_thread_vendor_oui` and `config_thread_model_name` to your vendor or
+product values. Those values will be included in the `_meshcop._udp` mDNS
+service which is always advertised by a Thread Border Router.
+
+To add the overlay, you need to create a new `ConnectivityOverlayOrange`
+runtime_resource_overlay target for your Orange device. Create a new
+`ConnectivityOverlay/` directory under `device/banana/orange/rro_overlays` and
+create below contents in it:
+
+```
+device/banana/orange/rro_overlays/ConnectivityOverlay/
+ res
+ values
+ config_thread.xml
+ Android.bp
+ AndroidManifest.xml
+```
+
+- `Android.bp`:
+ ```
+ package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+ }
+
+ runtime_resource_overlay {
+ name: "ConnectivityOverlayOrange",
+ manifest: "AndroidManifest.xml",
+ resource_dirs: ["res"],
+ certificate: "platform",
+ product_specific: true,
+ sdk_version: "current",
+ }
+ ```
+- `AndroidManifest.xml`:
+ ```
+ <!-- Orange overlays for the Connectivity module -->
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.banana.android.connectivity.resources.orange"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <application android:hasCode="false" />
+
+ <!-- If your device uses google-signed mainline modules, the targetPackage
+ needs to be "com.google.android.connectivity.resources", otherise, it
+ should be "com.android.connectivity.resources"
+ -->
+ <overlay
+ android:targetPackage="com.google.android.connectivity.resources"
+ android:targetName="ServiceConnectivityResourcesConfig"
+ android:isStatic="true"
+ android:priority="1"/>
+ </manifest>
+ ```
+- `config_thread.xml`:
+ ```
+ <string translatable="false" name="config_thread_vendor_name">Banana Inc.</string>
+ <string translatable="false" name="config_thread_vendor_oui">AC:DE:48</string>
+ <string translatable="false" name="config_thread_model_name">Orange</string>
+ ```
+
+Similar to the HAL APEX, you need to add the overlay app to your `device.mk`
+file:
+
+```
+PRODUCT_PACKAGES += \
+ ConnectivityOverlayOrange
+```
+
+If everything works, you will see that `ot-daemon` logs the vendor and model name
+at the very beginning of the log:
+```
+$ adb logcat -s ot-daemon
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Input: state
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Output: disabled
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Output: Done
+07-22 15:31:37.693 1472 1472 W ot-daemon: [W] P-Daemon------: Daemon read: Connection reset by peer
+07-22 15:31:50.091 1472 1472 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-22 15:31:50.091 1472 1472 I ot-daemon: [I] Cli-----------: Input: factoryreset
+07-22 15:31:50.092 1472 1472 I ot-daemon: [I] Settings------: Wiped all info
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-ADPROXY-: Stopped
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-DPROXY--: Stopped
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-BA------: Stop Thread Border Agent
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-BA------: Unpublish meshcop service Banana Inc. Orange #4833._meshcop._udp.local
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-MDNS----: Removing service Banana Inc. Orange #4833._meshcop._udp
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-MDNS----: Unpublishing service Banana Inc. Orange #4833._meshcop._udp listener ID = 0
+```
+
+Note: In case the overlay doesn't work, check https://source.android.com/docs/core/runtime/rro-troubleshoot
+for troubleshooting instructions.
+
+### Be compatible with Google Home
+
+Additionally, if you want to make your Border Router be used by the Google Home
+ecosystem, you can specify this configuration in `config_thread.xml`:
+
+```
+<string-array name="config_thread_mdns_vendor_specific_txts">
+ <item>vgh=1</item>
+</string-array>
+```
+
+## Testing
+
+Your device should be compatible with the Thread 1.3+ Border Router
+specification now. Before sending it to the Thread certification program, there
+are a few Android xTS tests should be exercised to ensure the compatibility.
+
+- The VTS test makes sure Thread HAL service work as expected on your device.
+ You can run the tests with command
+ ```
+ atest VtsHalThreadNetworkTargetTest
+ ```
+- The CTS test makes sure Thread APIs work as expected on your device. You can
+ run the tests with command
+ ```
+ atest CtsThreadNetworkTestCases
+ ```
+- The integration test provides more quality guarantee of how the Thread
+ mainline code works on your device. You can run the tests with command
+ ```
+ atest ThreadNetworkIntegrationTests
+ ```
+
+You can also find more instructions of how to run VTS/CTS/MTS tests with those
+released test suites:
+
+- https://source.android.com/docs/core/tests/vts
+- https://source.android.com/docs/compatibility/cts/run
+- https://docs.partner.android.com/mainline/test/mts (you need to be a partner to access this link)
+
+### Test with the Thread demo app
+
+Similar to the Cuttlefish device, you can add the Thread demo app to your system image:
+
+```
+# ThreadNetworkDemoApp for testing
+PRODUCT_PACKAGES_DEBUG += ThreadNetworkDemoApp
+```
+
+Note that you should add it to only the debug / eng variant (for example,
+`PRODUCT_PACKAGES_DEBUG`) given this is not supposed to be included in user
+build for end consumers.
diff --git a/thread/docs/demoapp-screenshot.png b/thread/docs/demoapp-screenshot.png
new file mode 100644
index 0000000..fa7f079
--- /dev/null
+++ b/thread/docs/demoapp-screenshot.png
Binary files differ
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
index 22457f5..1b50ba7 100644
--- a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -35,6 +35,7 @@
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.thread.flags.Flags;
import java.io.ByteArrayOutputStream;
import java.net.Inet6Address;
@@ -69,7 +70,7 @@
*
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
public final class ActiveOperationalDataset implements Parcelable {
/** The maximum length of the Active Operational Dataset TLV array in bytes. */
diff --git a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
index bcda8a8..abc00b9 100644
--- a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
+++ b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
@@ -16,11 +16,11 @@
package android.net.thread;
- /**
- * Mapping from a channel to its max power.
- *
- * {@hide}
- */
+/**
+ * Mapping from a channel to its max power.
+ *
+ * {@hide}
+ */
parcelable ChannelMaxPower {
int channel; // The Thread radio channel.
int maxPower; // The max power in the unit of 0.01dBm. Passing INT16_MAX(32767) will
diff --git a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
index b576b33..3fece65 100644
--- a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
+++ b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
@@ -23,6 +23,8 @@
* @hide
*/
oneway interface IOperationalDatasetCallback {
- void onActiveOperationalDatasetChanged(in @nullable ActiveOperationalDataset activeOpDataset);
- void onPendingOperationalDatasetChanged(in @nullable PendingOperationalDataset pendingOpDataset);
+ void onActiveOperationalDatasetChanged(
+ in @nullable ActiveOperationalDataset activeOpDataset);
+ void onPendingOperationalDatasetChanged(
+ in @nullable PendingOperationalDataset pendingOpDataset);
}
diff --git a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java b/thread/framework/java/android/net/thread/IOutputReceiver.aidl
similarity index 65%
rename from tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java
rename to thread/framework/java/android/net/thread/IOutputReceiver.aidl
index f343df5..b6b4375 100644
--- a/tests/cts/hostside-network-policy/app/src/com/android/cts/netpolicy/hostside/DozeModeNonMeteredTest.java
+++ b/thread/framework/java/android/net/thread/IOutputReceiver.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package com.android.cts.netpolicy.hostside;
+package android.net.thread;
-import static com.android.cts.netpolicy.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class DozeModeNonMeteredTest extends AbstractDozeModeTestCase {
+/** Receives the output of a Thread network operation. @hide */
+oneway interface IOutputReceiver {
+ void onOutput(in String output);
+ void onComplete();
+ void onError(int errorCode, String errorMessage);
}
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
index 9d0a571..d074b01 100644
--- a/thread/framework/java/android/net/thread/IStateCallback.aidl
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -23,4 +23,6 @@
void onDeviceRoleChanged(int deviceRole);
void onPartitionIdChanged(long partitionId);
void onThreadEnableStateChanged(int enabledState);
+ void onEphemeralKeyStateChanged(
+ int ephemeralKeyState, @nullable String ephemeralKey, long lifetimeMillis);
}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index f50de74..e9cbb83 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -28,9 +28,9 @@
import android.net.thread.ThreadConfiguration;
/**
-* Interface for communicating with ThreadNetworkControllerService.
-* @hide
-*/
+ * Interface for communicating with ThreadNetworkControllerService.
+ * @hide
+ */
interface IThreadNetworkController {
void registerStateCallback(in IStateCallback callback);
void unregisterStateCallback(in IStateCallback callback);
@@ -38,10 +38,12 @@
void unregisterOperationalDatasetCallback(in IOperationalDatasetCallback callback);
void join(in ActiveOperationalDataset activeOpDataset, in IOperationReceiver receiver);
- void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
+ void scheduleMigration(
+ in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
void leave(in IOperationReceiver receiver);
- void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+ void setTestNetworkAsUpstream(
+ in String testNetworkInterfaceName, in IOperationReceiver receiver);
void setChannelMaxPowers(in ChannelMaxPower[] channelMaxPowers, in IOperationReceiver receiver);
int getThreadVersion();
@@ -51,4 +53,7 @@
void setConfiguration(in ThreadConfiguration config, in IOperationReceiver receiver);
void registerConfigurationCallback(in IConfigurationReceiver receiver);
void unregisterConfigurationCallback(in IConfigurationReceiver receiver);
+
+ void activateEphemeralKeyMode(long lifetimeMillis, in IOperationReceiver receiver);
+ void deactivateEphemeralKeyMode(in IOperationReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
index 0e394b1..b63cd72 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
@@ -19,9 +19,9 @@
import android.net.thread.IThreadNetworkController;
/**
-* Interface for communicating with ThreadNetworkService.
-* @hide
-*/
+ * Interface for communicating with ThreadNetworkService.
+ * @hide
+ */
interface IThreadNetworkManager {
List<IThreadNetworkController> getAllThreadNetworkControllers();
}
diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
index cecb4e9..489f941 100644
--- a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -26,6 +26,8 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import com.android.net.thread.flags.Flags;
+
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Objects;
@@ -37,7 +39,7 @@
* @see PendingOperationalDataset
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
public final class OperationalDatasetTimestamp {
/** @hide */
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
index c1351af..235e563 100644
--- a/thread/framework/java/android/net/thread/PendingOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -27,6 +27,8 @@
import android.os.Parcelable;
import android.util.SparseArray;
+import com.android.net.thread.flags.Flags;
+
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
@@ -42,7 +44,7 @@
* @see ThreadNetworkController#scheduleMigration
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
public final class PendingOperationalDataset implements Parcelable {
// Value defined in Thread spec 8.10.1.16
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
index e09b3a6..0829265 100644
--- a/thread/framework/java/android/net/thread/ThreadConfiguration.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -15,10 +15,14 @@
*/
package android.net.thread;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.net.thread.flags.Flags;
+
import java.util.Objects;
/**
@@ -37,19 +41,39 @@
* @see ThreadNetworkController#unregisterConfigurationCallback
* @hide
*/
-// @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
-// @SystemApi
+@FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
+@SystemApi
public final class ThreadConfiguration implements Parcelable {
+ private final boolean mBorderRouterEnabled;
private final boolean mNat64Enabled;
- private final boolean mDhcp6PdEnabled;
+ private final boolean mDhcpv6PdEnabled;
private ThreadConfiguration(Builder builder) {
- this(builder.mNat64Enabled, builder.mDhcp6PdEnabled);
+ this(builder.mBorderRouterEnabled, builder.mNat64Enabled, builder.mDhcpv6PdEnabled);
}
- private ThreadConfiguration(boolean nat64Enabled, boolean dhcp6PdEnabled) {
+ private ThreadConfiguration(
+ boolean borderRouterEnabled, boolean nat64Enabled, boolean dhcpv6PdEnabled) {
+ this.mBorderRouterEnabled = borderRouterEnabled;
this.mNat64Enabled = nat64Enabled;
- this.mDhcp6PdEnabled = dhcp6PdEnabled;
+ this.mDhcpv6PdEnabled = dhcpv6PdEnabled;
+ }
+
+ /**
+ * Returns {@code true} if this device is operating as a Thread Border Router.
+ *
+ * <p>A Thread Border Router works on both Thread and infrastructure networks. For example, it
+ * can route packets between Thread and infrastructure networks (e.g. Wi-Fi or Ethernet), makes
+ * devices in both networks discoverable to each other, and accepts connections from external
+ * commissioner.
+ *
+ * <p>Note it costs significantly more power to operate as a Border Router, so this is typically
+ * only enabled for wired Android devices (e.g. TV or display).
+ *
+ * @hide
+ */
+ public boolean isBorderRouterEnabled() {
+ return mBorderRouterEnabled;
}
/** Returns {@code true} if NAT64 is enabled. */
@@ -57,9 +81,13 @@
return mNat64Enabled;
}
- /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
- public boolean isDhcp6PdEnabled() {
- return mDhcp6PdEnabled;
+ /**
+ * Returns {@code true} if DHCPv6 Prefix Delegation is enabled.
+ *
+ * @hide
+ */
+ public boolean isDhcpv6PdEnabled() {
+ return mDhcpv6PdEnabled;
}
@Override
@@ -70,22 +98,24 @@
return false;
} else {
ThreadConfiguration otherConfig = (ThreadConfiguration) other;
- return mNat64Enabled == otherConfig.mNat64Enabled
- && mDhcp6PdEnabled == otherConfig.mDhcp6PdEnabled;
+ return mBorderRouterEnabled == otherConfig.mBorderRouterEnabled
+ && mNat64Enabled == otherConfig.mNat64Enabled
+ && mDhcpv6PdEnabled == otherConfig.mDhcpv6PdEnabled;
}
}
@Override
public int hashCode() {
- return Objects.hash(mNat64Enabled, mDhcp6PdEnabled);
+ return Objects.hash(mBorderRouterEnabled, mNat64Enabled, mDhcpv6PdEnabled);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('{');
- sb.append("Nat64Enabled=").append(mNat64Enabled);
- sb.append(", Dhcp6PdEnabled=").append(mDhcp6PdEnabled);
+ sb.append("borderRouterEnabled=").append(mBorderRouterEnabled);
+ sb.append(", nat64Enabled=").append(mNat64Enabled);
+ sb.append(", dhcpv6PdEnabled=").append(mDhcpv6PdEnabled);
sb.append('}');
return sb.toString();
}
@@ -97,8 +127,9 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mBorderRouterEnabled);
dest.writeBoolean(mNat64Enabled);
- dest.writeBoolean(mDhcp6PdEnabled);
+ dest.writeBoolean(mDhcpv6PdEnabled);
}
public static final @NonNull Creator<ThreadConfiguration> CREATOR =
@@ -106,8 +137,9 @@
@Override
public ThreadConfiguration createFromParcel(Parcel in) {
ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder();
+ builder.setBorderRouterEnabled(in.readBoolean());
builder.setNat64Enabled(in.readBoolean());
- builder.setDhcp6PdEnabled(in.readBoolean());
+ builder.setDhcpv6PdEnabled(in.readBoolean());
return builder.build();
}
@@ -117,24 +149,58 @@
}
};
- /** The builder for creating {@link ThreadConfiguration} objects. */
+ /**
+ * The builder for creating {@link ThreadConfiguration} objects.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+ @SystemApi
public static final class Builder {
- private boolean mNat64Enabled = false;
- private boolean mDhcp6PdEnabled = false;
+ // Thread in Android V is default to a Border Router device, so the default value here needs
+ // to be {@code true} to be compatible.
+ private boolean mBorderRouterEnabled = true;
- /** Creates a new {@link Builder} object with all features disabled. */
+ private boolean mNat64Enabled = false;
+ private boolean mDhcpv6PdEnabled = false;
+
+ /**
+ * Creates a new {@link Builder} object with all features disabled.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+ @SystemApi
public Builder() {}
/**
* Creates a new {@link Builder} object from a {@link ThreadConfiguration} object.
*
* @param config the Border Router configurations to be copied
+ * @hide
*/
+ @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+ @SystemApi
public Builder(@NonNull ThreadConfiguration config) {
Objects.requireNonNull(config);
+ mBorderRouterEnabled = config.mBorderRouterEnabled;
mNat64Enabled = config.mNat64Enabled;
- mDhcp6PdEnabled = config.mDhcp6PdEnabled;
+ mDhcpv6PdEnabled = config.mDhcpv6PdEnabled;
+ }
+
+ /**
+ * Enables or disables this device as a Border Router.
+ *
+ * <p>Defaults to {@code true} if this method is not called.
+ *
+ * @see ThreadConfiguration#isBorderRouterEnabled
+ * @hide
+ */
+ @NonNull
+ public Builder setBorderRouterEnabled(boolean enabled) {
+ this.mBorderRouterEnabled = enabled;
+ return this;
}
/**
@@ -142,7 +208,11 @@
*
* <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
* IPv4.
+ *
+ * @hide
*/
+ @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+ @SystemApi
@NonNull
public Builder setNat64Enabled(boolean enabled) {
this.mNat64Enabled = enabled;
@@ -154,14 +224,22 @@
*
* <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
* IPv6.
+ *
+ * @hide
*/
@NonNull
- public Builder setDhcp6PdEnabled(boolean enabled) {
- this.mDhcp6PdEnabled = enabled;
+ public Builder setDhcpv6PdEnabled(boolean enabled) {
+ this.mDhcpv6PdEnabled = enabled;
return this;
}
- /** Creates a new {@link ThreadConfiguration} object. */
+ /**
+ * Creates a new {@link ThreadConfiguration} object.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+ @SystemApi
@NonNull
public ThreadConfiguration build() {
return new ThreadConfiguration(this);
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 30b3d6a..73a6bda 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -26,6 +26,7 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.Size;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.os.Binder;
import android.os.OutcomeReceiver;
@@ -34,10 +35,12 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.thread.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Duration;
+import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
@@ -51,7 +54,7 @@
*
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
public final class ThreadNetworkController {
private static final String TAG = "ThreadNetworkController";
@@ -80,6 +83,25 @@
/** The Thread radio is being disabled. */
public static final int STATE_DISABLING = 2;
+ /** The ephemeral key mode is disabled. */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ public static final int EPHEMERAL_KEY_DISABLED = 0;
+
+ /**
+ * The ephemeral key mode is enabled, an external commissioner candidate can use the ephemeral
+ * key to connect to this device and get Thread credential shared.
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ public static final int EPHEMERAL_KEY_ENABLED = 1;
+
+ /**
+ * The ephemeral key is in use. This state means there is already an active secure session
+ * connected to this device with the ephemeral key, it's not possible to use the ephemeral key
+ * for new connections in this state.
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ public static final int EPHEMERAL_KEY_IN_USE = 2;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -98,14 +120,25 @@
value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
public @interface EnabledState {}
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = {"EPHEMERAL_KEY_"},
+ value = {EPHEMERAL_KEY_DISABLED, EPHEMERAL_KEY_ENABLED, EPHEMERAL_KEY_IN_USE})
+ public @interface EphemeralKeyState {}
+
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
- /** Minimum value of max power in unit of 0.01dBm. @hide */
- private static final int POWER_LIMITATION_MIN = -32768;
+ /** The value of max power to disable the Thread channel. */
+ // This constant can never change. It has "max" in the name not because it indicates
+ // maximum power, but because it's passed to an API that sets the maximum power to
+ // disabled the Thread channel.
+ @SuppressLint("MinMaxConstant")
+ public static final int MAX_POWER_CHANNEL_DISABLED = Integer.MIN_VALUE;
- /** Maximum value of max power in unit of 0.01dBm. @hide */
- private static final int POWER_LIMITATION_MAX = 32767;
+ /** The maximum lifetime of an ephemeral key. @hide */
+ @NonNull private static final Duration EPHEMERAL_KEY_LIFETIME_MAX = Duration.ofMinutes(10);
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -171,6 +204,87 @@
}
}
+ /** Returns the maximum lifetime allowed when activating ephemeral key mode. */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @NonNull
+ public Duration getMaxEphemeralKeyLifetime() {
+ return EPHEMERAL_KEY_LIFETIME_MAX;
+ }
+
+ /**
+ * Activates ephemeral key mode with a given {@code lifetime}. The ephemeral key is a temporary,
+ * single-use numeric code that is used for Thread Administration Sharing. After activation, the
+ * mode may expire or get deactivated, caller to this method should subscribe to the ephemeral
+ * key state updates with {@link #registerStateCallback} to get notified when the ephemeral key
+ * state changes.
+ *
+ * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The ephemeral
+ * key string contains a sequence of numeric digits 0-9 of user-input friendly length (typically
+ * 9). Subscribers to ephemeral key state updates with {@link #registerStateCallback} will be
+ * notified with a call to {@link #onEphemeralKeyStateChanged}.
+ *
+ * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
+ * specific error:
+ *
+ * <ul>
+ * <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not
+ * attached to Thread network
+ * <li>{@link ThreadNetworkException#ERROR_BUSY} when ephemeral key mode is already activated
+ * on the device, caller can recover from this error when the ephemeral key mode gets
+ * deactivated
+ * </ul>
+ *
+ * @param lifetime valid lifetime of the generated ephemeral key, should be larger than {@link
+ * Duration#ZERO} and at most the duration returned by {@link #getMaxEphemeralKeyLifetime}.
+ * @param executor the executor on which to execute {@code receiver}
+ * @param receiver the receiver to receive the result of this operation
+ * @throws IllegalArgumentException if the {@code lifetime} exceeds the allowed range
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void activateEphemeralKeyMode(
+ @NonNull Duration lifetime,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ if (lifetime.compareTo(Duration.ZERO) <= 0
+ || lifetime.compareTo(EPHEMERAL_KEY_LIFETIME_MAX) > 0) {
+ throw new IllegalArgumentException(
+ "Invalid ephemeral key lifetime: the value must be in range of (0, "
+ + EPHEMERAL_KEY_LIFETIME_MAX
+ + "]");
+ }
+ long lifetimeMillis = lifetime.toMillis();
+ try {
+ mControllerService.activateEphemeralKeyMode(
+ lifetimeMillis, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Deactivates ephemeral key mode. If there is an active connection with the ephemeral key, the
+ * connection will be terminated.
+ *
+ * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The call will
+ * always succeed if the device is not in ephemeral key mode.
+ *
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive the result of this operation
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void deactivateEphemeralKeyMode(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ try {
+ mControllerService.deactivateEphemeralKeyMode(
+ new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/** Returns the Thread version this device is operating on. */
@ThreadVersion
public int getThreadVersion() {
@@ -245,6 +359,24 @@
* @param enabledState the new Thread enabled state
*/
default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
+
+ /**
+ * The ephemeral key state has changed.
+ *
+ * @param ephemeralKeyState the ephemeral key state
+ * @param ephemeralKey the ephemeral key string which contains a sequence of numeric digits
+ * 0-9 of user-input friendly length (typically 9), or {@code null} if {@code
+ * ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED} or the caller doesn't have the
+ * permission {@link android.permission.THREAD_NETWORK_PRIVILEGED}
+ * @param expiry a timestamp of when the ephemeral key will expire or {@code null} if {@code
+ * ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED}
+ */
+ @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ default void onEphemeralKeyStateChanged(
+ @EphemeralKeyState int ephemeralKeyState,
+ @Nullable String ephemeralKey,
+ @Nullable Instant expiry) {}
}
private static final class StateCallbackProxy extends IStateCallback.Stub {
@@ -285,13 +417,34 @@
Binder.restoreCallingIdentity(identity);
}
}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ @EphemeralKeyState int ephemeralKeyState,
+ String ephemeralKey,
+ long lifetimeMillis) {
+ final long identity = Binder.clearCallingIdentity();
+ final Instant expiry =
+ ephemeralKeyState == EPHEMERAL_KEY_DISABLED
+ ? null
+ : Instant.now().plusMillis(lifetimeMillis);
+
+ try {
+ mExecutor.execute(
+ () ->
+ mCallback.onEphemeralKeyStateChanged(
+ ephemeralKeyState, ephemeralKey, expiry));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
}
/**
* Registers a callback to be called when Thread network states are changed.
*
- * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
- * existing states.
+ * <p>Upon return of this method, all methods of {@code callback} will be invoked immediately
+ * with existing states. The order of the invoked callbacks is not guaranteed.
*
* @param executor the executor to execute the {@code callback}
* @param callback the callback to receive Thread network state changes
@@ -592,15 +745,19 @@
* OutcomeReceiver#onResult} will be called, and the {@code configuration} will be applied and
* persisted to the device; the configuration changes can be observed by {@link
* #registerConfigurationCallback}. On failure, {@link OutcomeReceiver#onError} of {@code
- * receiver} will be invoked with a specific error.
+ * receiver} will be invoked with a specific error:
+ *
+ * <ul>
+ * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_FEATURE} the configuration enables a
+ * feature which is not supported by the platform.
+ * </ul>
*
* @param configuration the configuration to set
* @param executor the executor to execute {@code receiver}
* @param receiver the receiver to receive result of this operation
- * @hide
*/
- // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
- // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+ @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+ @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
public void setConfiguration(
@NonNull ThreadConfiguration configuration,
@NonNull @CallbackExecutor Executor executor,
@@ -619,16 +776,15 @@
/**
* Registers a callback to be called when the configuration is changed.
*
- * <p>Upon return of this method, {@code callback} will be invoked immediately with the new
+ * <p>Upon return of this method, {@code callback} will be invoked immediately with the current
* {@link ThreadConfiguration}.
*
* @param executor the executor to execute the {@code callback}
* @param callback the callback to receive Thread configuration changes
* @throws IllegalArgumentException if {@code callback} has already been registered
- * @hide
*/
- // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
- // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+ @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
+ @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
public void registerConfigurationCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<ThreadConfiguration> callback) {
@@ -656,10 +812,9 @@
* @param callback the callback which has been registered with {@link
* #registerConfigurationCallback}
* @throws IllegalArgumentException if {@code callback} hasn't been registered
- * @hide
*/
- // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
- // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+ @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
+ @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
public void unregisterConfigurationCallback(@NonNull Consumer<ThreadConfiguration> callback) {
requireNonNull(callback, "callback cannot be null");
synchronized (mConfigurationCallbackMapLock) {
@@ -705,6 +860,10 @@
/**
* Sets max power of each channel.
*
+ * <p>This method sets the max power for the given channel. The platform sets the actual output
+ * power to be less than or equal to the {@code channelMaxPowers} and as close as possible to
+ * the {@code channelMaxPowers}.
+ *
* <p>If not set, the default max power is set by the Thread HAL service or the Thread radio
* chip firmware.
*
@@ -713,22 +872,27 @@
* OutcomeReceiver#onError} will be called with a specific error:
*
* <ul>
- * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_OPERATION} the operation is no
- * supported by the platform.
+ * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_FEATURE} the feature is not supported
+ * by the platform.
* </ul>
*
* @param channelMaxPowers SparseIntArray (key: channel, value: max power) consists of channel
* and corresponding max power. Valid channel values should be between {@link
* ActiveOperationalDataset#CHANNEL_MIN_24_GHZ} and {@link
- * ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. Max
- * power values should be between INT16_MIN (-32768) and INT16_MAX (32767). If the max power
- * is set to INT16_MAX, the corresponding channel is not supported.
+ * ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. For
+ * example, 1000 means 0.01W and 2000 means 0.1W. If the power value of {@code
+ * channelMaxPowers} is lower than the minimum output power supported by the platform, the
+ * output power will be set to the minimum output power supported by the platform. If the
+ * power value of {@code channelMaxPowers} is higher than the maximum output power supported
+ * by the platform, the output power will be set to the maximum output power supported by
+ * the platform. If the power value of {@code channelMaxPowers} is set to {@link
+ * #MAX_POWER_CHANNEL_DISABLED}, the corresponding channel is disabled.
* @param executor the executor to execute {@code receiver}.
* @param receiver the receiver to receive the result of this operation.
* @throws IllegalArgumentException if the size of {@code channelMaxPowers} is smaller than 1,
* or invalid channel or max power is configured.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED)
@RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
public final void setChannelMaxPowers(
@NonNull @Size(min = 1) SparseIntArray channelMaxPowers,
@@ -744,7 +908,6 @@
for (int i = 0; i < channelMaxPowers.size(); i++) {
int channel = channelMaxPowers.keyAt(i);
- int maxPower = channelMaxPowers.get(channel);
if ((channel < ActiveOperationalDataset.CHANNEL_MIN_24_GHZ)
|| (channel > ActiveOperationalDataset.CHANNEL_MAX_24_GHZ)) {
@@ -757,19 +920,6 @@
+ ActiveOperationalDataset.CHANNEL_MAX_24_GHZ
+ "]");
}
-
- if ((maxPower < POWER_LIMITATION_MIN) || (maxPower > POWER_LIMITATION_MAX)) {
- throw new IllegalArgumentException(
- "Channel power ({channel: "
- + channel
- + ", maxPower: "
- + maxPower
- + "}) exceeds allowed range ["
- + POWER_LIMITATION_MIN
- + ", "
- + POWER_LIMITATION_MAX
- + "]");
- }
}
try {
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index f699c30..1ea2459 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -23,6 +23,8 @@
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import com.android.net.thread.flags.Flags;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -31,7 +33,7 @@
*
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
public class ThreadNetworkException extends Exception {
/** @hide */
@@ -139,16 +141,14 @@
public static final int ERROR_THREAD_DISABLED = 12;
/**
- * The operation failed because it is not supported by the platform. For example, some platforms
- * may not support setting the target power of each channel. The caller should not retry and may
- * return an error to the user.
- *
- * @hide
+ * The operation failed because the feature is not supported by the platform. For example, some
+ * platforms may not support setting the target power of each channel. The caller should not
+ * retry and may return an error to the user.
*/
- public static final int ERROR_UNSUPPORTED_OPERATION = 13;
+ public static final int ERROR_UNSUPPORTED_FEATURE = 13;
private static final int ERROR_MIN = ERROR_INTERNAL_ERROR;
- private static final int ERROR_MAX = ERROR_UNSUPPORTED_OPERATION;
+ private static final int ERROR_MAX = ERROR_UNSUPPORTED_FEATURE;
private final int mErrorCode;
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
deleted file mode 100644
index 691bbf5..0000000
--- a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.thread;
-
-/**
- * Container for flag constants defined in the "thread_network" namespace.
- *
- * @hide
- */
-// TODO: replace this class with auto-generated "com.android.net.thread.flags.Flags" once the
-// flagging infra is fully supported for mainline modules.
-public final class ThreadNetworkFlags {
- /** @hide */
- public static final String FLAG_THREAD_ENABLED = "com.android.net.thread.flags.thread_enabled";
-
- /** @hide */
- public static final String FLAG_CONFIGURATION_ENABLED =
- "com.android.net.thread.flags.configuration_enabled";
-
- private ThreadNetworkFlags() {}
-}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 150b759..b863bc2 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -26,6 +26,7 @@
import android.os.RemoteException;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.thread.flags.Flags;
import java.util.Collections;
import java.util.List;
@@ -35,7 +36,7 @@
*
* @hide
*/
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
@SystemApi
@SystemService(ThreadNetworkManager.SERVICE_NAME)
public final class ThreadNetworkManager {
@@ -80,6 +81,19 @@
"android.permission.THREAD_NETWORK_PRIVILEGED";
/**
+ * Permission allows accessing Thread network state and performing certain testing-related
+ * operations.
+ *
+ * <p>This is the same value as android.Manifest.permission.THREAD_NETWORK_TESTING. That symbol
+ * is not available on U while this feature needs to support Android U TV devices, so here is
+ * making a copy of android.Manifest.permission.THREAD_NETWORK_TESTING.
+ *
+ * @hide
+ */
+ public static final String PERMISSION_THREAD_NETWORK_TESTING =
+ "android.permission.THREAD_NETWORK_TESTING";
+
+ /**
* This user restriction specifies if Thread network is disallowed on the device. If Thread
* network is disallowed it cannot be turned on via Settings.
*
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java b/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java
new file mode 100644
index 0000000..205c16e
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java
@@ -0,0 +1,227 @@
+/*
+ * 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 android.net.thread;
+
+import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkSpecifier;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.module.util.HexDump;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Represents and identifies a Thread network.
+ *
+ * @hide
+ */
+public final class ThreadNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+ /** The Extended PAN ID of a Thread network. */
+ @NonNull private final byte[] mExtendedPanId;
+
+ /** The Active Timestamp of a Thread network. */
+ @Nullable private final OperationalDatasetTimestamp mActiveTimestamp;
+
+ private final boolean mRouterEligibleForLeader;
+
+ private ThreadNetworkSpecifier(@NonNull Builder builder) {
+ mExtendedPanId = builder.mExtendedPanId.clone();
+ mActiveTimestamp = builder.mActiveTimestamp;
+ mRouterEligibleForLeader = builder.mRouterEligibleForLeader;
+ }
+
+ /** Returns the Extended PAN ID of the Thread network this specifier refers to. */
+ @NonNull
+ public byte[] getExtendedPanId() {
+ return mExtendedPanId.clone();
+ }
+
+ /**
+ * Returns the Active Timestamp of the Thread network this specifier refers to, or {@code null}
+ * if not specified.
+ */
+ @Nullable
+ public OperationalDatasetTimestamp getActiveTimestamp() {
+ return mActiveTimestamp;
+ }
+
+ /**
+ * Returns {@code true} if this device can be a leader during attachment when there are no
+ * nearby routers.
+ */
+ public boolean isRouterEligibleForLeader() {
+ return mRouterEligibleForLeader;
+ }
+
+ /**
+ * Returns {@code true} if both {@link #getExtendedPanId()} and {@link #getActiveTimestamp()}
+ * (if not {@code null}) of the two {@link ThreadNetworkSpecifier} objects are equal.
+ *
+ * <p>Note value of {@link #isRouterEligibleForLeader()} is expiclitly excluded because this is
+ * not part of the identifier.
+ *
+ * @hide
+ */
+ @Override
+ public boolean canBeSatisfiedBy(@Nullable NetworkSpecifier other) {
+ if (!(other instanceof ThreadNetworkSpecifier)) {
+ return false;
+ }
+ ThreadNetworkSpecifier otherSpecifier = (ThreadNetworkSpecifier) other;
+
+ if (mActiveTimestamp != null && !mActiveTimestamp.equals(otherSpecifier.mActiveTimestamp)) {
+ return false;
+ }
+
+ return Arrays.equals(mExtendedPanId, otherSpecifier.mExtendedPanId);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (!(other instanceof ThreadNetworkSpecifier)) {
+ return false;
+ } else if (this == other) {
+ return true;
+ }
+
+ ThreadNetworkSpecifier otherSpecifier = (ThreadNetworkSpecifier) other;
+
+ return Arrays.equals(mExtendedPanId, otherSpecifier.mExtendedPanId)
+ && Objects.equals(mActiveTimestamp, otherSpecifier.mActiveTimestamp)
+ && mRouterEligibleForLeader == otherSpecifier.mRouterEligibleForLeader;
+ }
+
+ @Override
+ public int hashCode() {
+ return deepHashCode(mExtendedPanId, mActiveTimestamp, mRouterEligibleForLeader);
+ }
+
+ /** An easy-to-use wrapper of {@link Arrays#deepHashCode}. */
+ private static int deepHashCode(Object... values) {
+ return Arrays.deepHashCode(values);
+ }
+
+ @Override
+ public String toString() {
+ return "ThreadNetworkSpecifier{extendedPanId="
+ + HexDump.toHexString(mExtendedPanId)
+ + ", activeTimestamp="
+ + mActiveTimestamp
+ + ", routerEligibleForLeader="
+ + mRouterEligibleForLeader
+ + "}";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeByteArray(mExtendedPanId);
+ dest.writeByteArray(mActiveTimestamp != null ? mActiveTimestamp.toTlvValue() : null);
+ dest.writeBoolean(mRouterEligibleForLeader);
+ }
+
+ public static final @NonNull Parcelable.Creator<ThreadNetworkSpecifier> CREATOR =
+ new Parcelable.Creator<ThreadNetworkSpecifier>() {
+ @Override
+ public ThreadNetworkSpecifier createFromParcel(Parcel in) {
+ byte[] extendedPanId = in.createByteArray();
+ byte[] activeTimestampBytes = in.createByteArray();
+ OperationalDatasetTimestamp activeTimestamp =
+ (activeTimestampBytes != null)
+ ? OperationalDatasetTimestamp.fromTlvValue(activeTimestampBytes)
+ : null;
+ boolean routerEligibleForLeader = in.readBoolean();
+
+ return new Builder(extendedPanId)
+ .setActiveTimestamp(activeTimestamp)
+ .setRouterEligibleForLeader(routerEligibleForLeader)
+ .build();
+ }
+
+ @Override
+ public ThreadNetworkSpecifier[] newArray(int size) {
+ return new ThreadNetworkSpecifier[size];
+ }
+ };
+
+ /** The builder for creating {@link ActiveOperationalDataset} objects. */
+ public static final class Builder {
+ @NonNull private final byte[] mExtendedPanId;
+ @Nullable private OperationalDatasetTimestamp mActiveTimestamp;
+ private boolean mRouterEligibleForLeader;
+
+ /**
+ * Creates a new {@link Builder} object with given Extended PAN ID.
+ *
+ * @throws IllegalArgumentException if {@code extendedPanId} is {@code null} or the length
+ * is not {@link ActiveOperationalDataset#LENGTH_EXTENDED_PAN_ID}
+ */
+ public Builder(@NonNull byte[] extendedPanId) {
+ if (extendedPanId == null || extendedPanId.length != LENGTH_EXTENDED_PAN_ID) {
+ throw new IllegalArgumentException(
+ "extendedPanId is null or length is not "
+ + LENGTH_EXTENDED_PAN_ID
+ + ": "
+ + Arrays.toString(extendedPanId));
+ }
+ mExtendedPanId = extendedPanId.clone();
+ mRouterEligibleForLeader = false;
+ }
+
+ /**
+ * Creates a new {@link Builder} object by copying the data in the given {@code specifier}
+ * object.
+ */
+ public Builder(@NonNull ThreadNetworkSpecifier specifier) {
+ this(specifier.getExtendedPanId());
+ setActiveTimestamp(specifier.getActiveTimestamp());
+ setRouterEligibleForLeader(specifier.isRouterEligibleForLeader());
+ }
+
+ /** Sets the Active Timestamp of the Thread network. */
+ @NonNull
+ public Builder setActiveTimestamp(@Nullable OperationalDatasetTimestamp activeTimestamp) {
+ mActiveTimestamp = activeTimestamp;
+ return this;
+ }
+
+ /**
+ * Sets whether this device should be a leader during attachment when there are no nearby
+ * routers.
+ */
+ @NonNull
+ public Builder setRouterEligibleForLeader(boolean eligible) {
+ mRouterEligibleForLeader = eligible;
+ return this;
+ }
+
+ /** Creates a new {@link ThreadNetworkSpecifier} object from values set so far. */
+ @NonNull
+ public ThreadNetworkSpecifier build() {
+ return new ThreadNetworkSpecifier(this);
+ }
+ }
+}
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
index c176bfa..e012d41 100755
--- a/thread/scripts/make-pretty.sh
+++ b/thread/scripts/make-pretty.sh
@@ -1,9 +1,35 @@
#!/usr/bin/env bash
-SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+ANDROID_ROOT_DIR=$(
+ while [ ! -d ".repo" ] && [ "$PWD" != "/" ]; do cd ..; done
+ pwd
+)
-GOOGLE_JAVA_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/tools/common/google-java-format/google-java-format
-ANDROID_BP_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/build-tools/linux-x86/bin/bpfmt
+if [ ! -d "$ANDROID_ROOT_DIR/.repo" ]; then
+ echo "Error: The script has to run in an Android repo checkout"
+ exit 1
+fi
-$GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
-$ANDROID_BP_FORMAT -w $(find $SCRIPT_DIR/../ -name "*.bp")
+GOOGLE_JAVA_FORMAT=$ANDROID_ROOT_DIR/prebuilts/tools/common/google-java-format/google-java-format
+ANDROID_BP_FORMAT=$ANDROID_ROOT_DIR/prebuilts/build-tools/linux-x86/bin/bpfmt
+AIDL_FORMAT=$ANDROID_ROOT_DIR/system/tools/aidl/aidl-format.sh
+
+CONNECTIVITY_DIR=$ANDROID_ROOT_DIR/packages/modules/Connectivity
+OPENTHREAD_DIR=$ANDROID_ROOT_DIR/external/openthread
+OTBR_POSIX_DIR=$ANDROID_ROOT_DIR/external/ot-br-posix
+
+ALLOWED_CODE_DIRS=($CONNECTIVITY_DIR $OPENTHREAD_DIR $OTBR_POSIX_DIR)
+CODE_DIR=$(git rev-parse --show-toplevel)
+
+if [[ ! " ${ALLOWED_CODE_DIRS[@]} " =~ " ${CODE_DIR} " ]]; then
+ echo "Error: The script has to run in the Git project Connectivity, openthread or ot-br-posix"
+ exit 1
+fi
+
+if [[ $CODE_DIR == $CONNECTIVITY_DIR ]]; then
+ CODE_DIR=$CODE_DIR"/thread"
+fi
+
+$GOOGLE_JAVA_FORMAT --aosp -i $(find $CODE_DIR -name "*.java")
+$ANDROID_BP_FORMAT -w $(find $CODE_DIR -name "*.bp")
+$AIDL_FORMAT -w $(find $CODE_DIR -name "*.aidl")
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index a82a499..1f4e601 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -37,7 +37,7 @@
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
"framework-location.stubs.module_lib",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"service-connectivity-pre-jarjar",
"ServiceConnectivityResources",
],
diff --git a/thread/service/java/com/android/server/thread/FeatureFlags.java b/thread/service/java/com/android/server/thread/FeatureFlags.java
new file mode 100644
index 0000000..29bcedd
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/FeatureFlags.java
@@ -0,0 +1,37 @@
+/*
+ * 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.thread;
+
+import com.android.net.module.util.DeviceConfigUtils;
+
+public class FeatureFlags {
+ // The namespace for Thread Network feature flags
+ private static final String NAMESPACE_THREAD_NETWORK = "thread_network";
+
+ // The prefix for TREL feature flags
+ private static final String TREL_FEATURE_PREFIX = "TrelFeature__";
+
+ // The feature flag for TREL enabled state
+ private static final String TREL_ENABLED_FLAG = TREL_FEATURE_PREFIX + "enabled";
+
+ private static boolean isFeatureEnabled(String flag, boolean defaultValue) {
+ return DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+ NAMESPACE_THREAD_NETWORK, flag, defaultValue);
+ }
+
+ public static boolean isTrelEnabled() {
+ return isFeatureEnabled(TREL_ENABLED_FLAG, false);
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
index 1447ff8..9697c02 100644
--- a/thread/service/java/com/android/server/thread/NsdPublisher.java
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.net.DnsResolver;
import android.net.InetAddresses;
+import android.net.LinkProperties;
import android.net.Network;
import android.net.nsd.DiscoveryRequest;
import android.net.nsd.NsdManager;
@@ -30,11 +31,12 @@
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.RemoteException;
+import android.system.Os;
import android.text.TextUtils;
-import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
import com.android.server.thread.openthread.DnsTxtAttribute;
import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
import com.android.server.thread.openthread.INsdPublisher;
@@ -62,9 +64,11 @@
*/
public final class NsdPublisher extends INsdPublisher.Stub {
private static final String TAG = NsdPublisher.class.getSimpleName();
+ private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
// TODO: b/321883491 - specify network for mDNS operations
@Nullable private Network mNetwork;
+ private final Map<Network, LinkProperties> mNetworkToLinkProperties;
private final NsdManager mNsdManager;
private final DnsResolver mDnsResolver;
private final Handler mHandler;
@@ -75,17 +79,28 @@
private final SparseArray<HostInfoListener> mHostInfoListeners = new SparseArray<>(0);
@VisibleForTesting
- public NsdPublisher(NsdManager nsdManager, DnsResolver dnsResolver, Handler handler) {
+ public NsdPublisher(
+ NsdManager nsdManager,
+ DnsResolver dnsResolver,
+ Handler handler,
+ Map<Network, LinkProperties> networkToLinkProperties) {
mNetwork = null;
mNsdManager = nsdManager;
mDnsResolver = dnsResolver;
mHandler = handler;
mExecutor = runnable -> mHandler.post(runnable);
+ mNetworkToLinkProperties = networkToLinkProperties;
}
- public static NsdPublisher newInstance(Context context, Handler handler) {
+ public static NsdPublisher newInstance(
+ Context context,
+ Handler handler,
+ Map<Network, LinkProperties> networkToLinkProperties) {
return new NsdPublisher(
- context.getSystemService(NsdManager.class), DnsResolver.getInstance(), handler);
+ context.getSystemService(NsdManager.class),
+ DnsResolver.getInstance(),
+ handler,
+ networkToLinkProperties);
}
// TODO: b/321883491 - NsdPublisher should be disabled when mNetwork is null
@@ -158,8 +173,7 @@
int listenerId,
String registrationType) {
checkOnHandlerThread();
- Log.i(
- TAG,
+ LOG.i(
"Registering "
+ registrationType
+ ". Listener ID: "
@@ -171,7 +185,7 @@
try {
mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, mExecutor, listener);
} catch (IllegalArgumentException e) {
- Log.i(TAG, "Failed to register service. serviceInfo: " + serviceInfo, e);
+ LOG.e("Failed to register service. serviceInfo: " + serviceInfo, e);
listener.onRegistrationFailed(serviceInfo, NsdManager.FAILURE_INTERNAL_ERROR);
}
}
@@ -184,8 +198,7 @@
checkOnHandlerThread();
RegistrationListener registrationListener = mRegistrationListeners.get(listenerId);
if (registrationListener == null) {
- Log.w(
- TAG,
+ LOG.w(
"Failed to unregister service."
+ " Listener ID: "
+ listenerId
@@ -193,8 +206,7 @@
return;
}
- Log.i(
- TAG,
+ LOG.i(
"Unregistering service."
+ " Listener ID: "
+ listenerId
@@ -212,13 +224,7 @@
private void discoverServiceInternal(
String type, INsdDiscoverServiceCallback callback, int listenerId) {
checkOnHandlerThread();
- Log.i(
- TAG,
- "Discovering services."
- + " Listener ID: "
- + listenerId
- + ", service type: "
- + type);
+ LOG.i("Discovering services." + " Listener ID: " + listenerId + ", service type: " + type);
DiscoveryListener listener = new DiscoveryListener(listenerId, type, callback);
mDiscoveryListeners.append(listenerId, listener);
@@ -237,15 +243,14 @@
DiscoveryListener listener = mDiscoveryListeners.get(listenerId);
if (listener == null) {
- Log.w(
- TAG,
+ LOG.w(
"Failed to stop service discovery. Listener ID "
+ listenerId
+ ". The listener is null.");
return;
}
- Log.i(TAG, "Stopping service discovery. Listener: " + listener);
+ LOG.i("Stopping service discovery. Listener: " + listener);
mNsdManager.stopServiceDiscovery(listener);
}
@@ -263,8 +268,7 @@
serviceInfo.setServiceName(name);
serviceInfo.setServiceType(type);
serviceInfo.setNetwork(null);
- Log.i(
- TAG,
+ LOG.i(
"Resolving service."
+ " Listener ID: "
+ listenerId
@@ -288,21 +292,19 @@
ServiceInfoListener listener = mServiceInfoListeners.get(listenerId);
if (listener == null) {
- Log.w(
- TAG,
+ LOG.w(
"Failed to stop service resolution. Listener ID: "
+ listenerId
+ ". The listener is null.");
return;
}
- Log.i(TAG, "Stopping service resolution. Listener: " + listener);
+ LOG.i("Stopping service resolution. Listener: " + listener);
try {
mNsdManager.unregisterServiceInfoCallback(listener);
} catch (IllegalArgumentException e) {
- Log.w(
- TAG,
+ LOG.w(
"Failed to stop the service resolution because it's already stopped. Listener: "
+ listener);
}
@@ -330,7 +332,7 @@
listener);
mHostInfoListeners.append(listenerId, listener);
- Log.i(TAG, "Resolving host." + " Listener ID: " + listenerId + ", hostname: " + name);
+ LOG.i("Resolving host." + " Listener ID: " + listenerId + ", hostname: " + name);
}
@Override
@@ -343,14 +345,13 @@
HostInfoListener listener = mHostInfoListeners.get(listenerId);
if (listener == null) {
- Log.w(
- TAG,
+ LOG.w(
"Failed to stop host resolution. Listener ID: "
+ listenerId
+ ". The listener is null.");
return;
}
- Log.i(TAG, "Stopping host resolution. Listener: " + listener);
+ LOG.i("Stopping host resolution. Listener: " + listener);
listener.cancel();
mHostInfoListeners.remove(listenerId);
}
@@ -373,14 +374,14 @@
try {
mNsdManager.unregisterService(mRegistrationListeners.valueAt(i));
} catch (IllegalArgumentException e) {
- Log.i(
- TAG,
+ LOG.i(
"Failed to unregister."
+ " Listener ID: "
+ mRegistrationListeners.keyAt(i)
+ " serviceInfo: "
- + mRegistrationListeners.valueAt(i).mServiceInfo,
- e);
+ + mRegistrationListeners.valueAt(i).mServiceInfo
+ + ", error: "
+ + e.getMessage());
}
}
mRegistrationListeners.clear();
@@ -415,8 +416,7 @@
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
checkOnHandlerThread();
mRegistrationListeners.remove(mListenerId);
- Log.i(
- TAG,
+ LOG.i(
"Failed to register listener ID: "
+ mListenerId
+ " error code: "
@@ -434,8 +434,7 @@
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
checkOnHandlerThread();
for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
- Log.i(
- TAG,
+ LOG.i(
"Failed to unregister."
+ "Listener ID: "
+ mListenerId
@@ -454,8 +453,7 @@
@Override
public void onServiceRegistered(NsdServiceInfo serviceInfo) {
checkOnHandlerThread();
- Log.i(
- TAG,
+ LOG.i(
"Registered successfully. "
+ "Listener ID: "
+ mListenerId
@@ -472,8 +470,7 @@
public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
checkOnHandlerThread();
for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
- Log.i(
- TAG,
+ LOG.i(
"Unregistered successfully. "
+ "Listener ID: "
+ mListenerId
@@ -505,8 +502,7 @@
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
- Log.e(
- TAG,
+ LOG.e(
"Failed to start service discovery."
+ " Error code: "
+ errorCode
@@ -517,8 +513,7 @@
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
- Log.e(
- TAG,
+ LOG.e(
"Failed to stop service discovery."
+ " Error code: "
+ errorCode
@@ -529,18 +524,18 @@
@Override
public void onDiscoveryStarted(String serviceType) {
- Log.i(TAG, "Started service discovery. Listener: " + this);
+ LOG.i("Started service discovery. Listener: " + this);
}
@Override
public void onDiscoveryStopped(String serviceType) {
- Log.i(TAG, "Stopped service discovery. Listener: " + this);
+ LOG.i("Stopped service discovery. Listener: " + this);
mDiscoveryListeners.remove(mListenerId);
}
@Override
public void onServiceFound(NsdServiceInfo serviceInfo) {
- Log.i(TAG, "Found service: " + serviceInfo);
+ LOG.i("Found service: " + serviceInfo);
try {
mDiscoverServiceCallback.onServiceDiscovered(
serviceInfo.getServiceName(), mType, true);
@@ -551,7 +546,7 @@
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {
- Log.i(TAG, "Lost service: " + serviceInfo);
+ LOG.i("Lost service: " + serviceInfo);
try {
mDiscoverServiceCallback.onServiceDiscovered(
serviceInfo.getServiceName(), mType, false);
@@ -584,8 +579,7 @@
@Override
public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
- Log.e(
- TAG,
+ LOG.e(
"Failed to register service info callback."
+ " Listener ID: "
+ mListenerId
@@ -599,14 +593,21 @@
@Override
public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
- Log.i(
- TAG,
+ LOG.i(
"Service is resolved. "
+ " Listener ID: "
+ mListenerId
+ ", serviceInfo: "
+ serviceInfo);
List<String> addresses = new ArrayList<>();
+ int interfaceIndex = 0;
+ if (mNetworkToLinkProperties.containsKey(serviceInfo.getNetwork())) {
+ interfaceIndex =
+ Os.if_nametoindex(
+ mNetworkToLinkProperties
+ .get(serviceInfo.getNetwork())
+ .getInterfaceName());
+ }
for (InetAddress address : serviceInfo.getHostAddresses()) {
if (address instanceof Inet6Address) {
addresses.add(address.getHostAddress());
@@ -623,6 +624,7 @@
try {
mResolveServiceCallback.onServiceResolved(
serviceInfo.getHostname(),
+ interfaceIndex,
serviceInfo.getServiceName(),
serviceInfo.getServiceType(),
serviceInfo.getPort(),
@@ -640,7 +642,7 @@
@Override
public void onServiceInfoCallbackUnregistered() {
- Log.i(TAG, "The service info callback is unregistered. Listener: " + this);
+ LOG.i("The service info callback is unregistered. Listener: " + this);
mServiceInfoListeners.remove(mListenerId);
}
@@ -671,8 +673,7 @@
public void onAnswer(@NonNull List<InetAddress> answerList, int rcode) {
checkOnHandlerThread();
- Log.i(
- TAG,
+ LOG.i(
"Host is resolved."
+ " Listener ID: "
+ mListenerId
@@ -698,14 +699,14 @@
public void onError(@NonNull DnsResolver.DnsException error) {
checkOnHandlerThread();
- Log.i(
- TAG,
+ LOG.i(
"Failed to resolve host."
+ " Listener ID: "
+ mListenerId
+ ", hostname: "
- + mHostname,
- error);
+ + mHostname
+ + ", error: "
+ + error.getMessage());
try {
mResolveHostCallback.onHostResolved(mHostname, Collections.emptyList());
} catch (RemoteException e) {
diff --git a/thread/service/java/com/android/server/thread/OutputReceiverWrapper.java b/thread/service/java/com/android/server/thread/OutputReceiverWrapper.java
new file mode 100644
index 0000000..aa9a05d
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/OutputReceiverWrapper.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+
+import android.net.thread.IOutputReceiver;
+import android.net.thread.ThreadNetworkException;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** A {@link IOutputReceiver} wrapper which makes it easier to invoke the callbacks. */
+final class OutputReceiverWrapper {
+ private final IOutputReceiver mReceiver;
+ private final boolean mExpectOtDaemonDied;
+
+ private static final Object sPendingReceiversLock = new Object();
+
+ @GuardedBy("sPendingReceiversLock")
+ private static final Set<OutputReceiverWrapper> sPendingReceivers = new HashSet<>();
+
+ public OutputReceiverWrapper(IOutputReceiver receiver) {
+ this(receiver, false /* expectOtDaemonDied */);
+ }
+
+ /**
+ * Creates a new {@link OutputReceiverWrapper}.
+ *
+ * <p>If {@code expectOtDaemonDied} is {@code true}, it's expected that ot-daemon becomes dead
+ * before {@code receiver} is completed with {@code onComplete} and {@code onError} and {@code
+ * receiver#onComplete} will be invoked in this case.
+ */
+ public OutputReceiverWrapper(IOutputReceiver receiver, boolean expectOtDaemonDied) {
+ mReceiver = receiver;
+ mExpectOtDaemonDied = expectOtDaemonDied;
+
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.add(this);
+ }
+ }
+
+ public static void onOtDaemonDied() {
+ synchronized (sPendingReceiversLock) {
+ for (OutputReceiverWrapper receiver : sPendingReceivers) {
+ try {
+ if (receiver.mExpectOtDaemonDied) {
+ receiver.mReceiver.onComplete();
+ } else {
+ receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+ }
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+ sPendingReceivers.clear();
+ }
+ }
+
+ public void onOutput(String output) {
+ try {
+ mReceiver.onOutput(output);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+
+ public void onComplete() {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onComplete();
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+
+ public void onError(Throwable e) {
+ if (e instanceof ThreadNetworkException) {
+ ThreadNetworkException threadException = (ThreadNetworkException) e;
+ onError(threadException.getErrorCode(), threadException.getMessage());
+ } else if (e instanceof RemoteException) {
+ onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ } else {
+ throw new AssertionError(e);
+ }
+ }
+
+ public void onError(int errorCode, String errorMessage, Object... messageArgs) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onError(errorCode, String.format(errorMessage, messageArgs));
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index e6f272b..30d5a02 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -15,9 +15,11 @@
package com.android.server.thread;
import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE;
import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
+import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID;
import static android.net.thread.ActiveOperationalDataset.LENGTH_MESH_LOCAL_PREFIX_BITS;
@@ -26,6 +28,7 @@
import static android.net.thread.ActiveOperationalDataset.MESH_LOCAL_PREFIX_FIRST_BYTE;
import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
@@ -40,9 +43,10 @@
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
-import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_TESTING;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
@@ -75,6 +79,8 @@
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.LocalNetworkConfig;
import android.net.LocalNetworkInfo;
@@ -94,6 +100,7 @@
import android.net.thread.IConfigurationReceiver;
import android.net.thread.IOperationReceiver;
import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IOutputReceiver;
import android.net.thread.IStateCallback;
import android.net.thread.IThreadNetworkController;
import android.net.thread.OperationalDatasetTimestamp;
@@ -108,32 +115,38 @@
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
+import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserManager;
-import android.util.Log;
import android.util.SparseArray;
import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.IIpv4PrefixRequest;
+import com.android.net.module.util.RoutingCoordinatorManager;
+import com.android.net.module.util.SharedLog;
import com.android.server.ServiceManagerWrapper;
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.thread.openthread.BackboneRouterState;
-import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
import com.android.server.thread.openthread.DnsTxtAttribute;
import com.android.server.thread.openthread.IChannelMasksReceiver;
import com.android.server.thread.openthread.IOtDaemon;
import com.android.server.thread.openthread.IOtDaemonCallback;
+import com.android.server.thread.openthread.IOtOutputReceiver;
import com.android.server.thread.openthread.IOtStatusReceiver;
+import com.android.server.thread.openthread.InfraLinkState;
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.OtDaemonConfiguration;
import com.android.server.thread.openthread.OtDaemonState;
import libcore.util.HexEncoding;
import java.io.IOException;
import java.net.Inet6Address;
+import java.net.InetAddress;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.DateTimeException;
@@ -158,7 +171,8 @@
*/
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
- private static final String TAG = "ThreadNetworkService";
+ private static final String TAG = "ControllerService";
+ private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
// The model name length in utf-8 bytes
private static final int MAX_MODEL_NAME_UTF8_BYTES = 24;
@@ -185,10 +199,12 @@
private final NetworkProvider mNetworkProvider;
private final Supplier<IOtDaemon> mOtDaemonSupplier;
private final ConnectivityManager mConnectivityManager;
+ private final RoutingCoordinatorManager mRoutingCoordinatorManager;
private final TunInterfaceController mTunIfController;
private final InfraInterfaceController mInfraIfController;
private final NsdPublisher mNsdPublisher;
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
+ private final Nat64CidrController mNat64CidrController = new Nat64CidrController();
private final ConnectivityResources mResources;
private final Supplier<String> mCountryCodeSupplier;
private final Map<IConfigurationReceiver, IBinder.DeathRecipient> mConfigurationReceivers =
@@ -206,13 +222,14 @@
private NetworkRequest mUpstreamNetworkRequest;
private UpstreamNetworkCallback mUpstreamNetworkCallback;
private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
- private final HashMap<Network, String> mNetworkToInterface;
+ private ThreadNetworkCallback mThreadNetworkCallback;
+ private final Map<Network, LinkProperties> mNetworkToLinkProperties;
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
private boolean mUserRestricted;
private boolean mForceStopOtDaemonEnabled;
- private BorderRouterConfigurationParcel mBorderRouterConfig;
+ private InfraLinkState mInfraLinkState;
@VisibleForTesting
ThreadNetworkControllerService(
@@ -221,23 +238,28 @@
NetworkProvider networkProvider,
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
+ RoutingCoordinatorManager routingCoordinatorManager,
TunInterfaceController tunIfController,
InfraInterfaceController infraIfController,
ThreadPersistentSettings persistentSettings,
NsdPublisher nsdPublisher,
UserManager userManager,
ConnectivityResources resources,
- Supplier<String> countryCodeSupplier) {
+ Supplier<String> countryCodeSupplier,
+ Map<Network, LinkProperties> networkToLinkProperties) {
mContext = context;
mHandler = handler;
mNetworkProvider = networkProvider;
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
+ mRoutingCoordinatorManager = routingCoordinatorManager;
mTunIfController = tunIfController;
mInfraIfController = infraIfController;
mUpstreamNetworkRequest = newUpstreamNetworkRequest();
- mNetworkToInterface = new HashMap<Network, String>();
- mBorderRouterConfig = new BorderRouterConfigurationParcel();
+ // TODO: networkToLinkProperties should be shared with NsdPublisher, add a test/assert to
+ // verify they are the same.
+ mNetworkToLinkProperties = networkToLinkProperties;
+ mInfraLinkState = new InfraLinkState.Builder().build();
mPersistentSettings = persistentSettings;
mNsdPublisher = nsdPublisher;
mUserManager = userManager;
@@ -254,27 +276,37 @@
Handler handler = new Handler(handlerThread.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
+ Map<Network, LinkProperties> networkToLinkProperties = new HashMap<>();
+ final ConnectivityManager connectivityManager =
+ context.getSystemService(ConnectivityManager.class);
+ final RoutingCoordinatorManager routingCoordinatorManager =
+ new RoutingCoordinatorManager(
+ context, connectivityManager.getRoutingCoordinatorService());
return new ThreadNetworkControllerService(
context,
handler,
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
- context.getSystemService(ConnectivityManager.class),
+ connectivityManager,
+ routingCoordinatorManager,
new TunInterfaceController(TUN_IF_NAME),
new InfraInterfaceController(),
persistentSettings,
- NsdPublisher.newInstance(context, handler),
+ NsdPublisher.newInstance(context, handler, networkToLinkProperties),
context.getSystemService(UserManager.class),
new ConnectivityResources(context),
- countryCodeSupplier);
+ countryCodeSupplier,
+ networkToLinkProperties);
}
private NetworkRequest newUpstreamNetworkRequest() {
- NetworkRequest.Builder builder = new NetworkRequest.Builder().clearCapabilities();
+ NetworkRequest.Builder builder = new NetworkRequest.Builder();
if (mUpstreamTestNetworkSpecifier != null) {
- return builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ // Test networks don't have NET_CAPABILITY_TRUSTED
+ return builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
.setNetworkSpecifier(mUpstreamTestNetworkSpecifier)
.build();
}
@@ -284,25 +316,17 @@
.build();
}
- private LocalNetworkConfig newLocalNetworkConfig() {
- return new LocalNetworkConfig.Builder()
- .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
- .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
- .setUpstreamSelector(mUpstreamNetworkRequest)
- .build();
- }
-
private void maybeInitializeOtDaemon() {
if (!shouldEnableThread()) {
return;
}
- Log.i(TAG, "Starting OT daemon...");
+ LOG.i("Starting OT daemon...");
try {
getOtDaemon();
} catch (RemoteException e) {
- Log.e(TAG, "Failed to initialize ot-daemon", e);
+ LOG.e("Failed to initialize ot-daemon", e);
} catch (ThreadNetworkException e) {
// no ThreadNetworkException.ERROR_THREAD_DISABLED error should be thrown
throw new AssertionError(e);
@@ -329,12 +353,15 @@
otDaemon.initialize(
mTunIfController.getTunFd(),
shouldEnableThread(),
+ newOtDaemonConfig(mPersistentSettings.getConfiguration()),
mNsdPublisher,
getMeshcopTxtAttributes(mResources.get()),
mOtDaemonCallbackProxy,
- mCountryCodeSupplier.get());
+ mCountryCodeSupplier.get(),
+ FeatureFlags.isTrelEnabled());
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
+ mHandler.post(mNat64CidrController::maybeUpdateNat64Cidr);
return mOtDaemon;
}
@@ -416,9 +443,10 @@
private void onOtDaemonDied() {
checkOnHandlerThread();
- Log.w(TAG, "OT daemon is dead, clean up...");
+ LOG.w("OT daemon is dead, clean up...");
OperationReceiverWrapper.onOtDaemonDied();
+ OutputReceiverWrapper.onOtDaemonDied();
mOtDaemonCallbackProxy.onOtDaemonDied();
mTunIfController.onOtDaemonDied();
mNsdPublisher.onOtDaemonDied();
@@ -427,25 +455,32 @@
}
public void initialize() {
- mHandler.post(
- () -> {
- Log.d(
- TAG,
- "Initializing Thread system service: Thread is "
- + (shouldEnableThread() ? "enabled" : "disabled"));
- try {
- mTunIfController.createTunInterface();
- } catch (IOException e) {
- throw new IllegalStateException(
- "Failed to create Thread tunnel interface", e);
- }
- mConnectivityManager.registerNetworkProvider(mNetworkProvider);
- requestUpstreamNetwork();
- requestThreadNetwork();
- mUserRestricted = isThreadUserRestricted();
- registerUserRestrictionsReceiver();
- maybeInitializeOtDaemon();
- });
+ mHandler.post(() -> initializeInternal());
+ }
+
+ private void initializeInternal() {
+ checkOnHandlerThread();
+
+ LOG.v(
+ "Initializing Thread system service: Thread is "
+ + (shouldEnableThread() ? "enabled" : "disabled"));
+ try {
+ mTunIfController.createTunInterface();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create Thread tunnel interface", e);
+ }
+ mConnectivityManager.registerNetworkProvider(mNetworkProvider);
+ mUserRestricted = isThreadUserRestricted();
+ registerUserRestrictionsReceiver();
+
+ if (isBorderRouterMode()) {
+ requestUpstreamNetwork();
+ registerThreadNetworkCallback();
+ } else {
+ cancelRequestUpstreamNetwork();
+ unregisterThreadNetworkCallback();
+ }
+ maybeInitializeOtDaemon();
}
/**
@@ -487,7 +522,7 @@
// become dead, so that it's guaranteed that ot-daemon is stopped when {@code
// receiver} is completed
} catch (RemoteException e) {
- Log.e(TAG, "otDaemon.terminate failed", e);
+ LOG.e("otDaemon.terminate failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
} catch (ThreadNetworkException e) {
// No ThreadNetworkException.ERROR_THREAD_DISABLED error will be thrown
@@ -518,7 +553,7 @@
return;
}
- Log.i(TAG, "Set Thread enabled: " + isEnabled + ", persist: " + persist);
+ LOG.i("Set Thread enabled: " + isEnabled + ", persist: " + persist);
if (persist) {
// The persistent setting keeps the desired enabled state, thus it's set regardless
@@ -530,7 +565,7 @@
try {
getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
+ LOG.e("otDaemon.setThreadEnabled failed", e);
receiver.onError(e);
}
}
@@ -539,22 +574,34 @@
public void setConfiguration(
@NonNull ThreadConfiguration configuration, @NonNull IOperationReceiver receiver) {
enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
- mHandler.post(() -> setConfigurationInternal(configuration, receiver));
+ mHandler.post(
+ () ->
+ setConfigurationInternal(
+ configuration, new OperationReceiverWrapper(receiver)));
}
private void setConfigurationInternal(
@NonNull ThreadConfiguration configuration,
- @NonNull IOperationReceiver operationReceiver) {
+ @NonNull OperationReceiverWrapper receiver) {
checkOnHandlerThread();
- Log.i(TAG, "Set Thread configuration: " + configuration);
+ LOG.i("Set Thread configuration: " + configuration);
final boolean changed = mPersistentSettings.putConfiguration(configuration);
- try {
- operationReceiver.onSuccess();
- } catch (RemoteException e) {
- // do nothing if the client is dead
+
+ if (changed) {
+ if (isBorderRouterMode()) {
+ requestUpstreamNetwork();
+ registerThreadNetworkCallback();
+ } else {
+ cancelRequestUpstreamNetwork();
+ unregisterThreadNetworkCallback();
+ disableBorderRouting();
+ }
}
+
+ receiver.onSuccess();
+
if (changed) {
for (IConfigurationReceiver configReceiver : mConfigurationReceivers.keySet()) {
try {
@@ -564,6 +611,30 @@
}
}
}
+
+ try {
+ getOtDaemon()
+ .setConfiguration(
+ newOtDaemonConfig(configuration),
+ new LoggingOtStatusReceiver("setConfiguration"));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("otDaemon.setConfiguration failed. Config: " + configuration, e);
+ }
+ mNat64CidrController.maybeUpdateNat64Cidr();
+ }
+
+ private static OtDaemonConfiguration newOtDaemonConfig(
+ @NonNull ThreadConfiguration threadConfig) {
+ return new OtDaemonConfiguration.Builder()
+ .setBorderRouterEnabled(threadConfig.isBorderRouterEnabled())
+ .setNat64Enabled(threadConfig.isNat64Enabled())
+ .setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled())
+ .build();
+ }
+
+ /** Returns {@code true} if this device is operating as a border router. */
+ private boolean isBorderRouterMode() {
+ return mPersistentSettings.getConfiguration().isBorderRouterEnabled();
}
@Override
@@ -624,8 +695,7 @@
if (mUserRestricted == newUserRestrictedState) {
return;
}
- Log.i(
- TAG,
+ LOG.i(
"Thread user restriction changed: "
+ mUserRestricted
+ " -> "
@@ -637,16 +707,14 @@
new IOperationReceiver.Stub() {
@Override
public void onSuccess() {
- Log.d(
- TAG,
+ LOG.v(
(shouldEnableThread ? "Enabled" : "Disabled")
+ " Thread due to user restriction change");
}
@Override
public void onError(int errorCode, String errorMessage) {
- Log.e(
- TAG,
+ LOG.e(
"Failed to "
+ (shouldEnableThread ? "enable" : "disable")
+ " Thread for user restriction change");
@@ -675,7 +743,7 @@
private void requestUpstreamNetwork() {
if (mUpstreamNetworkCallback != null) {
- throw new AssertionError("The upstream network request is already there.");
+ return;
}
mUpstreamNetworkCallback = new UpstreamNetworkCallback();
mConnectivityManager.registerNetworkCallback(
@@ -684,9 +752,9 @@
private void cancelRequestUpstreamNetwork() {
if (mUpstreamNetworkCallback == null) {
- throw new AssertionError("The upstream network request null.");
+ return;
}
- mNetworkToInterface.clear();
+ mNetworkToLinkProperties.clear();
mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
mUpstreamNetworkCallback = null;
}
@@ -695,33 +763,32 @@
@Override
public void onAvailable(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "Upstream network available: " + network);
+ LOG.i("Upstream network available: " + network);
}
@Override
public void onLost(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "Upstream network lost: " + network);
+ LOG.i("Upstream network lost: " + network);
// TODO: disable border routing when upsteam network disconnected
}
@Override
public void onLinkPropertiesChanged(
- @NonNull Network network, @NonNull LinkProperties linkProperties) {
+ @NonNull Network network, @NonNull LinkProperties newLinkProperties) {
checkOnHandlerThread();
- String existingIfName = mNetworkToInterface.get(network);
- String newIfName = linkProperties.getInterfaceName();
- if (Objects.equals(existingIfName, newIfName)) {
+ LinkProperties oldLinkProperties = mNetworkToLinkProperties.get(network);
+ if (Objects.equals(oldLinkProperties, newLinkProperties)) {
return;
}
- Log.i(TAG, "Upstream network changed: " + existingIfName + " -> " + newIfName);
- mNetworkToInterface.put(network, newIfName);
+ LOG.i("Upstream network changed: " + oldLinkProperties + " -> " + newLinkProperties);
+ mNetworkToLinkProperties.put(network, newLinkProperties);
// TODO: disable border routing if netIfName is null
if (network.equals(mUpstreamNetwork)) {
- enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+ setInfraLinkState(newInfraLinkStateBuilder(newLinkProperties).build());
}
}
}
@@ -730,52 +797,63 @@
@Override
public void onAvailable(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "Thread network is available: " + network);
+ LOG.i("Thread network is available: " + network);
}
@Override
public void onLost(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "Thread network is lost: " + network);
- disableBorderRouting();
+ LOG.i("Thread network is lost: " + network);
+ setInfraLinkState(newInfraLinkStateBuilder().build());
}
@Override
public void onLocalNetworkInfoChanged(
@NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
checkOnHandlerThread();
- Log.i(
- TAG,
+ LOG.i(
"LocalNetworkInfo of Thread network changed: {threadNetwork: "
+ network
+ ", localNetworkInfo: "
+ localNetworkInfo
+ "}");
- if (localNetworkInfo.getUpstreamNetwork() == null) {
- disableBorderRouting();
+ mUpstreamNetwork = localNetworkInfo.getUpstreamNetwork();
+ if (mUpstreamNetwork == null) {
+ setInfraLinkState(newInfraLinkStateBuilder().build());
return;
}
- if (!localNetworkInfo.getUpstreamNetwork().equals(mUpstreamNetwork)) {
- mUpstreamNetwork = localNetworkInfo.getUpstreamNetwork();
- if (mNetworkToInterface.containsKey(mUpstreamNetwork)) {
- enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
- }
- mNsdPublisher.setNetworkForHostResolution(mUpstreamNetwork);
+ if (mNetworkToLinkProperties.containsKey(mUpstreamNetwork)) {
+ setInfraLinkState(
+ newInfraLinkStateBuilder(mNetworkToLinkProperties.get(mUpstreamNetwork))
+ .build());
}
+ mNsdPublisher.setNetworkForHostResolution(mUpstreamNetwork);
}
}
- private void requestThreadNetwork() {
- mConnectivityManager.registerNetworkCallback(
+ private void registerThreadNetworkCallback() {
+ if (mThreadNetworkCallback != null) {
+ return;
+ }
+
+ mThreadNetworkCallback = new ThreadNetworkCallback();
+ NetworkRequest request =
new NetworkRequest.Builder()
// clearCapabilities() is needed to remove forbidden capabilities and UID
// requirement.
.clearCapabilities()
- .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .addTransportType(TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
- .build(),
- new ThreadNetworkCallback(),
- mHandler);
+ .build();
+ mConnectivityManager.registerNetworkCallback(request, mThreadNetworkCallback, mHandler);
+ }
+
+ private void unregisterThreadNetworkCallback() {
+ if (mThreadNetworkCallback == null) {
+ return;
+ }
+ mConnectivityManager.unregisterNetworkCallback(mThreadNetworkCallback);
+ mThreadNetworkCallback = null;
}
/** Injects a {@link NetworkAgent} for testing. */
@@ -789,27 +867,46 @@
return mTestNetworkAgent;
}
- final NetworkCapabilities netCaps =
+ final var netCapsBuilder =
new NetworkCapabilities.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+ .addTransportType(TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
- .build();
- final NetworkScore score =
- new NetworkScore.Builder()
- .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
- .build();
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+ final var scoreBuilder = new NetworkScore.Builder();
+
+ if (isBorderRouterMode()) {
+ netCapsBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK);
+ scoreBuilder.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK);
+ }
+
return new NetworkAgent(
mContext,
mHandler.getLooper(),
- TAG,
- netCaps,
- mTunIfController.getLinkProperties(),
- newLocalNetworkConfig(),
- score,
+ LOG.getTag(),
+ netCapsBuilder.build(),
+ getTunIfLinkProperties(),
+ isBorderRouterMode() ? newLocalNetworkConfig() : null,
+ scoreBuilder.build(),
new NetworkAgentConfig.Builder().build(),
- mNetworkProvider) {};
+ mNetworkProvider) {
+
+ // TODO(b/374037595): use NetworkFactory to handle dynamic network requests
+ @Override
+ public void onNetworkUnwanted() {
+ LOG.i("Thread network is unwanted by ConnectivityService");
+ if (!isBorderRouterMode()) {
+ leave(false /* eraseDataset */, new LoggingOperationReceiver("leave"));
+ }
+ }
+ };
+ }
+
+ private LocalNetworkConfig newLocalNetworkConfig() {
+ return new LocalNetworkConfig.Builder()
+ .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
+ .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
+ .setUpstreamSelector(mUpstreamNetworkRequest)
+ .build();
}
private void registerThreadNetwork() {
@@ -820,7 +917,7 @@
mNetworkAgent = newNetworkAgent();
mNetworkAgent.register();
mNetworkAgent.markConnected();
- Log.i(TAG, "Registered Thread network");
+ LOG.i("Registered Thread network");
}
private void unregisterThreadNetwork() {
@@ -830,7 +927,7 @@
return;
}
- Log.d(TAG, "Unregistering Thread network agent");
+ LOG.v("Unregistering Thread network agent");
mNetworkAgent.unregister();
mNetworkAgent = null;
@@ -842,6 +939,59 @@
}
@Override
+ public void activateEphemeralKeyMode(long lifetimeMillis, IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(
+ () ->
+ activateEphemeralKeyModeInternal(
+ lifetimeMillis, new OperationReceiverWrapper(receiver)));
+ }
+
+ private void activateEphemeralKeyModeInternal(
+ long lifetimeMillis, OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ if (!isBorderRouterMode()) {
+ receiver.onError(
+ ERROR_FAILED_PRECONDITION, "This device is not configured a Border Router");
+ return;
+ }
+
+ try {
+ getOtDaemon().activateEphemeralKeyMode(lifetimeMillis, newOtStatusReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("otDaemon.activateEphemeralKeyMode failed", e);
+ receiver.onError(e);
+ }
+ }
+
+ @Override
+ public void deactivateEphemeralKeyMode(IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(
+ () -> deactivateEphemeralKeyModeInternal(new OperationReceiverWrapper(receiver)));
+ }
+
+ private void deactivateEphemeralKeyModeInternal(OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ if (!isBorderRouterMode()) {
+ receiver.onError(
+ ERROR_FAILED_PRECONDITION, "This device is not configured a Border Router");
+ return;
+ }
+
+ try {
+ getOtDaemon().deactivateEphemeralKeyMode(newOtStatusReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("otDaemon.deactivateEphemeralKeyMode failed", e);
+ receiver.onError(e);
+ }
+ }
+
+ @Override
public void createRandomizedDataset(
String networkName, IActiveOperationalDatasetReceiver receiver) {
ActiveOperationalDatasetReceiverWrapper receiverWrapper =
@@ -856,7 +1006,7 @@
try {
getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.getChannelMasks failed", e);
+ LOG.e("otDaemon.getChannelMasks failed", e);
receiver.onError(e);
}
}
@@ -897,7 +1047,7 @@
now = clock.instant();
authoritative = true;
} catch (DateTimeException e) {
- Log.w(TAG, "Failed to get authoritative time", e);
+ LOG.w("Failed to get authoritative time: " + e.getMessage());
}
int panId = random.nextInt(/* bound= */ 0xffff);
@@ -990,7 +1140,14 @@
@Override
public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
- mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
+ boolean hasThreadPrivilegedPermission =
+ (mContext.checkCallingOrSelfPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+ == PERMISSION_GRANTED);
+
+ mHandler.post(
+ () ->
+ mOtDaemonCallbackProxy.registerStateCallback(
+ stateCallback, hasThreadPrivilegedPermission));
}
@Override
@@ -1039,6 +1196,25 @@
};
}
+ private IOtOutputReceiver newOtOutputReceiver(OutputReceiverWrapper receiver) {
+ return new IOtOutputReceiver.Stub() {
+ @Override
+ public void onOutput(String output) {
+ receiver.onOutput(output);
+ }
+
+ @Override
+ public void onComplete() {
+ receiver.onComplete();
+ }
+
+ @Override
+ public void onError(int otError, String message) {
+ receiver.onError(otErrorToAndroidError(otError), message);
+ }
+ };
+ }
+
@ErrorCode
private static int otErrorToAndroidError(int otError) {
// See external/openthread/include/openthread/error.h for OT error definition
@@ -1048,7 +1224,7 @@
case OT_ERROR_BUSY:
return ERROR_BUSY;
case OT_ERROR_NOT_IMPLEMENTED:
- return ERROR_UNSUPPORTED_OPERATION;
+ return ERROR_UNSUPPORTED_FEATURE;
case OT_ERROR_NO_BUFS:
return ERROR_RESOURCE_EXHAUSTED;
case OT_ERROR_PARSE:
@@ -1088,7 +1264,7 @@
// The otDaemon.join() will leave first if this device is currently attached
getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.join failed", e);
+ LOG.e("otDaemon.join failed", e);
receiver.onError(e);
}
}
@@ -1113,25 +1289,29 @@
.scheduleMigration(
pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.scheduleMigration failed", e);
+ LOG.e("otDaemon.scheduleMigration failed", e);
receiver.onError(e);
}
}
@Override
public void leave(@NonNull IOperationReceiver receiver) {
- enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
+ leave(true /* eraseDataset */, receiver);
}
- private void leaveInternal(@NonNull OperationReceiverWrapper receiver) {
+ private void leave(boolean eraseDataset, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(() -> leaveInternal(eraseDataset, new OperationReceiverWrapper(receiver)));
+ }
+
+ private void leaveInternal(boolean eraseDataset, @NonNull OperationReceiverWrapper receiver) {
checkOnHandlerThread();
try {
- getOtDaemon().leave(newOtStatusReceiver(receiver));
+ getOtDaemon().leave(eraseDataset, newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.leave failed", e);
+ LOG.e("otDaemon.leave failed", e);
receiver.onError(e);
}
}
@@ -1164,7 +1344,7 @@
try {
getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.setCountryCode failed", e);
+ LOG.e("otDaemon.setCountryCode failed", e);
receiver.onError(e);
}
}
@@ -1174,7 +1354,7 @@
@Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED, NETWORK_SETTINGS);
- Log.i(TAG, "setTestNetworkAsUpstream: " + testNetworkInterfaceName);
+ LOG.i("setTestNetworkAsUpstream: " + testNetworkInterfaceName);
mHandler.post(() -> setTestNetworkAsUpstreamInternal(testNetworkInterfaceName, receiver));
}
@@ -1220,63 +1400,94 @@
try {
getOtDaemon().setChannelMaxPowers(channelMaxPowers, newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.setChannelMaxPowers failed", e);
+ LOG.e("otDaemon.setChannelMaxPowers failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
}
}
- private void enableBorderRouting(String infraIfName) {
- if (mBorderRouterConfig.isBorderRoutingEnabled
- && infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
+ private void setInfraLinkState(InfraLinkState newInfraLinkState) {
+ if (Objects.equals(mInfraLinkState, newInfraLinkState)) {
return;
}
- Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
- try {
- mBorderRouterConfig.infraInterfaceName = infraIfName;
- mBorderRouterConfig.infraInterfaceIcmp6Socket =
- mInfraIfController.createIcmp6Socket(infraIfName);
- mBorderRouterConfig.isBorderRoutingEnabled = true;
+ LOG.i("Infra link state changed: " + mInfraLinkState + " -> " + newInfraLinkState);
+ setInfraLinkInterfaceName(newInfraLinkState.interfaceName);
+ setInfraLinkNat64Prefix(newInfraLinkState.nat64Prefix);
+ setInfraLinkDnsServers(newInfraLinkState.dnsServers);
+ mInfraLinkState = newInfraLinkState;
+ }
+ private void setInfraLinkInterfaceName(String newInfraLinkInterfaceName) {
+ if (Objects.equals(mInfraLinkState.interfaceName, newInfraLinkInterfaceName)) {
+ return;
+ }
+ ParcelFileDescriptor infraIcmp6Socket = null;
+ if (newInfraLinkInterfaceName != null) {
+ try {
+ infraIcmp6Socket = mInfraIfController.createIcmp6Socket(newInfraLinkInterfaceName);
+ } catch (IOException e) {
+ LOG.e("Failed to create ICMPv6 socket on infra network interface", e);
+ }
+ }
+ try {
getOtDaemon()
- .configureBorderRouter(
- mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
- } catch (RemoteException | IOException | ThreadNetworkException e) {
- Log.w(TAG, "Failed to enable border routing", e);
+ .setInfraLinkInterfaceName(
+ newInfraLinkInterfaceName,
+ infraIcmp6Socket,
+ new LoggingOtStatusReceiver("setInfraLinkInterfaceName"));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("Failed to set infra link interface name " + newInfraLinkInterfaceName, e);
+ }
+ }
+
+ private void setInfraLinkNat64Prefix(@Nullable String newNat64Prefix) {
+ if (Objects.equals(newNat64Prefix, mInfraLinkState.nat64Prefix)) {
+ return;
+ }
+ try {
+ getOtDaemon()
+ .setInfraLinkNat64Prefix(
+ newNat64Prefix, new LoggingOtStatusReceiver("setInfraLinkNat64Prefix"));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("Failed to set infra link NAT64 prefix " + newNat64Prefix, e);
+ }
+ }
+
+ private void setInfraLinkDnsServers(List<String> newDnsServers) {
+ if (Objects.equals(newDnsServers, mInfraLinkState.dnsServers)) {
+ return;
+ }
+ try {
+ getOtDaemon()
+ .setInfraLinkDnsServers(
+ newDnsServers, new LoggingOtStatusReceiver("setInfraLinkDnsServers"));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("Failed to set infra link DNS servers " + newDnsServers, e);
}
}
private void disableBorderRouting() {
- mUpstreamNetwork = null;
- mBorderRouterConfig.infraInterfaceName = null;
- mBorderRouterConfig.infraInterfaceIcmp6Socket = null;
- mBorderRouterConfig.isBorderRoutingEnabled = false;
- try {
- getOtDaemon()
- .configureBorderRouter(
- mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
- } catch (RemoteException | ThreadNetworkException e) {
- Log.w(TAG, "Failed to disable border routing", e);
- }
+ LOG.i("Disabling border routing");
+ setInfraLinkState(newInfraLinkStateBuilder().build());
}
private void handleThreadInterfaceStateChanged(boolean isUp) {
try {
mTunIfController.setInterfaceUp(isUp);
- Log.i(TAG, "Thread TUN interface becomes " + (isUp ? "up" : "down"));
+ LOG.i("Thread TUN interface becomes " + (isUp ? "up" : "down"));
} catch (IOException e) {
- Log.e(TAG, "Failed to handle Thread interface state changes", e);
+ LOG.e("Failed to handle Thread interface state changes", e);
}
}
private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
if (ThreadNetworkController.isAttached(deviceRole)) {
- Log.i(TAG, "Attached to the Thread network");
+ LOG.i("Attached to the Thread network");
// This is an idempotent method which can be called for multiple times when the device
// is already attached (e.g. going from Child to Router)
registerThreadNetwork();
} else {
- Log.i(TAG, "Detached from the Thread network");
+ LOG.i("Detached from the Thread network");
// This is an idempotent method which can be called for multiple times when the device
// is already detached or stopped
@@ -1291,9 +1502,7 @@
// The OT daemon can send link property updates before the networkAgent is
// registered
- if (mNetworkAgent != null) {
- mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
- }
+ maybeSendLinkProperties();
}
private void handlePrefixChanged(List<OnMeshPrefixConfig> onMeshPrefixConfigList) {
@@ -1303,8 +1512,42 @@
// The OT daemon can send link property updates before the networkAgent is
// registered
- if (mNetworkAgent != null) {
- mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
+ maybeSendLinkProperties();
+ }
+
+ private void maybeSendLinkProperties() {
+ if (mNetworkAgent == null) {
+ return;
+ }
+ mNetworkAgent.sendLinkProperties(getTunIfLinkProperties());
+ }
+
+ private LinkProperties getTunIfLinkProperties() {
+ return mTunIfController.getLinkPropertiesWithNat64Cidr(mNat64CidrController.mNat64Cidr);
+ }
+
+ @RequiresPermission(
+ allOf = {PERMISSION_THREAD_NETWORK_PRIVILEGED, PERMISSION_THREAD_NETWORK_TESTING})
+ public void runOtCtlCommand(
+ @NonNull String command, boolean isInteractive, @NonNull IOutputReceiver receiver) {
+ enforceAllPermissionsGranted(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED, PERMISSION_THREAD_NETWORK_TESTING);
+
+ mHandler.post(
+ () ->
+ runOtCtlCommandInternal(
+ command, isInteractive, new OutputReceiverWrapper(receiver)));
+ }
+
+ private void runOtCtlCommandInternal(
+ String command, boolean isInteractive, @NonNull OutputReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().runOtCtlCommand(command, isInteractive, newOtOutputReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("otDaemon.runOtCtlCommand failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
}
}
@@ -1314,7 +1557,7 @@
}
final LocalNetworkConfig localNetworkConfig = newLocalNetworkConfig();
mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
- Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
+ LOG.v("Sent localNetworkConfig: " + localNetworkConfig);
}
private void handleMulticastForwardingChanged(BackboneRouterState state) {
@@ -1357,6 +1600,34 @@
return builder.build();
}
+ private static InfraLinkState.Builder newInfraLinkStateBuilder() {
+ return new InfraLinkState.Builder().setInterfaceName("");
+ }
+
+ private static InfraLinkState.Builder newInfraLinkStateBuilder(
+ @Nullable LinkProperties linkProperties) {
+ if (linkProperties == null) {
+ return newInfraLinkStateBuilder();
+ }
+ String nat64Prefix = null;
+ if (linkProperties.getNat64Prefix() != null) {
+ nat64Prefix = linkProperties.getNat64Prefix().toString();
+ }
+ return new InfraLinkState.Builder()
+ .setInterfaceName(linkProperties.getInterfaceName())
+ .setNat64Prefix(nat64Prefix)
+ .setDnsServers(addressesToStrings(linkProperties.getDnsServers()));
+ }
+
+ private static List<String> addressesToStrings(List<InetAddress> addresses) {
+ List<String> strings = new ArrayList<>();
+
+ for (InetAddress address : addresses) {
+ strings.add(address.getHostAddress());
+ }
+ return strings;
+ }
+
private static final class CallbackMetadata {
private static long gId = 0;
@@ -1365,9 +1636,13 @@
final IBinder.DeathRecipient deathRecipient;
- CallbackMetadata(IBinder.DeathRecipient deathRecipient) {
+ final boolean hasThreadPrivilegedPermission;
+
+ CallbackMetadata(
+ IBinder.DeathRecipient deathRecipient, boolean hasThreadPrivilegedPermission) {
this.id = allocId();
this.deathRecipient = deathRecipient;
+ this.hasThreadPrivilegedPermission = hasThreadPrivilegedPermission;
}
private static long allocId() {
@@ -1378,17 +1653,40 @@
}
}
- private static final class ConfigureBorderRouterStatusReceiver extends IOtStatusReceiver.Stub {
- public ConfigureBorderRouterStatusReceiver() {}
+ /** An implementation of {@link IOperationReceiver} that simply logs the operation result. */
+ private static class LoggingOperationReceiver extends IOperationReceiver.Stub {
+ private final String mOperation;
+
+ LoggingOperationReceiver(String operation) {
+ mOperation = operation;
+ }
@Override
public void onSuccess() {
- Log.i(TAG, "Configured border router successfully");
+ LOG.i("The operation " + mOperation + " succeeded");
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ LOG.w("The operation " + mOperation + " failed: " + errorCode + " " + errorMessage);
+ }
+ }
+
+ private static class LoggingOtStatusReceiver extends IOtStatusReceiver.Stub {
+ private final String mAction;
+
+ LoggingOtStatusReceiver(String action) {
+ mAction = action;
+ }
+
+ @Override
+ public void onSuccess() {
+ LOG.i("The action " + mAction + " succeeded");
}
@Override
public void onError(int i, String s) {
- Log.w(TAG, String.format("Failed to configure border router: %d %s", i, s));
+ LOG.w("The action " + mAction + " failed: " + i + " " + s);
}
}
@@ -1405,7 +1703,8 @@
private ActiveOperationalDataset mActiveDataset;
private PendingOperationalDataset mPendingDataset;
- public void registerStateCallback(IStateCallback callback) {
+ public void registerStateCallback(
+ IStateCallback callback, boolean hasThreadPrivilegedPermission) {
checkOnHandlerThread();
if (mStateCallbacks.containsKey(callback)) {
throw new IllegalStateException("Registering the same IStateCallback twice");
@@ -1413,7 +1712,8 @@
IBinder.DeathRecipient deathRecipient =
() -> mHandler.post(() -> unregisterStateCallback(callback));
- CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+ CallbackMetadata callbackMetadata =
+ new CallbackMetadata(deathRecipient, hasThreadPrivilegedPermission);
mStateCallbacks.put(callback, callbackMetadata);
try {
callback.asBinder().linkToDeath(deathRecipient, 0);
@@ -1425,7 +1725,7 @@
try {
getOtDaemon().registerStateCallback(this, callbackMetadata.id);
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.registerStateCallback failed", e);
+ LOG.e("otDaemon.registerStateCallback failed", e);
}
}
@@ -1446,7 +1746,8 @@
IBinder.DeathRecipient deathRecipient =
() -> mHandler.post(() -> unregisterDatasetCallback(callback));
- CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+ CallbackMetadata callbackMetadata =
+ new CallbackMetadata(deathRecipient, true /* hasThreadPrivilegedPermission */);
mOpDatasetCallbacks.put(callback, callbackMetadata);
try {
callback.asBinder().linkToDeath(deathRecipient, 0);
@@ -1457,7 +1758,7 @@
try {
getOtDaemon().registerStateCallback(this, callbackMetadata.id);
} catch (RemoteException | ThreadNetworkException e) {
- Log.e(TAG, "otDaemon.registerStateCallback failed", e);
+ LOG.e("otDaemon.registerStateCallback failed", e);
}
}
@@ -1493,6 +1794,7 @@
// do nothing if the client is dead
}
}
+ mInfraLinkState = newInfraLinkStateBuilder().build();
}
private void onThreadEnabledChanged(int state, long listenerId) {
@@ -1525,16 +1827,18 @@
}
@Override
- public void onStateChanged(OtDaemonState newState, long listenerId) {
+ public void onStateChanged(@NonNull OtDaemonState newState, long listenerId) {
mHandler.post(() -> onStateChangedInternal(newState, listenerId));
}
private void onStateChangedInternal(OtDaemonState newState, long listenerId) {
checkOnHandlerThread();
+
onInterfaceStateChanged(newState.isInterfaceUp);
onDeviceRoleChanged(newState.deviceRole, listenerId);
onPartitionIdChanged(newState.partitionId, listenerId);
onThreadEnabledChanged(newState.threadEnabled, listenerId);
+ onEphemeralKeyStateChanged(newState, listenerId);
mState = newState;
ActiveOperationalDataset newActiveDataset;
@@ -1549,7 +1853,7 @@
mActiveDataset = newActiveDataset;
} catch (IllegalArgumentException e) {
// Is unlikely that OT will generate invalid Operational Dataset
- Log.wtf(TAG, "Invalid Active Operational Dataset from OpenThread", e);
+ LOG.wtf("Invalid Active Operational Dataset from OpenThread", e);
}
PendingOperationalDataset newPendingDataset;
@@ -1564,7 +1868,7 @@
mPendingDataset = newPendingDataset;
} catch (IllegalArgumentException e) {
// Is unlikely that OT will generate invalid Operational Dataset
- Log.wtf(TAG, "Invalid Pending Operational Dataset from OpenThread", e);
+ LOG.wtf("Invalid Pending Operational Dataset from OpenThread", e);
}
}
@@ -1610,6 +1914,43 @@
}
}
+ private void onEphemeralKeyStateChanged(OtDaemonState newState, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = isEphemeralKeyStateChanged(mState, newState);
+
+ for (var callbackEntry : mStateCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ String passcode =
+ callbackEntry.getValue().hasThreadPrivilegedPermission
+ ? newState.ephemeralKeyPasscode
+ : null;
+ if (newState.ephemeralKeyState == EPHEMERAL_KEY_DISABLED) {
+ passcode = null;
+ }
+ try {
+ callbackEntry
+ .getKey()
+ .onEphemeralKeyStateChanged(
+ newState.ephemeralKeyState,
+ passcode,
+ newState.ephemeralKeyLifetimeMillis);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ private static boolean isEphemeralKeyStateChanged(
+ OtDaemonState oldState, @NonNull OtDaemonState newState) {
+ if (oldState == null) return true;
+ if (oldState.ephemeralKeyState != newState.ephemeralKeyState) return true;
+ if (oldState.ephemeralKeyState == EPHEMERAL_KEY_DISABLED) return false;
+ return (!Objects.equals(oldState.ephemeralKeyPasscode, newState.ephemeralKeyPasscode)
+ || oldState.ephemeralKeyLifetimeMillis != newState.ephemeralKeyLifetimeMillis);
+ }
+
private void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeDataset, long listenerId) {
checkOnHandlerThread();
@@ -1658,4 +1999,64 @@
mHandler.post(() -> handlePrefixChanged(onMeshPrefixConfigList));
}
}
+
+ private final class Nat64CidrController extends IIpv4PrefixRequest.Stub {
+ private static final int RETRY_DELAY_ON_FAILURE_MILLIS = 600_000; // 10 minutes
+
+ @Nullable private LinkAddress mNat64Cidr;
+
+ @Override
+ public void onIpv4PrefixConflict(IpPrefix prefix) {
+ mHandler.post(() -> onIpv4PrefixConflictInternal(prefix));
+ }
+
+ private void onIpv4PrefixConflictInternal(IpPrefix prefix) {
+ checkOnHandlerThread();
+
+ LOG.i("Conflict on NAT64 CIDR: " + prefix);
+ maybeReleaseNat64Cidr();
+ maybeUpdateNat64Cidr();
+ }
+
+ public void maybeUpdateNat64Cidr() {
+ checkOnHandlerThread();
+
+ if (mPersistentSettings.getConfiguration().isNat64Enabled()) {
+ maybeRequestNat64Cidr();
+ } else {
+ maybeReleaseNat64Cidr();
+ }
+ try {
+ getOtDaemon()
+ .setNat64Cidr(
+ mNat64Cidr == null ? null : mNat64Cidr.toString(),
+ new LoggingOtStatusReceiver("setNat64Cidr"));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("Failed to set NAT64 CIDR at otd-daemon", e);
+ }
+ maybeSendLinkProperties();
+ }
+
+ private void maybeRequestNat64Cidr() {
+ if (mNat64Cidr != null) {
+ return;
+ }
+ final LinkAddress downstreamAddress =
+ mRoutingCoordinatorManager.requestDownstreamAddress(this);
+ if (downstreamAddress == null) {
+ mHandler.postDelayed(() -> maybeUpdateNat64Cidr(), RETRY_DELAY_ON_FAILURE_MILLIS);
+ }
+ mNat64Cidr = downstreamAddress;
+ LOG.i("Allocated NAT64 CIDR: " + mNat64Cidr);
+ }
+
+ private void maybeReleaseNat64Cidr() {
+ if (mNat64Cidr == null) {
+ return;
+ }
+ LOG.i("Released NAT64 CIDR: " + mNat64Cidr);
+ mNat64Cidr = null;
+ mRoutingCoordinatorManager.releaseDownstream(this);
+ }
+ }
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
index a194114..2cd34e8 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -38,10 +38,10 @@
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
-import android.util.Log;
import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.ConnectivityResources;
import java.io.FileDescriptor;
@@ -63,7 +63,9 @@
*/
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class ThreadNetworkCountryCode {
- private static final String TAG = "ThreadNetworkCountryCode";
+ private static final String TAG = "CountryCode";
+ private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
+
// To be used when there is no country code available.
@VisibleForTesting public static final String DEFAULT_COUNTRY_CODE = "WW";
@@ -280,11 +282,11 @@
String countryCode = addresses.get(0).getCountryCode();
if (isValidCountryCode(countryCode)) {
- Log.d(TAG, "Set location country code to: " + countryCode);
+ LOG.v("Set location country code to: " + countryCode);
mLocationCountryCodeInfo =
new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_LOCATION);
} else {
- Log.d(TAG, "Received invalid location country code");
+ LOG.v("Received invalid location country code");
mLocationCountryCodeInfo = null;
}
@@ -296,8 +298,7 @@
if ((location == null) || (mGeocoder == null)) return;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
- Log.wtf(
- TAG,
+ LOG.wtf(
"Unexpected call to set country code from the Geocoding location, "
+ "Thread code never runs under T or lower.");
return;
@@ -320,13 +321,13 @@
private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback {
@Override
public void onActiveCountryCodeChanged(String countryCode) {
- Log.d(TAG, "Wifi country code is changed to " + countryCode);
+ LOG.v("Wifi country code is changed to " + countryCode);
synchronized ("ThreadNetworkCountryCode.this") {
if (isValidCountryCode(countryCode)) {
mWifiCountryCodeInfo =
new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_WIFI);
} else {
- Log.w(TAG, "WiFi country code " + countryCode + " is invalid");
+ LOG.w("WiFi country code " + countryCode + " is invalid");
mWifiCountryCodeInfo = null;
}
@@ -336,7 +337,7 @@
@Override
public void onCountryCodeInactive() {
- Log.d(TAG, "Wifi country code is inactived");
+ LOG.v("Wifi country code is inactived");
synchronized ("ThreadNetworkCountryCode.this") {
mWifiCountryCodeInfo = null;
updateCountryCode(false /* forceUpdate */);
@@ -346,8 +347,7 @@
private synchronized void registerTelephonyCountryCodeCallback() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
- Log.wtf(
- TAG,
+ LOG.wtf(
"Unexpected call to register the telephony country code changed callback, "
+ "Thread code never runs under T or lower.");
return;
@@ -387,7 +387,7 @@
mSubscriptionManager.getActiveSubscriptionInfoList();
if (subscriptionInfoList == null) {
- Log.d(TAG, "No SIM card is found");
+ LOG.v("No SIM card is found");
return;
}
@@ -399,11 +399,11 @@
try {
countryCode = mTelephonyManager.getNetworkCountryIso(slotIndex);
} catch (IllegalArgumentException e) {
- Log.e(TAG, "Failed to get country code for slot index:" + slotIndex, e);
+ LOG.e("Failed to get country code for slot index:" + slotIndex, e);
continue;
}
- Log.d(TAG, "Telephony slot " + slotIndex + " country code is " + countryCode);
+ LOG.v("Telephony slot " + slotIndex + " country code is " + countryCode);
setTelephonyCountryCodeAndLastKnownCountryCode(
slotIndex, countryCode, null /* lastKnownCountryCode */);
}
@@ -411,8 +411,7 @@
private synchronized void setTelephonyCountryCodeAndLastKnownCountryCode(
int slotIndex, String countryCode, String lastKnownCountryCode) {
- Log.d(
- TAG,
+ LOG.v(
"Set telephony country code to: "
+ countryCode
+ ", last country code to: "
@@ -522,8 +521,7 @@
@Override
public void onError(int otError, String message) {
- Log.e(
- TAG,
+ LOG.e(
"Error "
+ otError
+ ": "
@@ -545,11 +543,11 @@
CountryCodeInfo countryCodeInfo = pickCountryCode();
if (!forceUpdate && countryCodeInfo.isCountryCodeMatch(mCurrentCountryCodeInfo)) {
- Log.i(TAG, "Ignoring already set country code " + countryCodeInfo.getCountryCode());
+ LOG.i("Ignoring already set country code " + countryCodeInfo.getCountryCode());
return;
}
- Log.i(TAG, "Set country code: " + countryCodeInfo);
+ LOG.i("Set country code: " + countryCodeInfo);
mThreadNetworkControllerService.setCountryCode(
countryCodeInfo.getCountryCode().toUpperCase(Locale.ROOT),
newOperationReceiver(countryCodeInfo));
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkLogger.java b/thread/service/java/com/android/server/thread/ThreadNetworkLogger.java
new file mode 100644
index 0000000..a765304
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkLogger.java
@@ -0,0 +1,37 @@
+/*
+ * 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.thread;
+
+import com.android.net.module.util.SharedLog;
+
+/**
+ * The Logger for Thread network.
+ *
+ * <p>Each class should log with its own tag using the logger of
+ * ThreadNetworkLogger.forSubComponent(TAG).
+ */
+public final class ThreadNetworkLogger {
+ private static final String TAG = "ThreadNetwork";
+ private static final SharedLog mLog = new SharedLog(TAG);
+
+ public static SharedLog forSubComponent(String subComponent) {
+ return mLog.forSubComponent(subComponent);
+ }
+
+ // Disable instantiation
+ private ThreadNetworkLogger() {}
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
index 54155ee..18ab1ca 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -19,10 +19,15 @@
import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IConfigurationReceiver;
import android.net.thread.IOperationReceiver;
+import android.net.thread.IOutputReceiver;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkException;
+import android.os.Binder;
+import android.os.Process;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
@@ -52,6 +57,8 @@
private static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
private static final Duration MIGRATE_TIMEOUT = Duration.ofSeconds(2);
private static final Duration FORCE_STOP_TIMEOUT = Duration.ofSeconds(1);
+ private static final Duration OT_CTL_COMMAND_TIMEOUT = Duration.ofSeconds(5);
+ private static final Duration CONFIG_TIMEOUT = Duration.ofSeconds(1);
private static final String PERMISSION_THREAD_NETWORK_TESTING =
"android.permission.THREAD_NETWORK_TESTING";
@@ -62,7 +69,8 @@
@Nullable private PrintWriter mOutputWriter;
@Nullable private PrintWriter mErrorWriter;
- public ThreadNetworkShellCommand(
+ @VisibleForTesting
+ ThreadNetworkShellCommand(
Context context,
ThreadNetworkControllerService controllerService,
ThreadNetworkCountryCode countryCode) {
@@ -77,6 +85,10 @@
mErrorWriter = errorWriter;
}
+ private static boolean isRootProcess() {
+ return Binder.getCallingUid() == Process.ROOT_UID;
+ }
+
private PrintWriter getOutputWriter() {
return (mOutputWriter != null) ? mOutputWriter : getOutPrintWriter();
}
@@ -107,6 +119,10 @@
pw.println(" Gets country code as a two-letter string");
pw.println(" force-country-code enabled <two-letter code> | disabled ");
pw.println(" Sets country code to <two-letter code> or left for normal value");
+ pw.println(" ot-ctl <subcommand>");
+ pw.println(" Runs ot-ctl command");
+ pw.println(" config [name] [value]");
+ pw.println(" Gets the config or sets the value for a config entry");
}
@Override
@@ -121,6 +137,8 @@
return setThreadEnabled(true);
case "disable":
return setThreadEnabled(false);
+ case "config":
+ return handleConfigCommand();
case "join":
return join();
case "leave":
@@ -133,6 +151,8 @@
return forceCountryCode();
case "get-country-code":
return getCountryCode();
+ case "ot-ctl":
+ return handleOtCtlCommand();
default:
return handleDefaultCommands(cmd);
}
@@ -248,6 +268,113 @@
return 0;
}
+ private int handleConfigCommand() {
+ ensureTestingPermission();
+
+ // Get config
+ if (peekNextArg() == null) {
+ try {
+ final ThreadConfiguration config = getConfig();
+ getOutputWriter().println("Thread configuration = " + config);
+ } catch (AssertionError e) {
+ getErrorWriter().println("Failed: " + e.getMessage());
+ return -1;
+ }
+ return 0;
+ }
+
+ // Set config
+ final String name = getNextArg();
+ final String value = getNextArg();
+ try {
+ setConfig(name, value);
+ } catch (AssertionError | IllegalArgumentException e) {
+ getErrorWriter().println(e.getMessage());
+ return -1;
+ }
+ return 0;
+ }
+
+ private ThreadConfiguration getConfig() throws AssertionError {
+ final CompletableFuture<ThreadConfiguration> future = new CompletableFuture<>();
+ mControllerService.registerConfigurationCallback(
+ new IConfigurationReceiver.Stub() {
+ @Override
+ public void onConfigurationChanged(ThreadConfiguration config) {
+ future.complete(config);
+ }
+ });
+ try {
+ return future.get(CONFIG_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ throw new AssertionError("Failed to get config within timeout", e);
+ }
+ }
+
+ private void setConfig(String name, String value)
+ throws IllegalArgumentException, AssertionError {
+ if (name == null || value == null) {
+ throw new IllegalArgumentException(
+ "Invalid config name = " + name + ", value=" + value);
+ }
+ final ThreadConfiguration oldConfig = getConfig();
+ final ThreadConfiguration.Builder newConfigBuilder =
+ new ThreadConfiguration.Builder(oldConfig);
+ switch (name) {
+ case "br" -> newConfigBuilder.setBorderRouterEnabled(argEnabledOrDisabled(value));
+ case "nat64" -> newConfigBuilder.setNat64Enabled(argEnabledOrDisabled(value));
+ case "pd" -> newConfigBuilder.setDhcpv6PdEnabled(argEnabledOrDisabled(value));
+ default -> throw new IllegalArgumentException("Invalid config name: " + name);
+ }
+ CompletableFuture<Void> future = new CompletableFuture();
+ mControllerService.setConfiguration(newConfigBuilder.build(), newOperationReceiver(future));
+ waitForFuture(future, CONFIG_TIMEOUT, mErrorWriter);
+ }
+
+ private static final class OutputReceiver extends IOutputReceiver.Stub {
+ private final CompletableFuture<Void> future;
+ private final PrintWriter outputWriter;
+
+ public OutputReceiver(CompletableFuture<Void> future, PrintWriter outputWriter) {
+ this.future = future;
+ this.outputWriter = outputWriter;
+ }
+
+ @Override
+ public void onOutput(String output) {
+ outputWriter.print(output);
+ outputWriter.flush();
+ }
+
+ @Override
+ public void onComplete() {
+ future.complete(null);
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
+ }
+ }
+
+ private int handleOtCtlCommand() {
+ ensureTestingPermission();
+
+ if (!isRootProcess()) {
+ getErrorWriter().println("No access to ot-ctl command");
+ return -1;
+ }
+
+ final String subCommand = String.join(" ", peekRemainingArgs());
+
+ CompletableFuture<Void> completeFuture = new CompletableFuture<>();
+ mControllerService.runOtCtlCommand(
+ subCommand,
+ false /* isInteractive */,
+ new OutputReceiver(completeFuture, getOutputWriter()));
+ return waitForFuture(completeFuture, OT_CTL_COMMAND_TIMEOUT, getErrorWriter());
+ }
+
private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
return new IOperationReceiver.Stub() {
@Override
@@ -302,6 +429,10 @@
}
}
+ private static boolean argEnabledOrDisabled(String arg) {
+ return argTrueOrFalse(arg, "enabled", "disabled");
+ }
+
private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString) {
String nextArg = getNextArgRequired();
return argTrueOrFalse(nextArg, trueString, falseString);
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index 747cc96..746b587 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -25,11 +25,11 @@
import android.net.thread.ThreadConfiguration;
import android.os.PersistableBundle;
import android.util.AtomicFile;
-import android.util.Log;
import com.android.connectivity.resources.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.ConnectivityResources;
import java.io.ByteArrayInputStream;
@@ -48,6 +48,7 @@
*/
public class ThreadPersistentSettings {
private static final String TAG = "ThreadPersistentSettings";
+ private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
/** File name used for storing settings. */
private static final String FILE_NAME = "ThreadPersistentSettings.xml";
@@ -76,6 +77,13 @@
/** 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);
+ /**
+ * Saves the boolean flag for border router being enabled. The value defaults to {@code true} if
+ * this config is missing.
+ */
+ private static final Key<Boolean> CONFIG_BORDER_ROUTER_ENABLED =
+ new Key<>("config_border_router_enabled", true);
+
/** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */
private static final Key<Boolean> CONFIG_NAT64_ENABLED =
new Key<>("config_nat64_enabled", false);
@@ -115,7 +123,7 @@
readFromStoreFile();
synchronized (mLock) {
if (!mSettings.containsKey(THREAD_ENABLED.key)) {
- Log.i(TAG, "\"thread_enabled\" is missing in settings file, using default value");
+ LOG.i("\"thread_enabled\" is missing in settings file, using default value");
put(
THREAD_ENABLED.key,
mResources.get().getBoolean(R.bool.config_thread_default_enabled));
@@ -196,8 +204,9 @@
if (getConfiguration().equals(configuration)) {
return false;
}
+ putObject(CONFIG_BORDER_ROUTER_ENABLED.key, configuration.isBorderRouterEnabled());
putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled());
- putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcp6PdEnabled());
+ putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcpv6PdEnabled());
writeToStoreFile();
return true;
}
@@ -205,8 +214,9 @@
/** Retrieve the {@link ThreadConfiguration} from the persistent settings. */
public ThreadConfiguration getConfiguration() {
return new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(get(CONFIG_BORDER_ROUTER_ENABLED))
.setNat64Enabled(get(CONFIG_NAT64_ENABLED))
- .setDhcp6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
+ .setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
.build();
}
@@ -243,7 +253,7 @@
writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
}
} catch (IOException e) {
- Log.wtf(TAG, "Write to store file failed", e);
+ LOG.wtf("Write to store file failed", e);
}
}
@@ -251,7 +261,7 @@
try {
final byte[] readData;
synchronized (mLock) {
- Log.i(TAG, "Reading from store file: " + mAtomicFile.getBaseFile());
+ LOG.i("Reading from store file: " + mAtomicFile.getBaseFile());
readData = readFromAtomicFile(mAtomicFile);
}
final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
@@ -262,9 +272,9 @@
mSettings.putAll(bundleRead);
}
} catch (FileNotFoundException e) {
- Log.w(TAG, "No store file to read", e);
+ LOG.w("No store file to read " + e.getMessage());
} catch (IOException e) {
- Log.e(TAG, "Read from store file failed", e);
+ LOG.e("Read from store file failed", e);
}
}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 976f93d..520a434 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -38,10 +38,10 @@
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.Os;
-import android.util.Log;
import com.android.net.module.util.HexDump;
import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+import com.android.net.module.util.SharedLog;
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.StructIfinfoMsg;
import com.android.net.module.util.netlink.StructNlAttr;
@@ -66,6 +66,7 @@
public class TunInterfaceController {
private static final String TAG = "TunIfController";
private static final boolean DBG = false;
+ private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
private static final long INFINITE_LIFETIME = 0xffffffffL;
static final int MTU = 1280;
@@ -91,8 +92,19 @@
}
/** Returns link properties of the Thread TUN interface. */
- public LinkProperties getLinkProperties() {
- return mLinkProperties;
+ private LinkProperties getLinkProperties() {
+ return new LinkProperties(mLinkProperties);
+ }
+
+ /** Returns link properties of the Thread TUN interface with the given NAT64 CIDR. */
+ // TODO: manage the NAT64 CIDR in the TunInterfaceController
+ public LinkProperties getLinkPropertiesWithNat64Cidr(@Nullable LinkAddress nat64Cidr) {
+ final LinkProperties lp = getLinkProperties();
+ if (nat64Cidr != null) {
+ lp.addLinkAddress(nat64Cidr);
+ lp.addRoute(getRouteForAddress(nat64Cidr));
+ }
+ return lp;
}
/**
@@ -147,7 +159,10 @@
/** Adds a new address to the interface. */
public void addAddress(LinkAddress address) {
- Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
+ if (!(address.getAddress() instanceof Inet6Address)) {
+ return;
+ }
+ LOG.v("Adding address " + address + " with flags: " + address.getFlags());
long preferredLifetimeSeconds;
long validLifetimeSeconds;
@@ -171,7 +186,7 @@
(address.getExpirationTime() - SystemClock.elapsedRealtime()) / 1000L,
0L);
}
-
+ // Only apply to Ipv6 address
if (!NetlinkUtils.sendRtmNewAddressRequest(
Os.if_nametoindex(mIfName),
address.getAddress(),
@@ -180,7 +195,7 @@
(byte) address.getScope(),
preferredLifetimeSeconds,
validLifetimeSeconds)) {
- Log.w(TAG, "Failed to add address " + address.getAddress().getHostAddress());
+ LOG.w("Failed to add address " + address.getAddress().getHostAddress());
return;
}
mLinkProperties.addLinkAddress(address);
@@ -189,18 +204,22 @@
/** Removes an address from the interface. */
public void removeAddress(LinkAddress address) {
- Log.d(TAG, "Removing address " + address);
+ if (!(address.getAddress() instanceof Inet6Address)) {
+ return;
+ }
+ LOG.v("Removing address " + address);
// Intentionally update the mLinkProperties before send netlink message because the
// address is already removed from ot-daemon and apps can't reach to the address even
// when the netlink request below fails
mLinkProperties.removeLinkAddress(address);
mLinkProperties.removeRoute(getRouteForAddress(address));
+ // Only apply to Ipv6 address
if (!NetlinkUtils.sendRtmDelAddressRequest(
Os.if_nametoindex(mIfName),
(Inet6Address) address.getAddress(),
(short) address.getPrefixLength())) {
- Log.w(TAG, "Failed to remove address " + address.getAddress().getHostAddress());
+ LOG.w("Failed to remove address " + address.getAddress().getHostAddress());
}
}
@@ -287,7 +306,7 @@
try {
setInterfaceUp(false);
} catch (IOException e) {
- Log.e(TAG, "Failed to set Thread TUN interface down");
+ LOG.e("Failed to set Thread TUN interface down");
}
}
@@ -347,11 +366,15 @@
if (e.getCause() instanceof ErrnoException) {
ErrnoException ee = (ErrnoException) e.getCause();
if (ee.errno == EADDRINUSE) {
- Log.w(TAG, "Already joined group" + address.getHostAddress(), e);
+ LOG.w(
+ "Already joined group "
+ + address.getHostAddress()
+ + ": "
+ + e.getMessage());
return;
}
}
- Log.e(TAG, "failed to join group " + address.getHostAddress(), e);
+ LOG.e("failed to join group " + address.getHostAddress(), e);
}
}
@@ -360,7 +383,7 @@
try {
mMulticastSocket.leaveGroup(socketAddress, mNetworkInterface);
} catch (IOException e) {
- Log.e(TAG, "failed to leave group " + address.getHostAddress(), e);
+ LOG.e("failed to leave group " + address.getHostAddress(), e);
}
}
@@ -415,14 +438,14 @@
}
if (DBG) {
- Log.d(TAG, "ADDR_GEN_MODE message is:");
- Log.d(TAG, HexDump.dumpHexString(msg));
+ LOG.v("ADDR_GEN_MODE message is:");
+ LOG.v(HexDump.dumpHexString(msg));
}
try {
NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
} catch (ErrnoException e) {
- Log.e(TAG, "Failed to set ADDR_GEN_MODE to NONE", e);
+ LOG.e("Failed to set ADDR_GEN_MODE to NONE", e);
}
}
}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index c1cf0a0..2630d21 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -21,9 +21,11 @@
android_test {
name: "CtsThreadNetworkTestCases",
- defaults: ["cts_defaults"],
+ defaults: [
+ "cts_defaults",
+ "framework-connectivity-test-defaults",
+ ],
min_sdk_version: "33",
- sdk_version: "test_current",
manifest: "AndroidManifest.xml",
test_config: "AndroidTest.xml",
srcs: [
@@ -38,6 +40,7 @@
static_libs: [
"androidx.test.ext.junit",
"compatibility-device-util-axt",
+ "com.android.net.thread.flags-aconfig-java",
"ctstestrunner-axt",
"guava",
"guava-android-testlib",
@@ -46,8 +49,8 @@
"truth",
],
libs: [
- "android.test.base",
- "android.test.runner",
+ "android.test.base.stubs",
+ "android.test.runner.stubs",
"framework-connectivity-module-api-stubs-including-flagged",
],
// Test coverage system runs on different devices. Need to
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index 34aabe2..e954d3b 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -56,4 +56,14 @@
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
</test>
+
+ <!--
+ This doesn't override a read-only flag, to run the tests locally with `epskc_enabled` flag
+ enabled, set the flag to `is_fixed_read_only: false`. This should be removed after the
+ `epskc_enabled` flag is rolled out.
+ -->
+ <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
+ <option name="flag-value"
+ value="thread_network/com.android.net.thread.flags.epskc_enabled=true"/>
+ </target_preparer>
</configuration>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
new file mode 100644
index 0000000..e2f0e47
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 android.net.thread.cts;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.thread.ThreadConfiguration;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Tests for {@link ThreadConfiguration}. */
+@SmallTest
+@RequiresThreadFeature
+@RunWith(Parameterized.class)
+public final class ThreadConfigurationTest {
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ public final boolean mIsBorderRouterEnabled;
+ public final boolean mIsNat64Enabled;
+ public final boolean mIsDhcpv6PdEnabled;
+
+ @Parameterized.Parameters
+ public static Collection configArguments() {
+ return Arrays.asList(
+ new Object[][] {
+ {false, false, false}, // All disabled
+ {false, true, false}, // NAT64 enabled
+ {false, false, true}, // DHCP6-PD enabled
+ {true, true, true}, // All enabled
+ });
+ }
+
+ public ThreadConfigurationTest(
+ boolean isBorderRouterEnabled, boolean isNat64Enabled, boolean isDhcpv6PdEnabled) {
+ mIsBorderRouterEnabled = isBorderRouterEnabled;
+ mIsNat64Enabled = isNat64Enabled;
+ mIsDhcpv6PdEnabled = isDhcpv6PdEnabled;
+ }
+
+ @Test
+ public void parcelable_parcelingIsLossLess() {
+ ThreadConfiguration config =
+ new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(mIsBorderRouterEnabled)
+ .setNat64Enabled(mIsNat64Enabled)
+ .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+ .build();
+ assertParcelingIsLossless(config);
+ }
+
+ @Test
+ public void builder_correctValuesAreSet() {
+ ThreadConfiguration config =
+ new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(mIsBorderRouterEnabled)
+ .setNat64Enabled(mIsNat64Enabled)
+ .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+ .build();
+
+ assertThat(config.isBorderRouterEnabled()).isEqualTo(mIsBorderRouterEnabled);
+ assertThat(config.isNat64Enabled()).isEqualTo(mIsNat64Enabled);
+ assertThat(config.isDhcpv6PdEnabled()).isEqualTo(mIsDhcpv6PdEnabled);
+ }
+
+ @Test
+ public void builderConstructor_configsAreEqual() {
+ ThreadConfiguration config1 =
+ new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(mIsBorderRouterEnabled)
+ .setNat64Enabled(mIsNat64Enabled)
+ .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+ .build();
+ ThreadConfiguration config2 = new ThreadConfiguration.Builder(config1).build();
+ assertThat(config1).isEqualTo(config2);
+ }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 22e7a98..2d487ca 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -27,14 +27,18 @@
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
+import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_DISABLED;
+import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_ENABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
+import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -45,6 +49,7 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
@@ -57,6 +62,7 @@
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
import android.net.thread.ThreadNetworkController.StateCallback;
@@ -68,14 +74,21 @@
import android.os.Build;
import android.os.HandlerThread;
import android.os.OutcomeReceiver;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import com.android.net.module.util.ArrayTrackRecord;
+import com.android.net.thread.flags.Flags;
import com.android.testutils.FunctionalUtils.ThrowingRunnable;
+import kotlin.Triple;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
@@ -90,13 +103,16 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
import java.util.function.Predicate;
/** CTS tests for {@link ThreadNetworkController}. */
@@ -109,14 +125,30 @@
private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
+ private static final int SET_CONFIGURATION_TIMEOUT_MILLIS = 1_000;
private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
private static final int SERVICE_LOST_TIMEOUT_MILLIS = 20_000;
+ private static final int VALID_POWER = 32_767;
+ private static final int VALID_CHANNEL = 20;
+ private static final int INVALID_CHANNEL = 10;
private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
private static final String THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
+ private static final ThreadConfiguration DEFAULT_CONFIG =
+ new ThreadConfiguration.Builder().build();
+ private static final SparseIntArray CHANNEL_MAX_POWERS =
+ new SparseIntArray() {
+ {
+ put(VALID_CHANNEL, VALID_POWER);
+ }
+ };
+ private static final Duration EPHEMERAL_KEY_LIFETIME = Duration.ofSeconds(1);
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private final Context mContext = ApplicationProvider.getApplicationContext();
private ExecutorService mExecutor;
private ThreadNetworkController mController;
@@ -126,6 +158,9 @@
private HandlerThread mHandlerThread;
private TapTestNetworkTracker mTestNetworkTracker;
+ private final List<Consumer<ThreadConfiguration>> mConfigurationCallbacksToCleanUp =
+ new ArrayList<>();
+
@Before
public void setUp() throws Exception {
mController =
@@ -140,13 +175,29 @@
mHandlerThread.start();
setEnabledAndWait(mController, true);
+ setConfigurationAndWait(mController, DEFAULT_CONFIG);
+ deactivateEphemeralKeyModeAndWait(mController);
}
@After
public void tearDown() throws Exception {
dropAllPermissions();
+ setEnabledAndWait(mController, true);
leaveAndWait(mController);
tearDownTestNetwork();
+ setConfigurationAndWait(mController, DEFAULT_CONFIG);
+ for (Consumer<ThreadConfiguration> configurationCallback :
+ mConfigurationCallbacksToCleanUp) {
+ try {
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.unregisterConfigurationCallback(configurationCallback));
+ } catch (IllegalArgumentException e) {
+ // Ignore the exception when the callback is not registered.
+ }
+ }
+ mConfigurationCallbacksToCleanUp.clear();
+ deactivateEphemeralKeyModeAndWait(mController);
}
@Test
@@ -783,6 +834,264 @@
listener.unregisterStateCallback();
}
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void getMaxEphemeralKeyLifetime_isLargerThanZero() {
+ assertThat(mController.getMaxEphemeralKeyLifetime()).isGreaterThan(Duration.ZERO);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withPrivilegedPermission_succeeds() throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+ CompletableFuture<Void> startFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME,
+ mExecutor,
+ newOutcomeReceiver(startFuture)));
+
+ startFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME, mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withZeroLifetime_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.activateEphemeralKeyMode(Duration.ZERO, mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_withInvalidLargeLifetime_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+ Duration lifetime = mController.getMaxEphemeralKeyLifetime().plusMillis(1);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.activateEphemeralKeyMode(lifetime, Runnable::run, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_concurrentRequests_secondOneFailsWithBusyError()
+ throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+ CompletableFuture<Void> future1 = new CompletableFuture<>();
+ CompletableFuture<Void> future2 = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME, mExecutor, newOutcomeReceiver(future1));
+ mController.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME, mExecutor, newOutcomeReceiver(future2));
+ });
+
+ var thrown =
+ assertThrows(
+ ExecutionException.class,
+ () -> {
+ future2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ });
+ var threadException = (ThreadNetworkException) thrown.getCause();
+ assertThat(threadException.getErrorCode()).isEqualTo(ERROR_BUSY);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void activateEphemeralKeyMode_notBorderRouter_failsWithFailedPrecondition()
+ throws Exception {
+ setConfigurationAndWait(
+ mController,
+ new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build());
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+ CompletableFuture<Void> future = new CompletableFuture<>();
+
+ mController.activateEphemeralKeyMode(
+ Duration.ofSeconds(1), mExecutor, newOutcomeReceiver(future));
+
+ var thrown =
+ assertThrows(
+ ExecutionException.class,
+ () -> future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS));
+ var threadException = (ThreadNetworkException) thrown.getCause();
+ assertThat(threadException.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void deactivateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.deactivateEphemeralKeyMode(mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void deactivateEphemeralKeyMode_notBorderRouter_failsWithFailedPrecondition()
+ throws Exception {
+ setConfigurationAndWait(
+ mController,
+ new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build());
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+ CompletableFuture<Void> future = new CompletableFuture<>();
+
+ mController.deactivateEphemeralKeyMode(mExecutor, newOutcomeReceiver(future));
+
+ var thrown =
+ assertThrows(
+ ExecutionException.class,
+ () -> future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS));
+ var threadException = (ThreadNetworkException) thrown.getCause();
+ assertThat(threadException.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_permissionsGranted_returnsCurrentState() throws Exception {
+ CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
+ CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
+ CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
+ StateCallback callback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ int state, String ephemeralKey, Instant expiry) {
+ stateFuture.complete(state);
+ ephemeralKeyFuture.complete(ephemeralKey);
+ expiryFuture.complete(expiry);
+ }
+ };
+
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.registerStateCallback(mExecutor, callback));
+
+ try {
+ assertThat(stateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+ .isEqualTo(EPHEMERAL_KEY_DISABLED);
+ assertThat(ephemeralKeyFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ assertThat(expiryFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_withoutThreadPriviledgedPermission_returnsNullEphemeralKey()
+ throws Exception {
+ CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
+ CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
+ CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
+ StateCallback callback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ int state, String ephemeralKey, Instant expiry) {
+ stateFuture.complete(state);
+ ephemeralKeyFuture.complete(ephemeralKey);
+ expiryFuture.complete(expiry);
+ }
+ };
+ joinRandomizedDatasetAndWait(mController);
+ activateEphemeralKeyModeAndWait(mController);
+
+ runAsShell(
+ ACCESS_NETWORK_STATE, () -> mController.registerStateCallback(mExecutor, callback));
+
+ try {
+ assertThat(stateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+ .isEqualTo(EPHEMERAL_KEY_ENABLED);
+ assertThat(ephemeralKeyFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ assertThat(
+ expiryFuture
+ .get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)
+ .isAfter(Instant.now()))
+ .isTrue();
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_ephemralKeyStateChanged_returnsUpdatedState() throws Exception {
+ EphemeralKeyStateListener listener = new EphemeralKeyStateListener(mController);
+ joinRandomizedDatasetAndWait(mController);
+
+ try {
+ activateEphemeralKeyModeAndWait(mController);
+ deactivateEphemeralKeyModeAndWait(mController);
+
+ listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_DISABLED);
+ listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+ listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_DISABLED);
+ } finally {
+ listener.unregisterStateCallback();
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+ public void subscribeEpskcState_epskcEnabled_returnsSameExpiry() throws Exception {
+ EphemeralKeyStateListener listener1 = new EphemeralKeyStateListener(mController);
+ Triple<Integer, String, Instant> epskc1;
+ try {
+ joinRandomizedDatasetAndWait(mController);
+ activateEphemeralKeyModeAndWait(mController);
+ epskc1 = listener1.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+ } finally {
+ listener1.unregisterStateCallback();
+ }
+
+ EphemeralKeyStateListener listener2 = new EphemeralKeyStateListener(mController);
+ try {
+ Triple<Integer, String, Instant> epskc2 =
+ listener2.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+
+ assertThat(epskc2.getSecond()).isEqualTo(epskc1.getSecond());
+ // allow time precision loss of a second since the value is passed via IPC
+ assertThat(epskc2.getThird()).isGreaterThan(epskc1.getThird().minusSeconds(1));
+ assertThat(epskc2.getThird()).isLessThan(epskc1.getThird().plusSeconds(1));
+ } finally {
+ listener2.unregisterStateCallback();
+ }
+ }
+
// TODO (b/322437869): add test case to verify when Thread is in DISABLING state, any commands
// (join/leave/scheduleMigration/setEnabled) fail with ERROR_BUSY. This is not currently tested
// because DISABLING has very short lifecycle, it's not possible to guarantee the command can be
@@ -831,6 +1140,152 @@
NET_CAPABILITY_TRUSTED);
}
+ @Test
+ public void setConfiguration_null_throwsNullPointerException() throws Exception {
+ CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ mController.setConfiguration(
+ null, mExecutor, newOutcomeReceiver(setConfigFuture)));
+ }
+
+ @Test
+ public void setConfiguration_noPermissions_throwsSecurityException() throws Exception {
+ ThreadConfiguration configuration =
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+ CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+ assertThrows(
+ SecurityException.class,
+ () -> {
+ mController.setConfiguration(
+ configuration, mExecutor, newOutcomeReceiver(setConfigFuture));
+ });
+ }
+
+ @Test
+ public void registerConfigurationCallback_permissionsGranted_returnsCurrentStatus()
+ throws Exception {
+ CompletableFuture<ThreadConfiguration> getConfigFuture = new CompletableFuture<>();
+ Consumer<ThreadConfiguration> callback = getConfigFuture::complete;
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> registerConfigurationCallback(mController, mExecutor, callback));
+ assertThat(getConfigFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+ .isEqualTo(DEFAULT_CONFIG);
+ }
+
+ @Test
+ public void registerConfigurationCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> registerConfigurationCallback(mController, mExecutor, config -> {}));
+ }
+
+ @Test
+ public void registerConfigurationCallback_returnsUpdatedConfigurations() throws Exception {
+ CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+ ConfigurationListener listener = new ConfigurationListener(mController);
+ ThreadConfiguration config1 =
+ new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(true)
+ .setNat64Enabled(true)
+ .build();
+ ThreadConfiguration config2 =
+ new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(false)
+ .setNat64Enabled(false)
+ .build();
+
+ try {
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setConfiguration(
+ config1, mExecutor, newOutcomeReceiver(setFuture1)));
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setConfiguration(
+ config2, mExecutor, newOutcomeReceiver(setFuture2)));
+ setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+ listener.expectConfiguration(DEFAULT_CONFIG);
+ listener.expectConfiguration(config1);
+ listener.expectConfiguration(config2);
+ listener.expectNoMoreConfiguration();
+ } finally {
+ listener.unregisterConfigurationCallback();
+ }
+ }
+
+ @Test
+ public void registerConfigurationCallback_alreadyRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+ Consumer<ThreadConfiguration> callback = config -> {};
+ registerConfigurationCallback(mController, mExecutor, callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> registerConfigurationCallback(mController, mExecutor, callback));
+ }
+
+ @Test
+ public void unregisterConfigurationCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ Consumer<ThreadConfiguration> callback = config -> {};
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> registerConfigurationCallback(mController, mExecutor, callback));
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.unregisterConfigurationCallback(callback));
+ }
+
+ @Test
+ public void unregisterConfigurationCallback_callbackRegistered_success() throws Exception {
+ Consumer<ThreadConfiguration> callback = config -> {};
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ registerConfigurationCallback(mController, mExecutor, callback);
+ mController.unregisterConfigurationCallback(callback);
+ });
+ }
+
+ @Test
+ public void
+ unregisterConfigurationCallback_callbackNotRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ Consumer<ThreadConfiguration> callback = config -> {};
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.unregisterConfigurationCallback(callback));
+ }
+
+ @Test
+ public void unregisterConfigurationCallback_alreadyUnregistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+ Consumer<ThreadConfiguration> callback = config -> {};
+ registerConfigurationCallback(mController, mExecutor, callback);
+ mController.unregisterConfigurationCallback(callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.unregisterConfigurationCallback(callback));
+ }
+
private void grantPermissions(String... permissions) {
for (String permission : permissions) {
mGrantedPermissions.add(permission);
@@ -857,7 +1312,10 @@
assertThat(txtMap.get("rv")).isNotNull();
assertThat(txtMap.get("tv")).isNotNull();
- assertThat(txtMap.get("sb")).isNotNull();
+ // Border Agent State Bitmap is 32 bits
+ assertThat(txtMap.get("sb").length).isEqualTo(4);
+ // The 12th bit (4th bit of the second byte) for ePSKc support should be set to 1.
+ assertThat(txtMap.get("sb")[2] & 8).isEqualTo(8);
}
@Test
@@ -882,7 +1340,10 @@
Map<String, byte[]> txtMap = resolvedService.getAttributes();
assertThat(txtMap.get("rv")).isNotNull();
assertThat(txtMap.get("tv")).isNotNull();
- assertThat(txtMap.get("sb")).isNotNull();
+ // Border Agent State Bitmap is 32 bits
+ assertThat(txtMap.get("sb").length).isEqualTo(4);
+ // The 12th bit (4th bit of the second byte) for ePSKc support should be set to 1.
+ assertThat(txtMap.get("sb")[2] & 8).isEqualTo(8);
assertThat(txtMap.get("id").length).isEqualTo(16);
}
@@ -1037,6 +1498,35 @@
}
}
+ private class ConfigurationListener {
+ private ArrayTrackRecord<ThreadConfiguration> mConfigurations = new ArrayTrackRecord<>();
+ private final ArrayTrackRecord<ThreadConfiguration>.ReadHead mReadHead =
+ mConfigurations.newReadHead();
+ ThreadNetworkController mController;
+ Consumer<ThreadConfiguration> mCallback = (config) -> mConfigurations.add(config);
+
+ ConfigurationListener(ThreadNetworkController controller) {
+ this.mController = controller;
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.registerConfigurationCallback(mExecutor, mCallback));
+ }
+
+ public void expectConfiguration(ThreadConfiguration config) {
+ assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> c.equals(config))).isNotNull();
+ }
+
+ public void expectNoMoreConfiguration() {
+ assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> true)).isNull();
+ }
+
+ public void unregisterConfigurationCallback() {
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.unregisterConfigurationCallback(mCallback));
+ }
+ }
+
private int booleanToEnabledState(boolean enabled) {
return enabled ? STATE_ENABLED : STATE_DISABLED;
}
@@ -1051,6 +1541,83 @@
waitForEnabledState(controller, booleanToEnabledState(enabled));
}
+ private void setConfigurationAndWait(
+ ThreadNetworkController controller, ThreadConfiguration configuration)
+ throws Exception {
+ CompletableFuture<Void> setFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ controller.setConfiguration(
+ configuration, mExecutor, newOutcomeReceiver(setFuture)));
+ setFuture.get(SET_CONFIGURATION_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private void deactivateEphemeralKeyModeAndWait(ThreadNetworkController controller)
+ throws Exception {
+ CompletableFuture<Void> clearFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ controller.deactivateEphemeralKeyMode(
+ mExecutor, newOutcomeReceiver(clearFuture)));
+ clearFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private void activateEphemeralKeyModeAndWait(ThreadNetworkController controller)
+ throws Exception {
+ CompletableFuture<Void> startFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ controller.activateEphemeralKeyMode(
+ EPHEMERAL_KEY_LIFETIME,
+ mExecutor,
+ newOutcomeReceiver(startFuture)));
+ startFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private class EphemeralKeyStateListener {
+ private ArrayTrackRecord<Triple<Integer, String, Instant>> mEphemeralKeyStates =
+ new ArrayTrackRecord<>();
+ private final ArrayTrackRecord<Triple<Integer, String, Instant>>.ReadHead mReadHead =
+ mEphemeralKeyStates.newReadHead();
+ ThreadNetworkController mController;
+ StateCallback mCallback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onEphemeralKeyStateChanged(
+ int state, String ephemeralKey, Instant expiry) {
+ mEphemeralKeyStates.add(new Triple<>(state, ephemeralKey, expiry));
+ }
+ };
+
+ EphemeralKeyStateListener(ThreadNetworkController controller) {
+ this.mController = controller;
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.registerStateCallback(mExecutor, mCallback));
+ }
+
+ // Expect that EphemeralKey has the expected state, and return a Triple of <state,
+ // passcode, expiry>.
+ public Triple<Integer, String, Instant> expectThreadEphemeralKeyMode(int state) {
+ Triple<Integer, String, Instant> epskc =
+ mReadHead.poll(
+ ENABLED_TIMEOUT_MILLIS, e -> Objects.equals(e.getFirst(), state));
+ assertThat(epskc).isNotNull();
+ return epskc;
+ }
+
+ public void unregisterStateCallback() {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
+ }
+ }
+
private CompletableFuture joinRandomizedDataset(
ThreadNetworkController controller, String networkName) throws Exception {
ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
@@ -1117,6 +1684,14 @@
};
}
+ private void registerConfigurationCallback(
+ ThreadNetworkController controller,
+ Executor executor,
+ Consumer<ThreadConfiguration> callback) {
+ controller.registerConfigurationCallback(executor, callback);
+ mConfigurationCallbacksToCleanUp.add(callback);
+ }
+
private static void assertDoesNotThrow(ThrowingRunnable runnable) {
try {
runnable.run();
@@ -1242,4 +1817,52 @@
@Override
public void onServiceInfoCallbackUnregistered() {}
}
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED})
+ public void setChannelMaxPowers_withPrivilegedPermission_success() throws Exception {
+ CompletableFuture<Void> powerFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setChannelMaxPowers(
+ CHANNEL_MAX_POWERS, mExecutor, newOutcomeReceiver(powerFuture)));
+
+ try {
+ assertThat(powerFuture.get()).isNull();
+ } catch (ExecutionException exception) {
+ ThreadNetworkException thrown = (ThreadNetworkException) exception.getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_UNSUPPORTED_FEATURE);
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED})
+ public void setChannelMaxPowers_withoutPrivilegedPermission_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.setChannelMaxPowers(CHANNEL_MAX_POWERS, mExecutor, v -> {}));
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED})
+ public void setChannelMaxPowers_invalidChannel_throwsIllegalArgumentException() {
+ final SparseIntArray INVALID_CHANNEL_ARRAY =
+ new SparseIntArray() {
+ {
+ put(INVALID_CHANNEL, VALID_POWER);
+ }
+ };
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.setChannelMaxPowers(new SparseIntArray(), mExecutor, v -> {}));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.setChannelMaxPowers(INVALID_CHANNEL_ARRAY, mExecutor, v -> {}));
+ }
}
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index 71693af..798a51e 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -37,9 +37,9 @@
"ot-daemon-aidl-java",
],
libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
],
}
@@ -58,6 +58,27 @@
],
srcs: [
"src/**/*.java",
+ "src/**/*.kt",
+ ],
+ compile_multilib: "both",
+}
+
+android_test {
+ name: "ThreadNetworkTrelDisabledTests",
+ platform_apis: true,
+ manifest: "AndroidManifest.xml",
+ test_config: "AndroidTestTrelDisabled.xml",
+ defaults: [
+ "framework-connectivity-test-defaults",
+ "ThreadNetworkIntegrationTestsDefaults",
+ ],
+ test_suites: [
+ "mts-tethering",
+ "general-tests",
+ ],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
],
compile_multilib: "both",
}
diff --git a/thread/tests/integration/AndroidTest.xml b/thread/tests/integration/AndroidTest.xml
index 8f98941..08409b4 100644
--- a/thread/tests/integration/AndroidTest.xml
+++ b/thread/tests/integration/AndroidTest.xml
@@ -48,4 +48,10 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.thread.tests.integration" />
</test>
+
+ <!-- Enable TREL for integration tests -->
+ <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
+ <option name="flag-value"
+ value="thread_network/TrelFeature__enabled=true"/>
+ </target_preparer>
</configuration>
diff --git a/thread/tests/integration/AndroidTestTrelDisabled.xml b/thread/tests/integration/AndroidTestTrelDisabled.xml
new file mode 100644
index 0000000..600652a
--- /dev/null
+++ b/thread/tests/integration/AndroidTestTrelDisabled.xml
@@ -0,0 +1,57 @@
+<?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 integration tests with TREL disabled">
+ <option name="test-tag" value="ThreadNetworkTrelDisabledTests" />
+ <option name="test-suite-tag" value="apct" />
+
+ <!--
+ 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" />
+
+ <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+ <option name="required-feature" value="android.hardware.thread_network" />
+ </object>
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+ <!-- Install test -->
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="ThreadNetworkTrelDisabledTests.apk" />
+ <option name="check-min-sdk" value="true" />
+ <option name="cleanup-apks" value="true" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.thread.tests.integration" />
+ </test>
+
+ <!-- Disable TREL for integration tests -->
+ <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
+ <option name="flag-value"
+ value="thread_network/TrelFeature__enabled=false"/>
+ </target_preparer>
+</configuration>
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 8c63d37..aeeed65 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -17,23 +17,29 @@
package android.net.thread;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
-import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
+import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply;
+import static android.net.thread.utils.IntegrationTestUtils.enableThreadAndJoinNetwork;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
+import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
-import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
+import static android.net.thread.utils.IntegrationTestUtils.isFrom;
import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
-import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
+import static android.net.thread.utils.IntegrationTestUtils.isTo;
+import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
+import static android.net.thread.utils.IntegrationTestUtils.leaveNetworkAndDisableThread;
import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
import static android.net.thread.utils.IntegrationTestUtils.sendUdpMessage;
+import static android.net.thread.utils.IntegrationTestUtils.stopOtDaemon;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+import static android.system.OsConstants.ICMP_ECHO;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
-import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
-import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
@@ -44,14 +50,15 @@
import static java.util.Objects.requireNonNull;
import android.content.Context;
-import android.net.InetAddresses;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
-import android.net.MacAddress;
+import android.net.RouteInfo;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.InfraNetworkDevice;
+import android.net.thread.utils.IntegrationTestUtils;
import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.TestTunNetworkUtils;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresIpv6MulticastRouting;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
@@ -65,16 +72,23 @@
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.testutils.TapPacketReader;
+import com.android.testutils.PollPacketReader;
import com.android.testutils.TestNetworkTracker;
import org.junit.After;
+import org.junit.AfterClass;
import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.IOException;
+import java.net.Inet4Address;
import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -89,50 +103,50 @@
private static final String TAG = BorderRoutingTest.class.getSimpleName();
private static final int NUM_FTD = 2;
private static final Inet6Address GROUP_ADDR_SCOPE_5 =
- (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+ (Inet6Address) parseNumericAddress("ff05::1234");
private static final Inet6Address GROUP_ADDR_SCOPE_4 =
- (Inet6Address) InetAddresses.parseNumericAddress("ff04::1234");
+ (Inet6Address) parseNumericAddress("ff04::1234");
private static final Inet6Address GROUP_ADDR_SCOPE_3 =
- (Inet6Address) InetAddresses.parseNumericAddress("ff03::1234");
-
- // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
- private static final byte[] DEFAULT_DATASET_TLVS =
- base16().decode(
- "0E080000000000010000000300001335060004001FFFE002"
- + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
- + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
- + "642D643961300102D9A00410A245479C836D551B9CA557F7"
- + "B9D351B40C0402A0FFF8");
- private static final ActiveOperationalDataset DEFAULT_DATASET =
- ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+ (Inet6Address) parseNumericAddress("ff03::1234");
+ private static final Inet4Address IPV4_SERVER_ADDR =
+ (Inet4Address) parseNumericAddress("8.8.8.8");
+ private static final IpPrefix DHCP6_PD_PREFIX = new IpPrefix("2001:db8::/64");
+ private static final IpPrefix AIL_NAT64_PREFIX = new IpPrefix("2001:db8:1234::/96");
+ private static final Inet6Address AIL_NAT64_SYNTHESIZED_SERVER_ADDR =
+ (Inet6Address) parseNumericAddress("2001:db8:1234::8.8.8.8");
+ private static final Duration UPDATE_NAT64_PREFIX_TIMEOUT = Duration.ofSeconds(10);
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
private final Context mContext = ApplicationProvider.getApplicationContext();
private final ThreadNetworkControllerWrapper mController =
ThreadNetworkControllerWrapper.newInstance(mContext);
- private OtDaemonController mOtCtl;
+ private final OtDaemonController mOtCtl = new OtDaemonController();
private HandlerThread mHandlerThread;
private Handler mHandler;
private TestNetworkTracker mInfraNetworkTracker;
private List<FullThreadDevice> mFtds;
- private TapPacketReader mInfraNetworkReader;
+ private PollPacketReader mInfraNetworkReader;
private InfraNetworkDevice mInfraDevice;
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ enableThreadAndJoinNetwork(DEFAULT_DATASET);
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ leaveNetworkAndDisableThread();
+ }
+
@Before
public void setUp() throws Exception {
- // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
- mOtCtl = new OtDaemonController();
- mOtCtl.factoryReset();
-
mHandlerThread = new HandlerThread(getClass().getSimpleName());
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mFtds = new ArrayList<>();
setUpInfraNetwork();
- mController.setEnabledAndWait(true);
- mController.joinAndWait(DEFAULT_DATASET);
// Creates a infra network device.
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
@@ -147,8 +161,7 @@
@After
public void tearDown() throws Exception {
mController.setTestNetworkAsUpstreamAndWait(null);
- mController.leaveAndWait();
- tearDownInfraNetwork();
+ TestTunNetworkUtils.tearDownAllInfraNetworks();
mHandlerThread.quitSafely();
mHandlerThread.join();
@@ -171,12 +184,12 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
// Infra device receives an echo reply sent by FTD.
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@@ -193,11 +206,11 @@
startInfraDeviceAndWaitForOnLinkAddr();
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@@ -213,21 +226,15 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
Inet6Address ftdOmr = ftd.getOmrAddress();
- // Create a new infra network and let Thread prefer it
- TestNetworkTracker oldInfraNetworkTracker = mInfraNetworkTracker;
- try {
- setUpInfraNetwork();
- mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
- startInfraDeviceAndWaitForOnLinkAddr();
+ setUpInfraNetwork();
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDeviceAndWaitForOnLinkAddr();
- mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+ mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
- } finally {
- runAsShell(MANAGE_TEST_NETWORKS, () -> oldInfraNetworkTracker.teardown());
- }
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
}
@Test
@@ -243,7 +250,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
Inet6Address ftdOmr = requireNonNull(ftd.getOmrAddress());
Inet6Address ftdMlEid = requireNonNull(ftd.getMlEid());
@@ -272,6 +279,28 @@
}
@Test
+ public void unicastRouting_otDaemonRestarts_borderRoutingWorks() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+
+ stopOtDaemon();
+ ftd.waitForStateAnyOf(List.of("leader", "router", "child"), Duration.ofSeconds(40));
+
+ startInfraDeviceAndWaitForOnLinkAddr();
+ mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
@RequiresIpv6MulticastRouting
public void multicastRouting_ftdSubscribedMulticastAddress_infraLinkJoinsMulticastGroup()
throws Exception {
@@ -285,7 +314,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_5);
@@ -307,7 +336,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
@@ -328,12 +357,12 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@@ -360,12 +389,12 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_3);
- assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ assertNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@@ -382,11 +411,11 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
- assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ assertNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@@ -407,22 +436,24 @@
*/
FullThreadDevice ftd1 = mFtds.get(0);
- startFtdChild(ftd1);
+ joinNetworkAndWaitForOmr(ftd1, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
FullThreadDevice ftd2 = mFtds.get(1);
- startFtdChild(ftd2);
+ joinNetworkAndWaitForOmr(ftd2, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+ assertNotNull(
+ pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
// Verify ping reply from ftd1 and ftd2 separately as the order of replies can't be
// predicted.
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+ assertNotNull(
+ pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
}
@Test
@@ -443,21 +474,23 @@
*/
FullThreadDevice ftd1 = mFtds.get(0);
- startFtdChild(ftd1);
+ joinNetworkAndWaitForOmr(ftd1, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
FullThreadDevice ftd2 = mFtds.get(1);
- startFtdChild(ftd2);
+ joinNetworkAndWaitForOmr(ftd2, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+ assertNotNull(
+ pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
// Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+ assertNotNull(
+ pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
}
@Test
@@ -473,16 +506,18 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
Inet6Address ftdOmr = ftd.getOmrAddress();
ftd.ping(GROUP_ADDR_SCOPE_5);
ftd.ping(GROUP_ADDR_SCOPE_4);
assertNotNull(
- pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
+ pollForIcmpPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
assertNotNull(
- pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ pollForIcmpPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
}
@Test
@@ -499,12 +534,12 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.ping(GROUP_ADDR_SCOPE_3);
assertNull(
- pollForPacketOnInfraNetwork(
+ pollForIcmpPacketOnInfraNetwork(
ICMPV6_ECHO_REQUEST_TYPE, ftd.getOmrAddress(), GROUP_ADDR_SCOPE_3));
}
@@ -521,14 +556,15 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
Inet6Address ftdLla = ftd.getLinkLocalAddress();
assertNotNull(ftdLla);
ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla);
assertNull(
- pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
+ pollForIcmpPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
}
@Test
@@ -544,7 +580,7 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
List<Inet6Address> ftdMlas = ftd.getMeshLocalAddresses();
assertFalse(ftdMlas.isEmpty());
@@ -552,7 +588,7 @@
ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla);
assertNull(
- pollForPacketOnInfraNetwork(
+ pollForIcmpPacketOnInfraNetwork(
ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
}
}
@@ -571,19 +607,17 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
Inet6Address ftdOmr = ftd.getOmrAddress();
- // Destroy infra link and re-create
- tearDownInfraNetwork();
setUpInfraNetwork();
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
startInfraDeviceAndWaitForOnLinkAddr();
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
}
@Test
@@ -599,11 +633,9 @@
*/
FullThreadDevice ftd = mFtds.get(0);
- startFtdChild(ftd);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
Inet6Address ftdOmr = ftd.getOmrAddress();
- // Destroy infra link and re-create
- tearDownInfraNetwork();
setUpInfraNetwork();
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
startInfraDeviceAndWaitForOnLinkAddr();
@@ -611,38 +643,78 @@
ftd.ping(GROUP_ADDR_SCOPE_4);
assertNotNull(
- pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ pollForIcmpPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ }
+
+ @Test
+ public void nat64_threadDevicePingIpv4InfraDevice_outboundPacketIsForwardedAndReplyIsReceived()
+ throws Exception {
+ FullThreadDevice ftd = mFtds.get(0);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+ mController.setNat64EnabledAndWait(true);
+ waitFor(() -> mOtCtl.hasNat64PrefixInNetdata(), UPDATE_NAT64_PREFIX_TIMEOUT);
+ Thread echoReplyThread = new Thread(() -> respondToEchoRequestOnce(IPV4_SERVER_ADDR));
+ echoReplyThread.start();
+
+ assertThat(ftd.ping(IPV4_SERVER_ADDR, 1 /* count */)).isEqualTo(1);
+
+ echoReplyThread.join();
+ }
+
+ private void respondToEchoRequestOnce(Inet4Address dstAddress) {
+ byte[] echoRequest = pollForIcmpPacketOnInfraNetwork(ICMP_ECHO, null, dstAddress);
+ assertNotNull(echoRequest);
+ try {
+ mInfraNetworkReader.sendResponse(buildIcmpv4EchoReply(ByteBuffer.wrap(echoRequest)));
+ } catch (IOException ignored) {
+ }
+ }
+
+ @Ignore("TODO: b/376573921 - Enable when it's not flaky at all")
+ @Test
+ public void nat64_withAilNat64Prefix_threadDevicePingIpv4InfraDevice_outboundPacketIsForwarded()
+ throws Exception {
+ TestTunNetworkUtils.tearDownInfraNetwork(mInfraNetworkTracker);
+ LinkProperties lp = new LinkProperties();
+ // NAT64 feature requires the infra network to have an IPv4 default route.
+ lp.addRoute(
+ new RouteInfo(
+ new IpPrefix("0.0.0.0/0") /* destination */,
+ null /* gateway */,
+ null /* iface */,
+ RouteInfo.RTN_UNICAST,
+ 1500 /* mtu */));
+ lp.addRoute(
+ new RouteInfo(
+ new IpPrefix("::/0") /* destination */,
+ null /* gateway */,
+ null /* iface */,
+ RouteInfo.RTN_UNICAST,
+ 1500 /* mtu */));
+ lp.setNat64Prefix(AIL_NAT64_PREFIX);
+ mInfraNetworkTracker = TestTunNetworkUtils.setUpInfraNetwork(mContext, mController, lp);
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ FullThreadDevice ftd = mFtds.get(0);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+ mController.setNat64EnabledAndWait(true);
+ mOtCtl.addPrefixInNetworkData(DHCP6_PD_PREFIX, "paros", "med");
+ waitFor(() -> mOtCtl.hasNat64PrefixInNetdata(), UPDATE_NAT64_PREFIX_TIMEOUT);
+
+ ftd.ping(IPV4_SERVER_ADDR);
+
+ assertNotNull(
+ pollForIcmpPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, null, AIL_NAT64_SYNTHESIZED_SERVER_ADDR));
}
private void setUpInfraNetwork() throws Exception {
- mInfraNetworkTracker =
- runAsShell(
- MANAGE_TEST_NETWORKS,
- () ->
- initTestNetwork(
- mContext, new LinkProperties(), 5000 /* timeoutMs */));
- mController.setTestNetworkAsUpstreamAndWait(
- mInfraNetworkTracker.getTestIface().getInterfaceName());
+ mInfraNetworkTracker = TestTunNetworkUtils.setUpInfraNetwork(mContext, mController);
}
- private void tearDownInfraNetwork() {
- runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
- }
-
- private void startFtdChild(FullThreadDevice ftd) throws Exception {
- ftd.factoryReset();
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
- waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
- Inet6Address ftdOmr = ftd.getOmrAddress();
- assertNotNull(ftdOmr);
- }
-
- private void startInfraDeviceAndWaitForOnLinkAddr() throws Exception {
+ private void startInfraDeviceAndWaitForOnLinkAddr() {
mInfraDevice =
- new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
- mInfraDevice.runSlaac(Duration.ofSeconds(60));
- assertNotNull(mInfraDevice.ipv6Addr);
+ TestTunNetworkUtils.startInfraDeviceAndWaitForOnLinkAddr(mInfraNetworkReader);
}
private void assertInfraLinkMemberOfGroup(Inet6Address address) throws Exception {
@@ -668,20 +740,28 @@
assertInfraLinkMemberOfGroup(address);
}
- private byte[] pollForPacketOnInfraNetwork(int type, Inet6Address srcAddress) {
- return pollForPacketOnInfraNetwork(type, srcAddress, null);
+ private byte[] pollForIcmpPacketOnInfraNetwork(int type, InetAddress srcAddress) {
+ return pollForIcmpPacketOnInfraNetwork(type, srcAddress, null /* destAddress */);
}
- private byte[] pollForPacketOnInfraNetwork(
- int type, Inet6Address srcAddress, Inet6Address destAddress) {
- Predicate<byte[]> filter;
- filter =
+ private byte[] pollForIcmpPacketOnInfraNetwork(
+ int type, InetAddress srcAddress, InetAddress destAddress) {
+ if (srcAddress == null && destAddress == null) {
+ throw new IllegalArgumentException("srcAddress and destAddress cannot be both null");
+ }
+ if (srcAddress != null && destAddress != null) {
+ if ((srcAddress instanceof Inet4Address) != (destAddress instanceof Inet4Address)) {
+ throw new IllegalArgumentException(
+ "srcAddress and destAddress must be both IPv4 or both IPv6");
+ }
+ }
+ boolean isIpv4 =
+ (srcAddress instanceof Inet4Address) || (destAddress instanceof Inet4Address);
+ final Predicate<byte[]> filter =
p ->
- (isExpectedIcmpv6Packet(p, type)
- && (srcAddress == null ? true : isFromIpv6Source(p, srcAddress))
- && (destAddress == null
- ? true
- : isToIpv6Destination(p, destAddress)));
+ (isIpv4 ? isExpectedIcmpv4Packet(p, type) : isExpectedIcmpv6Packet(p, type))
+ && (srcAddress == null || isFrom(p, srcAddress))
+ && (destAddress == null || isTo(p, destAddress));
return pollForPacket(mInfraNetworkReader, filter);
}
}
diff --git a/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt b/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
new file mode 100644
index 0000000..3c9aa07
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
@@ -0,0 +1,225 @@
+/*
+ * 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 android.net.thread
+
+import android.content.Context
+import android.net.DnsResolver.CLASS_IN
+import android.net.DnsResolver.TYPE_A
+import android.net.DnsResolver.TYPE_AAAA
+import android.net.InetAddresses.parseNumericAddress
+import android.net.thread.utils.FullThreadDevice
+import android.net.thread.utils.InfraNetworkDevice
+import android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET
+import android.net.thread.utils.IntegrationTestUtils.enableThreadAndJoinNetwork
+import android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr
+import android.net.thread.utils.IntegrationTestUtils.leaveNetworkAndDisableThread
+import android.net.thread.utils.IntegrationTestUtils.newPacketReader
+import android.net.thread.utils.IntegrationTestUtils.waitFor
+import android.net.thread.utils.OtDaemonController
+import android.net.thread.utils.TestDnsServer
+import android.net.thread.utils.TestTunNetworkUtils
+import android.net.thread.utils.TestUdpEchoServer
+import android.net.thread.utils.ThreadFeatureCheckerRule
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature
+import android.net.thread.utils.ThreadNetworkControllerWrapper
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.android.net.module.util.DnsPacket
+import com.android.net.module.util.DnsPacket.ANSECTION
+import com.android.testutils.PollPacketReader
+import com.android.testutils.TestNetworkTracker
+import com.google.common.truth.Truth.assertThat
+import java.net.Inet4Address
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.time.Duration
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Integration test cases for Thread Internet Access features. */
+@RunWith(AndroidJUnit4::class)
+@RequiresThreadFeature
+@RequiresSimulationThreadDevice
+@LargeTest
+class InternetAccessTest {
+ companion object {
+ private val TAG = BorderRoutingTest::class.java.simpleName
+ private val NUM_FTD = 1
+ private val DNS_SERVER_ADDR = parseNumericAddress("8.8.8.8") as Inet4Address
+ private val UDP_ECHO_SERVER_ADDRESS =
+ InetSocketAddress(parseNumericAddress("1.2.3.4"), 12345)
+ private val ANSWER_RECORDS =
+ listOf(
+ DnsPacket.DnsRecord.makeAOrAAAARecord(
+ ANSECTION,
+ "google.com",
+ CLASS_IN,
+ 30 /* ttl */,
+ parseNumericAddress("1.2.3.4"),
+ ),
+ DnsPacket.DnsRecord.makeAOrAAAARecord(
+ ANSECTION,
+ "google.com",
+ CLASS_IN,
+ 30 /* ttl */,
+ parseNumericAddress("2001::234"),
+ ),
+ )
+
+ @BeforeClass
+ @JvmStatic
+ fun beforeClass() {
+ enableThreadAndJoinNetwork(DEFAULT_DATASET)
+ }
+
+ @AfterClass
+ @JvmStatic
+ fun afterClass() {
+ leaveNetworkAndDisableThread()
+ }
+ }
+
+ @get:Rule val threadRule = ThreadFeatureCheckerRule()
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context))
+ private lateinit var otCtl: OtDaemonController
+ private lateinit var handlerThread: HandlerThread
+ private lateinit var handler: Handler
+ private lateinit var infraNetworkTracker: TestNetworkTracker
+ private lateinit var ftds: ArrayList<FullThreadDevice>
+ private lateinit var infraNetworkReader: PollPacketReader
+ private lateinit var infraDevice: InfraNetworkDevice
+ private lateinit var dnsServer: TestDnsServer
+ private lateinit var udpEchoServer: TestUdpEchoServer
+
+ @Before
+ @Throws(Exception::class)
+ fun setUp() {
+ otCtl = OtDaemonController()
+
+ handlerThread = HandlerThread(javaClass.simpleName)
+ handlerThread.start()
+ handler = Handler(handlerThread.looper)
+ ftds = ArrayList()
+
+ infraNetworkTracker = TestTunNetworkUtils.setUpInfraNetwork(context, controller)
+
+ // Create an infra network device.
+ infraNetworkReader = newPacketReader(infraNetworkTracker.testIface, handler)
+ infraDevice = TestTunNetworkUtils.startInfraDeviceAndWaitForOnLinkAddr(infraNetworkReader)
+
+ // Create a DNS server
+ dnsServer = TestDnsServer(infraNetworkReader, DNS_SERVER_ADDR, ANSWER_RECORDS)
+
+ // Create a UDP echo server
+ udpEchoServer = TestUdpEchoServer(infraNetworkReader, UDP_ECHO_SERVER_ADDRESS)
+
+ // Create Ftds
+ for (i in 0 until NUM_FTD) {
+ ftds.add(FullThreadDevice(15 + i /* node ID */))
+ }
+ }
+
+ @After
+ @Throws(Exception::class)
+ fun tearDown() {
+ controller.setTestNetworkAsUpstreamAndWait(null)
+ TestTunNetworkUtils.tearDownAllInfraNetworks()
+
+ dnsServer.stop()
+ udpEchoServer.stop()
+
+ handlerThread.quitSafely()
+ handlerThread.join()
+
+ ftds.forEach { it.destroy() }
+ ftds.clear()
+ }
+
+ @Test
+ fun nat64Enabled_threadDeviceResolvesHost_hostIsResolved() {
+ controller.setNat64EnabledAndWait(true)
+ waitFor({ otCtl.hasNat64PrefixInNetdata() }, Duration.ofSeconds(10))
+ val ftd = ftds[0]
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET)
+ dnsServer.start()
+
+ val ipv4Addresses =
+ ftd.resolveHost("google.com", TYPE_A).map { extractIpv4AddressFromMappedAddress(it) }
+ assertThat(ipv4Addresses).isEqualTo(listOf(parseNumericAddress("1.2.3.4")))
+ val ipv6Addresses = ftd.resolveHost("google.com", TYPE_AAAA)
+ assertThat(ipv6Addresses).isEqualTo(listOf(parseNumericAddress("2001::234")))
+ }
+
+ @Test
+ fun nat64Disabled_threadDeviceResolvesHost_hostIsNotResolved() {
+ controller.setNat64EnabledAndWait(false)
+ val ftd = ftds[0]
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET)
+ dnsServer.start()
+
+ assertThat(ftd.resolveHost("google.com", TYPE_A)).isEmpty()
+ assertThat(ftd.resolveHost("google.com", TYPE_AAAA)).isEmpty()
+ }
+
+ @Test
+ fun nat64Enabled_threadDeviceSendsUdpToEchoServer_replyIsReceived() {
+ controller.setNat64EnabledAndWait(true)
+ waitFor({ otCtl.hasNat64PrefixInNetdata() }, Duration.ofSeconds(10))
+ val ftd = ftds[0]
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET)
+ udpEchoServer.start()
+
+ ftd.udpOpen()
+ ftd.udpSend("Hello,Thread", UDP_ECHO_SERVER_ADDRESS.address, UDP_ECHO_SERVER_ADDRESS.port)
+ val reply = ftd.udpReceive()
+ assertThat(reply).isEqualTo("Hello,Thread")
+ }
+
+ @Test
+ fun nat64Enabled_afterInfraNetworkSwitch_threadDeviceSendsUdpToEchoServer_replyIsReceived() {
+ controller.setNat64EnabledAndWait(true)
+ waitFor({ otCtl.hasNat64PrefixInNetdata() }, Duration.ofSeconds(10))
+ infraNetworkTracker = TestTunNetworkUtils.setUpInfraNetwork(context, controller)
+ infraNetworkReader = newPacketReader(infraNetworkTracker.testIface, handler)
+ udpEchoServer = TestUdpEchoServer(infraNetworkReader, UDP_ECHO_SERVER_ADDRESS)
+ val ftd = ftds[0]
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET)
+ waitFor({ otCtl.hasNat64PrefixInNetdata() }, Duration.ofSeconds(10))
+ udpEchoServer.start()
+
+ ftd.udpOpen()
+ ftd.udpSend("Hello,Thread", UDP_ECHO_SERVER_ADDRESS.address, UDP_ECHO_SERVER_ADDRESS.port)
+ val reply = ftd.udpReceive()
+ assertThat(reply).isEqualTo("Hello,Thread")
+ }
+
+ private fun extractIpv4AddressFromMappedAddress(address: InetAddress): Inet4Address {
+ return InetAddress.getByAddress(address.address.slice(12 until 16).toByteArray())
+ as Inet4Address
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index e10f134..6c2a9bb 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -18,10 +18,10 @@
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
-import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
import static android.net.thread.utils.IntegrationTestUtils.discoverService;
+import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
import static android.net.thread.utils.IntegrationTestUtils.resolveService;
import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
@@ -30,6 +30,8 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -50,6 +52,9 @@
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HexDump;
+
import com.google.common.truth.Correspondence;
import org.junit.After;
@@ -64,6 +69,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
@@ -168,8 +174,7 @@
// Creates Full Thread Devices (FTD) and let them join the network.
for (FullThreadDevice ftd : mFtds) {
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
}
int randomId = new Random().nextInt(10_000);
@@ -223,8 +228,7 @@
// Creates a Full Thread Devices (FTD) and let it join the network.
FullThreadDevice ftd = mFtds.get(0);
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.setSrpHostname("my-host");
ftd.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001:db8::1")));
ftd.addSrpService(
@@ -279,8 +283,7 @@
// Creates a Full Thread Devices (FTD) and let it join the network.
FullThreadDevice ftd = mFtds.get(0);
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.setSrpHostname("my-host");
ftd.setSrpHostAddresses(
List.of(
@@ -346,8 +349,7 @@
mRegistrationListeners.add(listener);
for (int i = 0; i < NUM_FTD; ++i) {
FullThreadDevice ftd = mFtds.get(i);
- ftd.joinNetwork(DEFAULT_DATASET);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
ftd.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
}
final ArrayList<NsdServiceInfo> browsedServices = new ArrayList<>();
@@ -409,8 +411,7 @@
* </pre>
*/
FullThreadDevice srpClient = mFtds.get(0);
- srpClient.joinNetwork(DEFAULT_DATASET);
- srpClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(srpClient, DEFAULT_DATASET);
srpClient.setSrpHostname("my-host");
srpClient.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001::1")));
srpClient.addSrpService(
@@ -421,8 +422,7 @@
Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
FullThreadDevice dnsClient = mFtds.get(1);
- dnsClient.joinNetwork(DEFAULT_DATASET);
- dnsClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ joinNetworkAndWaitForOmr(dnsClient, DEFAULT_DATASET);
dnsClient.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
NsdServiceInfo browsedService = dnsClient.browseService("_test._udp.default.service.arpa.");
@@ -447,6 +447,40 @@
.containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
}
+ @Test
+ // TODO: move this case out of ServiceDiscoveryTest when the service discovery utilities
+ // are decoupled from this test.
+ public void trelFeatureFlagEnabled_trelServicePublished() throws Exception {
+ assumeTrue(
+ DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+ "thread_network", "TrelFeature__enabled", false));
+
+ NsdServiceInfo discoveredService = discoverService(mNsdManager, "_trel._udp");
+ assertThat(discoveredService).isNotNull();
+ // Resolve service with the current TREL port, otherwise it may return stale service from
+ // a previous infra link setup.
+ NsdServiceInfo trelService =
+ resolveServiceUntil(
+ mNsdManager, discoveredService, s -> s.getPort() == mOtCtl.getTrelPort());
+
+ Map<String, byte[]> txtMap = trelService.getAttributes();
+ assertThat(HexDump.toHexString(txtMap.get("xa")).toLowerCase(Locale.ROOT))
+ .isEqualTo(mOtCtl.getExtendedAddr().toLowerCase(Locale.ROOT));
+ assertThat(HexDump.toHexString(txtMap.get("xp")).toLowerCase(Locale.ROOT))
+ .isEqualTo(mOtCtl.getExtendedPanId().toLowerCase(Locale.ROOT));
+ }
+
+ @Test
+ // TODO: move this case out of ServiceDiscoveryTest when the service discovery utilities
+ // are decoupled from this test.
+ public void trelFeatureFlagDisabled_trelServiceNotPublished() throws Exception {
+ assumeFalse(
+ DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+ "thread_network", "TrelFeature__enabled", false));
+
+ assertThrows(TimeoutException.class, () -> discoverService(mNsdManager, "_trel._udp"));
+ }
+
private void registerService(NsdServiceInfo serviceInfo, RegistrationListener listener)
throws InterruptedException, ExecutionException, TimeoutException {
mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, listener);
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 61b6eac..d41550b 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -16,36 +16,47 @@
package android.net.thread;
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_CONFIG;
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;
+import static android.net.thread.utils.ThreadNetworkControllerWrapper.JOIN_TIMEOUT;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
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.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
import android.net.thread.utils.ThreadNetworkControllerWrapper;
+import android.net.thread.utils.ThreadStateListener;
import android.os.SystemClock;
import androidx.test.core.app.ApplicationProvider;
@@ -66,6 +77,7 @@
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -83,6 +95,8 @@
// The maximum time for changes to be propagated to netdata.
private static final Duration NET_DATA_UPDATE_TIMEOUT = Duration.ofSeconds(1);
+ private static final Duration NETWORK_CALLBACK_TIMEOUT = Duration.ofSeconds(10);
+
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
base16().decode(
@@ -93,6 +107,8 @@
+ "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+ private static final ThreadConfiguration DEFAULT_CONFIG =
+ new ThreadConfiguration.Builder().build();
private static final Inet6Address GROUP_ADDR_ALL_ROUTERS =
(Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
@@ -124,8 +140,11 @@
@After
public void tearDown() throws Exception {
+ ThreadStateListener.stopAllListeners();
+
mController.setTestNetworkAsUpstreamAndWait(null);
mController.leaveAndWait();
+ mController.setConfigurationAndWait(DEFAULT_CONFIG);
mFtd.destroy();
mExecutor.shutdownNow();
@@ -253,6 +272,20 @@
}
@Test
+ public void joinNetwork_joinTheSameNetworkTwice_neverDetached() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+ mController.waitForRole(DEVICE_ROLE_LEADER, JOIN_TIMEOUT);
+
+ var listener = ThreadStateListener.startListener(mController.get());
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ assertThat(
+ listener.pollForAnyRoleOf(
+ List.of(DEVICE_ROLE_DETACHED, DEVICE_ROLE_STOPPED), JOIN_TIMEOUT))
+ .isNull();
+ }
+
+ @Test
public void edPingsMeshLocalAddresses_oneReplyPerRequest() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
startFtdChild(mFtd, DEFAULT_DATASET);
@@ -327,6 +360,44 @@
.isFalse();
}
+ @Test
+ public void setConfiguration_disableBorderRouter_noBrfunctionsEnabled() throws Exception {
+ NetworkRequest request =
+ new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .build();
+ startFtdLeader(mFtd, DEFAULT_DATASET);
+
+ mController.setConfigurationAndWait(
+ new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build());
+ mController.joinAndWait(DEFAULT_DATASET);
+ NetworkCapabilities caps = registerNetworkCallbackAndWait(request);
+
+ assertThat(caps.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)).isFalse();
+ assertThat(mOtCtl.getBorderRoutingState()).ignoringCase().isEqualTo("disabled");
+ assertThat(mOtCtl.getSrpServerState()).ignoringCase().isNotEqualTo("disabled");
+ // TODO: b/376217403 - enables / disables Border Agent at runtime
+ }
+
+ private NetworkCapabilities registerNetworkCallbackAndWait(NetworkRequest request)
+ throws Exception {
+ CompletableFuture<Network> networkFuture = new CompletableFuture<>();
+ ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ ConnectivityManager.NetworkCallback callback =
+ new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ networkFuture.complete(network);
+ }
+ };
+
+ runAsShell(ACCESS_NETWORK_STATE, () -> cm.registerNetworkCallback(request, callback));
+
+ assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT.getSeconds(), SECONDS)).isNotNull();
+ return runAsShell(
+ ACCESS_NETWORK_STATE, () -> cm.getNetworkCapabilities(networkFuture.get()));
+ }
+
// TODO (b/323300829): add more tests for integration with linux platform and
// ConnectivityService
@@ -341,6 +412,14 @@
ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8));
}
+ /** Starts a Thread FTD device as a leader. */
+ private void startFtdLeader(FullThreadDevice ftd, ActiveOperationalDataset activeDataset)
+ throws Exception {
+ ftd.factoryReset();
+ ftd.joinNetwork(activeDataset);
+ ftd.waitForStateAnyOf(List.of("leader"), Duration.ofSeconds(8));
+ }
+
/**
* Starts a UDP echo server and replies to the first UDP message.
*
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
deleted file mode 100644
index ba04348..0000000
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * 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 android.net.thread;
-
-import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.testutils.TestPermissionUtil.runAsShell;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import android.content.Context;
-import android.net.thread.utils.ThreadFeatureCheckerRule;
-import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
-import android.os.OutcomeReceiver;
-import android.util.SparseIntArray;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-/** Tests for hide methods of {@link ThreadNetworkController}. */
-@LargeTest
-@RequiresThreadFeature
-@RunWith(AndroidJUnit4.class)
-public class ThreadNetworkControllerTest {
- private static final int VALID_POWER = 32_767;
- private static final int INVALID_POWER = 32_768;
- private static final int VALID_CHANNEL = 20;
- private static final int INVALID_CHANNEL = 10;
- private static final String THREAD_NETWORK_PRIVILEGED =
- "android.permission.THREAD_NETWORK_PRIVILEGED";
-
- private static final SparseIntArray CHANNEL_MAX_POWERS =
- new SparseIntArray() {
- {
- put(20, 32767);
- }
- };
-
- @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
-
- private final Context mContext = ApplicationProvider.getApplicationContext();
- private ExecutorService mExecutor;
- private ThreadNetworkController mController;
-
- @Before
- public void setUp() throws Exception {
- mController =
- mContext.getSystemService(ThreadNetworkManager.class)
- .getAllThreadNetworkControllers()
- .get(0);
-
- mExecutor = Executors.newSingleThreadExecutor();
- }
-
- @After
- public void tearDown() throws Exception {
- dropAllPermissions();
- }
-
- @Test
- public void setChannelMaxPowers_withPrivilegedPermission_success() throws Exception {
- CompletableFuture<Void> powerFuture = new CompletableFuture<>();
-
- runAsShell(
- THREAD_NETWORK_PRIVILEGED,
- () ->
- mController.setChannelMaxPowers(
- CHANNEL_MAX_POWERS, mExecutor, newOutcomeReceiver(powerFuture)));
-
- try {
- assertThat(powerFuture.get()).isNull();
- } catch (ExecutionException exception) {
- ThreadNetworkException thrown = (ThreadNetworkException) exception.getCause();
- assertThat(thrown.getErrorCode()).isEqualTo(ERROR_UNSUPPORTED_OPERATION);
- }
- }
-
- @Test
- public void setChannelMaxPowers_withoutPrivilegedPermission_throwsSecurityException()
- throws Exception {
- dropAllPermissions();
-
- assertThrows(
- SecurityException.class,
- () -> mController.setChannelMaxPowers(CHANNEL_MAX_POWERS, mExecutor, v -> {}));
- }
-
- @Test
- public void setChannelMaxPowers_emptyChannelMaxPower_throwsIllegalArgumentException() {
- assertThrows(
- IllegalArgumentException.class,
- () -> mController.setChannelMaxPowers(new SparseIntArray(), mExecutor, v -> {}));
- }
-
- @Test
- public void setChannelMaxPowers_invalidChannel_throwsIllegalArgumentException() {
- final SparseIntArray INVALID_CHANNEL_ARRAY =
- new SparseIntArray() {
- {
- put(INVALID_CHANNEL, VALID_POWER);
- }
- };
-
- assertThrows(
- IllegalArgumentException.class,
- () -> mController.setChannelMaxPowers(INVALID_CHANNEL_ARRAY, mExecutor, v -> {}));
- }
-
- @Test
- public void setChannelMaxPowers_invalidPower_throwsIllegalArgumentException() {
- final SparseIntArray INVALID_POWER_ARRAY =
- new SparseIntArray() {
- {
- put(VALID_CHANNEL, INVALID_POWER);
- }
- };
-
- assertThrows(
- IllegalArgumentException.class,
- () -> mController.setChannelMaxPowers(INVALID_POWER_ARRAY, mExecutor, v -> {}));
- }
-
- private static void dropAllPermissions() {
- getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
- }
-
- private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
- CompletableFuture<V> future) {
- return new OutcomeReceiver<V, ThreadNetworkException>() {
- @Override
- public void onResult(V result) {
- future.complete(result);
- }
-
- @Override
- public void onError(ThreadNetworkException e) {
- future.completeExceptionally(e);
- }
- };
- }
-}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
index 8835f40..2f0ab34 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
@@ -19,14 +19,19 @@
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_CONFIG;
+import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import android.content.Context;
+import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
import android.net.thread.utils.ThreadNetworkControllerWrapper;
@@ -41,6 +46,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.net.Inet6Address;
+import java.time.Duration;
+import java.util.List;
import java.util.concurrent.ExecutionException;
/** Integration tests for {@link ThreadNetworkShellCommand}. */
@@ -53,15 +61,26 @@
private final Context mContext = ApplicationProvider.getApplicationContext();
private final ThreadNetworkControllerWrapper mController =
ThreadNetworkControllerWrapper.newInstance(mContext);
+ private final OtDaemonController mOtCtl = new OtDaemonController();
+ private FullThreadDevice mFtd;
@Before
- public void setUp() {
+ public void setUp() throws Exception {
+ // TODO(b/366141754): The current implementation of "thread_network ot-ctl factoryreset"
+ // results in timeout error.
+ // A future fix will provide proper support for factoryreset, allowing us to replace the
+ // legacy "ot-ctl".
+ mOtCtl.factoryReset();
+
+ mFtd = new FullThreadDevice(10 /* nodeId */);
ensureThreadEnabled();
}
@After
- public void tearDown() {
+ public void tearDown() throws Exception {
+ mFtd.destroy();
ensureThreadEnabled();
+ mController.setConfigurationAndWait(DEFAULT_CONFIG);
}
private static void ensureThreadEnabled() {
@@ -69,6 +88,13 @@
runThreadCommand("enable");
}
+ private static void startFtdChild(FullThreadDevice ftd, ActiveOperationalDataset activeDataset)
+ throws Exception {
+ ftd.factoryReset();
+ ftd.joinNetwork(activeDataset);
+ ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8));
+ }
+
@Test
public void enable_threadStateIsEnabled() throws Exception {
runThreadCommand("enable");
@@ -123,6 +149,59 @@
assertThat(result).contains("Thread country code = CN");
}
+ @Test
+ public void handleOtCtlCommand_enableIfconfig_getIfconfigReturnsUP() {
+ runThreadCommand("ot-ctl ifconfig up");
+
+ final String result = runThreadCommand("ot-ctl ifconfig");
+
+ assertThat(result).isEqualTo("up\r\nDone\r\n");
+ }
+
+ @Test
+ public void handleOtCtlCommand_disableIfconfig_startThreadFailsWithInvalidState() {
+ runThreadCommand("ot-ctl ifconfig down");
+
+ final String result = runThreadCommand("ot-ctl thread start");
+
+ assertThat(result).isEqualTo("Error 13: InvalidState\r\n");
+ }
+
+ @Test
+ public void handleOtCtlCommand_pingFtd_getValidResponse() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+ startFtdChild(mFtd, DEFAULT_DATASET);
+ final Inet6Address ftdMlEid = mFtd.getMlEid();
+ assertNotNull(ftdMlEid);
+
+ final String result = runThreadCommand("ot-ctl ping " + ftdMlEid.getHostAddress());
+
+ assertThat(result).contains("1 packets transmitted, 1 packets received");
+ assertThat(result).contains("Packet loss = 0.0%");
+ assertThat(result).endsWith("Done\r\n");
+ }
+
+ @Test
+ public void config_getConfig_expectedValueIsPrinted() throws Exception {
+ ThreadConfiguration config =
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+ mController.setConfigurationAndWait(config);
+
+ final String result = runThreadCommand("config");
+
+ assertThat(result).contains("nat64Enabled=true");
+ }
+
+ @Test
+ public void config_setConfig_expectedValueIsSet() throws Exception {
+ ThreadConfiguration config = new ThreadConfiguration.Builder().build();
+ mController.setConfigurationAndWait(config);
+
+ runThreadCommand("config nat64 enabled");
+
+ assertThat(mController.getConfiguration().isNat64Enabled()).isTrue();
+ }
+
private static String runThreadCommand(String cmd) {
return runShellCommandOrThrow("cmd thread_network " + cmd);
}
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 c0a8eea..209eed6 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -15,6 +15,8 @@
*/
package android.net.thread.utils;
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
@@ -232,8 +234,8 @@
return matcher.group(4);
}
- /** Sends a UDP message to given IPv6 address and port. */
- public void udpSend(String message, Inet6Address serverAddr, int serverPort) {
+ /** Sends a UDP message to given IP address and port. */
+ public void udpSend(String message, InetAddress serverAddr, int serverPort) {
executeCommand("udp send %s %d %s", serverAddr.getHostAddress(), serverPort, message);
}
@@ -282,6 +284,7 @@
for (String subtype : subtypes) {
fullServiceType.append(",").append(subtype);
}
+ waitForSrpServer();
executeCommand(
"srp client service add %s %s %d %d %d %s",
serviceName,
@@ -353,6 +356,31 @@
executeCommand("dns config " + address);
}
+ /** Resolves the {@code queryType} record of the {@code hostname} via DNS. */
+ public List<InetAddress> resolveHost(String hostname, int queryType) {
+ // CLI output:
+ // DNS response for hostname.com. - fd12::abc1 TTL:50 fd12::abc2 TTL:50 fd12::abc3 TTL:50
+
+ String command;
+ switch (queryType) {
+ case TYPE_A -> command = "resolve4";
+ case TYPE_AAAA -> command = "resolve";
+ default -> throw new IllegalArgumentException("Invalid query type: " + queryType);
+ }
+ final List<InetAddress> addresses = new ArrayList<>();
+ String line;
+ try {
+ line = executeCommand("dns " + command + " " + hostname).get(0);
+ } catch (IllegalStateException e) {
+ return addresses;
+ }
+ final String[] addressTtlPairs = line.split("-")[1].strip().split(" ");
+ for (int i = 0; i < addressTtlPairs.length; i += 2) {
+ addresses.add(InetAddresses.parseNumericAddress(addressTtlPairs[i]));
+ }
+ return addresses;
+ }
+
/** Returns the first browsed service instance of {@code serviceType}. */
public NsdServiceInfo browseService(String serviceType) {
// CLI output:
@@ -416,7 +444,7 @@
executeCommand("ipmaddr add " + address.getHostAddress());
}
- public void ping(Inet6Address address, Inet6Address source) {
+ public void ping(InetAddress address, Inet6Address source) {
ping(
address,
source,
@@ -427,7 +455,7 @@
PING_TIMEOUT_0_1_SECOND);
}
- public void ping(Inet6Address address) {
+ public void ping(InetAddress address) {
ping(
address,
null,
@@ -439,7 +467,7 @@
}
/** Returns the number of ping reply packets received. */
- public int ping(Inet6Address address, int count) {
+ public int ping(InetAddress address, int count) {
List<String> output =
ping(
address,
@@ -453,7 +481,7 @@
}
private List<String> ping(
- Inet6Address address,
+ InetAddress address,
Inet6Address source,
int size,
int count,
@@ -492,6 +520,22 @@
return -1;
}
+ /** Waits for an SRP server to be present in Network Data */
+ private void waitForSrpServer() throws TimeoutException {
+ // CLI output:
+ // > srp client server
+ // [fd64:db12:25f4:7e0b:1bfc:6344:25ac:2dd7]:53538
+ // Done
+ waitFor(
+ () -> {
+ final String serverAddr = executeCommand("srp client server").get(0);
+ final int lastColonIndex = serverAddr.lastIndexOf(':');
+ final int port = Integer.parseInt(serverAddr.substring(lastColonIndex + 1));
+ return port > 0;
+ },
+ SERVICE_DISCOVERY_TIMEOUT);
+ }
+
@FormatMethod
private List<String> executeCommand(String commandFormat, Object... args) {
return executeCommand(String.format(commandFormat, args));
diff --git a/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
index 72a278c..cb0c8ee 100644
--- a/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
@@ -28,7 +28,7 @@
import com.android.net.module.util.Ipv6Utils;
import com.android.net.module.util.structs.LlaOption;
import com.android.net.module.util.structs.PrefixInformationOption;
-import com.android.testutils.TapPacketReader;
+import com.android.testutils.PollPacketReader;
import java.io.IOException;
import java.net.Inet6Address;
@@ -49,18 +49,18 @@
// The MAC address of this device.
public final MacAddress macAddr;
// The packet reader of the TUN interface of the test network.
- public final TapPacketReader packetReader;
+ public final PollPacketReader packetReader;
// The IPv6 address generated by SLAAC for the device.
public Inet6Address ipv6Addr;
/**
* Constructs an InfraNetworkDevice with the given {@link MAC address} and {@link
- * TapPacketReader}.
+ * PollPacketReader}.
*
* @param macAddr the MAC address of the device
* @param packetReader the packet reader of the TUN interface of the test network.
*/
- public InfraNetworkDevice(MacAddress macAddr, TapPacketReader packetReader) {
+ public InfraNetworkDevice(MacAddress macAddr, PollPacketReader packetReader) {
this.macAddr = macAddr;
this.packetReader = packetReader;
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
deleted file mode 100644
index ada46c8..0000000
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ /dev/null
@@ -1,474 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.net.thread.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;
-import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
-import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
-
-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;
-import com.android.net.module.util.structs.Ipv6Header;
-import com.android.net.module.util.structs.PrefixInformationOption;
-import com.android.net.module.util.structs.RaHeader;
-import com.android.testutils.HandlerUtils;
-import com.android.testutils.TapPacketReader;
-
-import com.google.common.util.concurrent.SettableFuture;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.net.DatagramPacket;
-import java.net.DatagramSocket;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.SocketAddress;
-import java.nio.ByteBuffer;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-
-/** Static utility methods relating to Thread integration tests. */
-public final class IntegrationTestUtils {
- // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
- // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
- // seconds to be safe
- public static final Duration RESTART_JOIN_TIMEOUT = Duration.ofSeconds(40);
- public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(30);
- public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
- public static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
- public static final Duration SERVICE_DISCOVERY_TIMEOUT = Duration.ofSeconds(20);
-
- private IntegrationTestUtils() {}
-
- /**
- * Waits for the given {@link Supplier} to be true until given timeout.
- *
- * @param condition the condition to check
- * @param timeout the time to wait for the condition before throwing
- * @throws TimeoutException if the condition is still not met when the timeout expires
- */
- public static void waitFor(Supplier<Boolean> condition, Duration timeout)
- throws TimeoutException {
- final long intervalMills = 500;
- final long timeoutMills = timeout.toMillis();
-
- for (long i = 0; i < timeoutMills; i += intervalMills) {
- if (condition.get()) {
- return;
- }
- SystemClock.sleep(intervalMills);
- }
- if (condition.get()) {
- return;
- }
- throw new TimeoutException("The condition failed to become true in " + timeout);
- }
-
- /**
- * Creates a {@link TapPacketReader} given the {@link TestNetworkInterface} and {@link Handler}.
- *
- * @param testNetworkInterface the TUN interface of the test network
- * @param handler the handler to process the packets
- * @return the {@link TapPacketReader}
- */
- public static TapPacketReader newPacketReader(
- TestNetworkInterface testNetworkInterface, Handler handler) {
- FileDescriptor fd = testNetworkInterface.getFileDescriptor().getFileDescriptor();
- final TapPacketReader reader =
- new TapPacketReader(handler, fd, testNetworkInterface.getMtu());
- handler.post(() -> reader.start());
- HandlerUtils.waitForIdle(handler, 5000 /* timeout in milliseconds */);
- return reader;
- }
-
- /**
- * Waits for the Thread module to enter any state of the given {@code deviceRoles}.
- *
- * @param controller the {@link ThreadNetworkController}
- * @param deviceRoles the desired device roles. See also {@link
- * ThreadNetworkController.DeviceRole}
- * @param timeout the time to wait for the expected state before throwing
- * @return the {@link ThreadNetworkController.DeviceRole} after waiting
- * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
- * expires
- */
- public static int waitForStateAnyOf(
- ThreadNetworkController controller, List<Integer> deviceRoles, Duration timeout)
- throws TimeoutException {
- SettableFuture<Integer> future = SettableFuture.create();
- ThreadNetworkController.StateCallback callback =
- newRole -> {
- if (deviceRoles.contains(newRole)) {
- future.set(newRole);
- }
- };
- controller.registerStateCallback(directExecutor(), callback);
- try {
- return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
- } catch (InterruptedException | ExecutionException e) {
- throw new TimeoutException(
- String.format(
- "The device didn't become an expected role in %s: %s",
- timeout, e.getMessage()));
- } finally {
- controller.unregisterStateCallback(callback);
- }
- }
-
- /**
- * Polls for a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
- *
- * @param packetReader a TUN packet reader
- * @param filter the filter to be applied on the packet
- * @return the first IPv6 packet that satisfies the {@code filter}. If it has waited for more
- * than 3000ms to read the next packet, the method will return null
- */
- public static byte[] pollForPacket(TapPacketReader packetReader, Predicate<byte[]> filter) {
- byte[] packet;
- while ((packet = packetReader.poll(3000 /* timeoutMs */, filter)) != null) {
- return packet;
- }
- return null;
- }
-
- /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
- public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
- if (packet == null) {
- return false;
- }
- ByteBuffer buf = ByteBuffer.wrap(packet);
- try {
- if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
- return false;
- }
- return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
- } catch (IllegalArgumentException ignored) {
- // It's fine that the passed in packet is malformed because it's could be sent
- // by anybody.
- }
- return false;
- }
-
- public static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
- if (packet == null) {
- return false;
- }
- ByteBuffer buf = ByteBuffer.wrap(packet);
- try {
- return Struct.parse(Ipv6Header.class, buf).srcIp.equals(src);
- } catch (IllegalArgumentException ignored) {
- // It's fine that the passed in packet is malformed because it's could be sent
- // by anybody.
- }
- return false;
- }
-
- public static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
- if (packet == null) {
- return false;
- }
- ByteBuffer buf = ByteBuffer.wrap(packet);
- try {
- return Struct.parse(Ipv6Header.class, buf).dstIp.equals(dest);
- } catch (IllegalArgumentException ignored) {
- // It's fine that the passed in packet is malformed because it's could be sent
- // by anybody.
- }
- return false;
- }
-
- /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
- public static List<PrefixInformationOption> getRaPios(byte[] raMsg) {
- final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
-
- if (raMsg == null) {
- return pioList;
- }
-
- final ByteBuffer buf = ByteBuffer.wrap(raMsg);
- final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buf);
- if (ipv6Header.nextHeader != (byte) IPPROTO_ICMPV6) {
- return pioList;
- }
-
- final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
- if (icmpv6Header.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) {
- return pioList;
- }
-
- Struct.parse(RaHeader.class, buf);
- while (buf.position() < raMsg.length) {
- final int currentPos = buf.position();
- final int type = Byte.toUnsignedInt(buf.get());
- final int length = Byte.toUnsignedInt(buf.get());
- if (type == ICMPV6_ND_OPTION_PIO) {
- final ByteBuffer pioBuf =
- ByteBuffer.wrap(
- buf.array(),
- currentPos,
- Struct.getSize(PrefixInformationOption.class));
- final PrefixInformationOption pio =
- Struct.parse(PrefixInformationOption.class, pioBuf);
- pioList.add(pio);
-
- // Move ByteBuffer position to the next option.
- buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
- } else {
- // The length is in units of 8 octets.
- buf.position(currentPos + (length * 8));
- }
- }
- return pioList;
- }
-
- /**
- * Sends a UDP message to a destination.
- *
- * @param dstAddress the IP address of the destination
- * @param dstPort the port of the destination
- * @param message the message in UDP payload
- * @throws IOException if failed to send the message
- */
- public static void sendUdpMessage(InetAddress dstAddress, int dstPort, String message)
- throws IOException {
- SocketAddress dstSockAddr = new InetSocketAddress(dstAddress, dstPort);
-
- try (DatagramSocket socket = new DatagramSocket()) {
- socket.connect(dstSockAddr);
-
- byte[] msgBytes = message.getBytes();
- DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length);
-
- socket.send(packet);
- }
- }
-
- public static boolean isInMulticastGroup(String interfaceName, Inet6Address address) {
- final String cmd = "ip -6 maddr show dev " + interfaceName;
- final String output = runShellCommandOrThrow(cmd);
- final String addressStr = address.getHostAddress();
- for (final String line : output.split("\\n")) {
- if (line.contains(addressStr)) {
- return true;
- }
- }
- return false;
- }
-
- public static List<LinkAddress> getIpv6LinkAddresses(String interfaceName) {
- List<LinkAddress> addresses = new ArrayList<>();
- final String cmd = " ip -6 addr show dev " + interfaceName;
- final String output = runShellCommandOrThrow(cmd);
-
- for (final String line : output.split("\\n")) {
- if (line.contains("inet6")) {
- addresses.add(parseAddressLine(line));
- }
- }
-
- return addresses;
- }
-
- /** Return the first discovered service of {@code serviceType}. */
- public static NsdServiceInfo discoverService(NsdManager nsdManager, String serviceType)
- throws Exception {
- CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
- NsdManager.DiscoveryListener listener =
- new DefaultDiscoveryListener() {
- @Override
- public void onServiceFound(NsdServiceInfo serviceInfo) {
- serviceInfoFuture.complete(serviceInfo);
- }
- };
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
- try {
- serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
- } finally {
- nsdManager.stopServiceDiscovery(listener);
- }
-
- return serviceInfoFuture.get();
- }
-
- /**
- * Returns the {@link NsdServiceInfo} when a service instance of {@code serviceType} gets lost.
- */
- public static NsdManager.DiscoveryListener discoverForServiceLost(
- NsdManager nsdManager,
- String serviceType,
- CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
- NsdManager.DiscoveryListener listener =
- new DefaultDiscoveryListener() {
- @Override
- public void onServiceLost(NsdServiceInfo serviceInfo) {
- serviceInfoFuture.complete(serviceInfo);
- }
- };
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
- return listener;
- }
-
- /** Resolves the service. */
- public static NsdServiceInfo resolveService(NsdManager nsdManager, NsdServiceInfo serviceInfo)
- throws Exception {
- return resolveServiceUntil(nsdManager, serviceInfo, s -> true);
- }
-
- /** Returns the first resolved service that satisfies the {@code predicate}. */
- public static NsdServiceInfo resolveServiceUntil(
- NsdManager nsdManager, NsdServiceInfo serviceInfo, Predicate<NsdServiceInfo> predicate)
- throws Exception {
- CompletableFuture<NsdServiceInfo> resolvedServiceInfoFuture = new CompletableFuture<>();
- NsdManager.ServiceInfoCallback callback =
- new DefaultServiceInfoCallback() {
- @Override
- public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
- if (predicate.test(serviceInfo)) {
- resolvedServiceInfoFuture.complete(serviceInfo);
- }
- }
- };
- nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback);
- try {
- return resolvedServiceInfoFuture.get(
- SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
- } finally {
- nsdManager.unregisterServiceInfoCallback(callback);
- }
- }
-
- 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) {}
-
- @Override
- public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
-
- @Override
- public void onDiscoveryStarted(String serviceType) {}
-
- @Override
- public void onDiscoveryStopped(String serviceType) {}
-
- @Override
- public void onServiceFound(NsdServiceInfo serviceInfo) {}
-
- @Override
- public void onServiceLost(NsdServiceInfo serviceInfo) {}
- }
-
- private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
- @Override
- public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
-
- @Override
- public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
-
- @Override
- public void onServiceLost() {}
-
- @Override
- public void onServiceInfoCallbackUnregistered() {}
- }
-
- /**
- * Parses a line of output from "ip -6 addr show" into a {@link LinkAddress}.
- *
- * <p>Example line: "inet6 2001:db8:1:1::1/64 scope global deprecated"
- */
- private static LinkAddress parseAddressLine(String line) {
- String[] parts = line.trim().split("\\s+");
- String addressString = parts[1];
- String[] pieces = addressString.split("/", 2);
- int prefixLength = Integer.parseInt(pieces[1]);
- final InetAddress address = InetAddresses.parseNumericAddress(pieces[0]);
- long deprecationTimeMillis =
- line.contains("deprecated")
- ? SystemClock.elapsedRealtime()
- : LinkAddress.LIFETIME_PERMANENT;
-
- return new LinkAddress(
- address,
- prefixLength,
- 0 /* flags */,
- 0 /* scope */,
- deprecationTimeMillis,
- LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
- }
-}
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
new file mode 100644
index 0000000..07d0390
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -0,0 +1,659 @@
+/*
+ * 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 android.net.thread.utils
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.MacAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.RouteInfo
+import android.net.TestNetworkInterface
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdServiceInfo
+import android.net.thread.ActiveOperationalDataset
+import android.net.thread.ThreadConfiguration
+import android.net.thread.ThreadNetworkController
+import android.os.Build
+import android.os.Handler
+import android.os.SystemClock
+import android.system.OsConstants
+import android.system.OsConstants.IPPROTO_ICMP
+import androidx.test.core.app.ApplicationProvider
+import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import com.android.net.module.util.IpUtils
+import com.android.net.module.util.NetworkStackConstants
+import com.android.net.module.util.NetworkStackConstants.ICMP_CHECKSUM_OFFSET
+import com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET
+import com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET
+import com.android.net.module.util.Struct
+import com.android.net.module.util.structs.Icmpv4Header
+import com.android.net.module.util.structs.Icmpv6Header
+import com.android.net.module.util.structs.Ipv4Header
+import com.android.net.module.util.structs.Ipv6Header
+import com.android.net.module.util.structs.PrefixInformationOption
+import com.android.net.module.util.structs.RaHeader
+import com.android.testutils.PollPacketReader
+import com.android.testutils.TestNetworkTracker
+import com.android.testutils.initTestNetwork
+import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
+import com.google.common.io.BaseEncoding
+import com.google.common.util.concurrent.MoreExecutors
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import com.google.common.util.concurrent.SettableFuture
+import java.io.IOException
+import java.lang.Byte.toUnsignedInt
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.Inet4Address
+import java.net.Inet6Address
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.SocketAddress
+import java.nio.ByteBuffer
+import java.time.Duration
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import java.util.function.Predicate
+import java.util.function.Supplier
+import org.junit.Assert
+
+/** Utilities for Thread integration tests. */
+object IntegrationTestUtils {
+ // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
+ // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
+ // seconds to be safe
+ @JvmField
+ val RESTART_JOIN_TIMEOUT: Duration = Duration.ofSeconds(40)
+
+ @JvmField
+ val JOIN_TIMEOUT: Duration = Duration.ofSeconds(30)
+
+ @JvmField
+ val LEAVE_TIMEOUT: Duration = Duration.ofSeconds(2)
+
+ @JvmField
+ val CALLBACK_TIMEOUT: Duration = Duration.ofSeconds(1)
+
+ @JvmField
+ val SERVICE_DISCOVERY_TIMEOUT: Duration = Duration.ofSeconds(20)
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+ private val DEFAULT_DATASET_TLVS: ByteArray = BaseEncoding.base16().decode(
+ ("0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8")
+ )
+
+ @JvmField
+ val DEFAULT_DATASET: ActiveOperationalDataset =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS)
+
+ @JvmField
+ val DEFAULT_CONFIG = ThreadConfiguration.Builder().build()
+
+ /**
+ * Waits for the given [Supplier] to be true until given timeout.
+ *
+ * @param condition the condition to check
+ * @param timeout the time to wait for the condition before throwing
+ * @throws TimeoutException if the condition is still not met when the timeout expires
+ */
+ @JvmStatic
+ @Throws(TimeoutException::class)
+ fun waitFor(condition: Supplier<Boolean>, timeout: Duration) {
+ val intervalMills: Long = 500
+ val timeoutMills = timeout.toMillis()
+
+ var i: Long = 0
+ while (i < timeoutMills) {
+ if (condition.get()) {
+ return
+ }
+ SystemClock.sleep(intervalMills)
+ i += intervalMills
+ }
+ if (condition.get()) {
+ return
+ }
+ throw TimeoutException("The condition failed to become true in $timeout")
+ }
+
+ /**
+ * Creates a [PollPacketReader] given the [TestNetworkInterface] and [Handler].
+ *
+ * @param testNetworkInterface the TUN interface of the test network
+ * @param handler the handler to process the packets
+ * @return the [PollPacketReader]
+ */
+ @JvmStatic
+ fun newPacketReader(
+ testNetworkInterface: TestNetworkInterface, handler: Handler
+ ): PollPacketReader {
+ val fd = testNetworkInterface.fileDescriptor.fileDescriptor
+ val reader = PollPacketReader(handler, fd, testNetworkInterface.mtu)
+ handler.post { reader.start() }
+ handler.waitForIdle(timeoutMs = 5000)
+ return reader
+ }
+
+ /**
+ * Waits for the Thread module to enter any state of the given `deviceRoles`.
+ *
+ * @param controller the [ThreadNetworkController]
+ * @param deviceRoles the desired device roles. See also [ ]
+ * @param timeout the time to wait for the expected state before throwing
+ * @return the [ThreadNetworkController.DeviceRole] after waiting
+ * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
+ * expires
+ */
+ @JvmStatic
+ @Throws(TimeoutException::class)
+ fun waitForStateAnyOf(
+ controller: ThreadNetworkController, deviceRoles: List<Int>, timeout: Duration
+ ): Int {
+ val future = SettableFuture.create<Int>()
+ val callback = ThreadNetworkController.StateCallback { newRole: Int ->
+ if (deviceRoles.contains(newRole)) {
+ future.set(newRole)
+ }
+ }
+ controller.registerStateCallback(MoreExecutors.directExecutor(), callback)
+ try {
+ return future[timeout.toMillis(), TimeUnit.MILLISECONDS]
+ } catch (e: InterruptedException) {
+ throw TimeoutException(
+ "The device didn't become an expected role in $timeout: $e.message"
+ )
+ } catch (e: ExecutionException) {
+ throw TimeoutException(
+ "The device didn't become an expected role in $timeout: $e.message"
+ )
+ } finally {
+ controller.unregisterStateCallback(callback)
+ }
+ }
+
+ /**
+ * Polls for a packet from a given [PollPacketReader] that satisfies the `filter`.
+ *
+ * @param packetReader a TUN packet reader
+ * @param filter the filter to be applied on the packet
+ * @return the first IPv6 packet that satisfies the `filter`. If it has waited for more
+ * than 3000ms to read the next packet, the method will return null
+ */
+ @JvmStatic
+ fun pollForPacket(packetReader: PollPacketReader, filter: Predicate<ByteArray>): ByteArray? {
+ var packet: ByteArray?
+ while ((packetReader.poll(3000 /* timeoutMs */, filter).also { packet = it }) != null) {
+ return packet
+ }
+ return null
+ }
+
+ /** Returns `true` if `packet` is an ICMPv4 packet of given `type`. */
+ @JvmStatic
+ fun isExpectedIcmpv4Packet(packet: ByteArray, type: Int): Boolean {
+ val buf = makeByteBuffer(packet)
+ val header = extractIpv4Header(buf) ?: return false
+ if (header.protocol != OsConstants.IPPROTO_ICMP.toByte()) {
+ return false
+ }
+ try {
+ return Struct.parse(Icmpv4Header::class.java, buf).type == type.toShort()
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false
+ }
+
+ /** Returns `true` if `packet` is an ICMPv6 packet of given `type`. */
+ @JvmStatic
+ fun isExpectedIcmpv6Packet(packet: ByteArray, type: Int): Boolean {
+ val buf = makeByteBuffer(packet)
+ val header = extractIpv6Header(buf) ?: return false
+ if (header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
+ return false
+ }
+ try {
+ return Struct.parse(Icmpv6Header::class.java, buf).type == type.toShort()
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false
+ }
+
+ @JvmStatic
+ fun isFrom(packet: ByteArray, src: InetAddress): Boolean {
+ when (src) {
+ is Inet4Address -> return isFromIpv4Source(packet, src)
+ is Inet6Address -> return isFromIpv6Source(packet, src)
+ else -> return false
+ }
+ }
+
+ @JvmStatic
+ fun isTo(packet: ByteArray, dest: InetAddress): Boolean {
+ when (dest) {
+ is Inet4Address -> return isToIpv4Destination(packet, dest)
+ is Inet6Address -> return isToIpv6Destination(packet, dest)
+ else -> return false
+ }
+ }
+
+ private fun isFromIpv4Source(packet: ByteArray, src: Inet4Address): Boolean {
+ val header = extractIpv4Header(makeByteBuffer(packet))
+ return header?.srcIp == src
+ }
+
+ private fun isFromIpv6Source(packet: ByteArray, src: Inet6Address): Boolean {
+ val header = extractIpv6Header(makeByteBuffer(packet))
+ return header?.srcIp == src
+ }
+
+ private fun isToIpv4Destination(packet: ByteArray, dest: Inet4Address): Boolean {
+ val header = extractIpv4Header(makeByteBuffer(packet))
+ return header?.dstIp == dest
+ }
+
+ private fun isToIpv6Destination(packet: ByteArray, dest: Inet6Address): Boolean {
+ val header = extractIpv6Header(makeByteBuffer(packet))
+ return header?.dstIp == dest
+ }
+
+ private fun makeByteBuffer(packet: ByteArray): ByteBuffer {
+ return ByteBuffer.wrap(packet)
+ }
+
+ private fun extractIpv4Header(buf: ByteBuffer): Ipv4Header? {
+ try {
+ return Struct.parse(Ipv4Header::class.java, buf)
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return null
+ }
+
+ private fun extractIpv6Header(buf: ByteBuffer): Ipv6Header? {
+ try {
+ return Struct.parse(Ipv6Header::class.java, buf)
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return null
+ }
+
+ /** Builds an ICMPv4 Echo Reply packet to respond to the given ICMPv4 Echo Request packet. */
+ @JvmStatic
+ fun buildIcmpv4EchoReply(request: ByteBuffer): ByteBuffer? {
+ val requestIpv4Header = Struct.parse(Ipv4Header::class.java, request) ?: return null
+ val requestIcmpv4Header = Struct.parse(Icmpv4Header::class.java, request) ?: return null
+
+ val id = request.getShort()
+ val seq = request.getShort()
+
+ val payload = ByteBuffer.allocate(4 + request.limit() - request.position())
+ payload.putShort(id)
+ payload.putShort(seq)
+ payload.put(request)
+ payload.rewind()
+
+ val ipv4HeaderLen = Struct.getSize(Ipv4Header::class.java)
+ val Icmpv4HeaderLen = Struct.getSize(Icmpv4Header::class.java)
+ val payloadLen = payload.limit();
+
+ val reply = ByteBuffer.allocate(ipv4HeaderLen + Icmpv4HeaderLen + payloadLen)
+
+ // IPv4 header
+ val replyIpv4Header = Ipv4Header(
+ 0 /* TYPE OF SERVICE */,
+ 0.toShort().toInt()/* totalLength, calculate later */,
+ requestIpv4Header.id,
+ requestIpv4Header.flagsAndFragmentOffset,
+ 0x40 /* ttl */,
+ IPPROTO_ICMP.toByte(),
+ 0.toShort()/* checksum, calculate later */,
+ requestIpv4Header.dstIp /* srcIp */,
+ requestIpv4Header.srcIp /* dstIp */
+ )
+ replyIpv4Header.writeToByteBuffer(reply)
+
+ // ICMPv4 header
+ val replyIcmpv4Header = Icmpv4Header(
+ 0 /* type, ICMP_ECHOREPLY */,
+ requestIcmpv4Header.code,
+ 0.toShort() /* checksum, calculate later */
+ )
+ replyIcmpv4Header.writeToByteBuffer(reply)
+
+ // Payload
+ reply.put(payload)
+ reply.flip()
+
+ // Populate the IPv4 totalLength field.
+ reply.putShort(
+ IPV4_LENGTH_OFFSET, (ipv4HeaderLen + Icmpv4HeaderLen + payloadLen).toShort()
+ )
+
+ // Populate the IPv4 header checksum field.
+ reply.putShort(
+ IPV4_CHECKSUM_OFFSET, IpUtils.ipChecksum(reply, 0 /* headerOffset */)
+ )
+
+ // Populate the ICMP checksum field.
+ reply.putShort(
+ IPV4_HEADER_MIN_LEN + ICMP_CHECKSUM_OFFSET, IpUtils.icmpChecksum(
+ reply, IPV4_HEADER_MIN_LEN, Icmpv4HeaderLen + payloadLen
+ )
+ )
+
+ return reply
+ }
+
+ /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
+ @JvmStatic
+ fun getRaPios(raMsg: ByteArray?): List<PrefixInformationOption> {
+ val pioList = ArrayList<PrefixInformationOption>()
+
+ raMsg ?: return pioList
+
+ val buf = ByteBuffer.wrap(raMsg)
+ val ipv6Header = Struct.parse(Ipv6Header::class.java, buf)
+ if (ipv6Header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
+ return pioList
+ }
+
+ val icmpv6Header = Struct.parse(Icmpv6Header::class.java, buf)
+ if (icmpv6Header.type != NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT.toShort()) {
+ return pioList
+ }
+
+ Struct.parse(RaHeader::class.java, buf)
+ while (buf.position() < raMsg.size) {
+ val currentPos = buf.position()
+ val type = toUnsignedInt(buf.get())
+ val length = toUnsignedInt(buf.get())
+ if (type == NetworkStackConstants.ICMPV6_ND_OPTION_PIO) {
+ val pioBuf = ByteBuffer.wrap(
+ buf.array(), currentPos, Struct.getSize(PrefixInformationOption::class.java)
+ )
+ val pio = Struct.parse(PrefixInformationOption::class.java, pioBuf)
+ pioList.add(pio)
+
+ // Move ByteBuffer position to the next option.
+ buf.position(
+ currentPos + Struct.getSize(PrefixInformationOption::class.java)
+ )
+ } else {
+ // The length is in units of 8 octets.
+ buf.position(currentPos + (length * 8))
+ }
+ }
+ return pioList
+ }
+
+ /**
+ * Sends a UDP message to a destination.
+ *
+ * @param dstAddress the IP address of the destination
+ * @param dstPort the port of the destination
+ * @param message the message in UDP payload
+ * @throws IOException if failed to send the message
+ */
+ @JvmStatic
+ @Throws(IOException::class)
+ fun sendUdpMessage(dstAddress: InetAddress, dstPort: Int, message: String) {
+ val dstSockAddr: SocketAddress = InetSocketAddress(dstAddress, dstPort)
+
+ DatagramSocket().use { socket ->
+ socket.connect(dstSockAddr)
+ val msgBytes = message.toByteArray()
+ val packet = DatagramPacket(msgBytes, msgBytes.size)
+ socket.send(packet)
+ }
+ }
+
+ @JvmStatic
+ fun isInMulticastGroup(interfaceName: String, address: Inet6Address): Boolean {
+ val cmd = "ip -6 maddr show dev $interfaceName"
+ val output: String = runShellCommandOrThrow(cmd)
+ val addressStr = address.hostAddress
+ for (line in output.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
+ if (line.contains(addressStr)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ @JvmStatic
+ fun getIpv6LinkAddresses(interfaceName: String): List<LinkAddress> {
+ val addresses: MutableList<LinkAddress> = ArrayList()
+ val cmd = " ip -6 addr show dev $interfaceName"
+ val output: String = runShellCommandOrThrow(cmd)
+
+ for (line in output.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
+ if (line.contains("inet6")) {
+ addresses.add(parseAddressLine(line))
+ }
+ }
+
+ return addresses
+ }
+
+ /** Return the first discovered service of `serviceType`. */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun discoverService(nsdManager: NsdManager, serviceType: String): NsdServiceInfo {
+ val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
+ val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
+ override fun onServiceFound(serviceInfo: NsdServiceInfo) {
+ serviceInfoFuture.complete(serviceInfo)
+ }
+ }
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
+ try {
+ serviceInfoFuture[SERVICE_DISCOVERY_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS]
+ } finally {
+ nsdManager.stopServiceDiscovery(listener)
+ }
+
+ return serviceInfoFuture.get()
+ }
+
+ /**
+ * Returns the [NsdServiceInfo] when a service instance of `serviceType` gets lost.
+ */
+ @JvmStatic
+ fun discoverForServiceLost(
+ nsdManager: NsdManager,
+ serviceType: String?,
+ serviceInfoFuture: CompletableFuture<NsdServiceInfo?>
+ ): NsdManager.DiscoveryListener {
+ val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
+ override fun onServiceLost(serviceInfo: NsdServiceInfo): Unit {
+ serviceInfoFuture.complete(serviceInfo)
+ }
+ }
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
+ return listener
+ }
+
+ /** Resolves the service. */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun resolveService(nsdManager: NsdManager, serviceInfo: NsdServiceInfo): NsdServiceInfo {
+ return resolveServiceUntil(nsdManager, serviceInfo) { true }
+ }
+
+ /** Returns the first resolved service that satisfies the `predicate`. */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun resolveServiceUntil(
+ nsdManager: NsdManager, serviceInfo: NsdServiceInfo, predicate: Predicate<NsdServiceInfo>
+ ): NsdServiceInfo {
+ val resolvedServiceInfoFuture = CompletableFuture<NsdServiceInfo>()
+ val callback: NsdManager.ServiceInfoCallback = object : DefaultServiceInfoCallback() {
+ override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
+ if (predicate.test(serviceInfo)) {
+ resolvedServiceInfoFuture.complete(serviceInfo)
+ }
+ }
+ }
+ nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback)
+ try {
+ return resolvedServiceInfoFuture[
+ SERVICE_DISCOVERY_TIMEOUT.toMillis(),
+ TimeUnit.MILLISECONDS]
+ } finally {
+ nsdManager.unregisterServiceInfoCallback(callback)
+ }
+ }
+
+ @JvmStatic
+ fun getPrefixesFromNetData(netData: String): String {
+ val startIdx = netData.indexOf("Prefixes:")
+ val endIdx = netData.indexOf("Routes:")
+ return netData.substring(startIdx, endIdx)
+ }
+
+ @JvmStatic
+ @Throws(Exception::class)
+ fun getThreadNetwork(timeout: Duration): Network {
+ val networkFuture = CompletableFuture<Network>()
+ val cm =
+ ApplicationProvider.getApplicationContext<Context>()
+ .getSystemService(ConnectivityManager::class.java)
+ val networkRequestBuilder =
+ 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(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+ }
+ val networkRequest = networkRequestBuilder.build()
+ val networkCallback: ConnectivityManager.NetworkCallback =
+ object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ networkFuture.complete(network)
+ }
+ }
+ cm.registerNetworkCallback(networkRequest, networkCallback)
+ return networkFuture[timeout.toSeconds(), TimeUnit.SECONDS]
+ }
+
+ /**
+ * Let the FTD join the specified Thread network and wait for border routing to be available.
+ *
+ * @return the OMR address
+ */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun joinNetworkAndWaitForOmr(
+ ftd: FullThreadDevice, dataset: ActiveOperationalDataset
+ ): Inet6Address {
+ ftd.factoryReset()
+ ftd.joinNetwork(dataset)
+ ftd.waitForStateAnyOf(listOf("router", "child"), JOIN_TIMEOUT)
+ waitFor({ ftd.omrAddress != null }, Duration.ofSeconds(60))
+ Assert.assertNotNull(ftd.omrAddress)
+ return ftd.omrAddress
+ }
+
+ /** Enables Thread and joins the specified Thread network. */
+ @JvmStatic
+ fun enableThreadAndJoinNetwork(dataset: ActiveOperationalDataset) {
+ // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
+ OtDaemonController().factoryReset();
+
+ val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
+ val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
+ controller.setEnabledAndWait(true);
+ controller.joinAndWait(dataset);
+ }
+
+ /** Leaves the Thread network and disables Thread. */
+ @JvmStatic
+ fun leaveNetworkAndDisableThread() {
+ val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
+ val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
+ controller.leaveAndWait();
+ controller.setEnabledAndWait(false);
+ }
+
+ private open class DefaultDiscoveryListener : NsdManager.DiscoveryListener {
+ override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {}
+ override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {}
+ override fun onDiscoveryStarted(serviceType: String) {}
+ override fun onDiscoveryStopped(serviceType: String) {}
+ override fun onServiceFound(serviceInfo: NsdServiceInfo) {}
+ override fun onServiceLost(serviceInfo: NsdServiceInfo) {}
+ }
+
+ private open class DefaultServiceInfoCallback : NsdManager.ServiceInfoCallback {
+ override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {}
+ override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {}
+ override fun onServiceLost(): Unit {}
+ override fun onServiceInfoCallbackUnregistered() {}
+ }
+
+ /**
+ * Parses a line of output from "ip -6 addr show" into a [LinkAddress].
+ *
+ * Example line: "inet6 2001:db8:1:1::1/64 scope global deprecated"
+ */
+ private fun parseAddressLine(line: String): LinkAddress {
+ val parts = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }.toTypedArray()
+ val addressString = parts[1]
+ val pieces = addressString.split("/".toRegex(), limit = 2).toTypedArray()
+ val prefixLength = pieces[1].toInt()
+ val address = parseNumericAddress(pieces[0])
+ val deprecationTimeMillis =
+ if (line.contains("deprecated")) SystemClock.elapsedRealtime()
+ else LinkAddress.LIFETIME_PERMANENT
+
+ return LinkAddress(
+ address, prefixLength,
+ 0 /* flags */, 0 /* scope */,
+ deprecationTimeMillis, LinkAddress.LIFETIME_PERMANENT /* expirationTime */
+ )
+ }
+
+ /**
+ * Stop the ot-daemon by shell command.
+ */
+ @JvmStatic
+ fun stopOtDaemon() {
+ runShellCommandOrThrow("stop ot-daemon")
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
index b3175fd..9fbfa45 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -54,6 +54,16 @@
SystemClock.sleep(500);
}
+ /** Returns the output string of the "ot-ctl br state" command. */
+ public String getBorderRoutingState() {
+ return executeCommandAndParse("br state").getFirst();
+ }
+
+ /** Returns the output string of the "ot-ctl srp server state" command. */
+ public String getSrpServerState() {
+ return executeCommandAndParse("srp server state").getFirst();
+ }
+
/** Returns the list of IPv6 addresses on ot-daemon. */
public List<Inet6Address> getAddresses() {
return executeCommandAndParse("ipaddr").stream()
@@ -105,6 +115,47 @@
return prefixes.isEmpty() ? null : prefixes.get(0);
}
+ /** Enables/Disables NAT64 feature. */
+ public void setNat64Enabled(boolean enabled) {
+ executeCommand("nat64 " + (enabled ? "enable" : "disable"));
+ }
+
+ /** Sets the NAT64 CIDR. */
+ public void setNat64Cidr(String cidr) {
+ executeCommand("nat64 cidr " + cidr);
+ }
+
+ /** Returns whether there's a NAT64 prefix in network data */
+ public boolean hasNat64PrefixInNetdata() {
+ // Example (in the 'Routes' section):
+ // fdb2:bae3:5b59:2:0:0::/96 sn low c000
+ List<String> outputLines = executeCommandAndParse("netdata show");
+ for (String line : outputLines) {
+ if (line.contains(" sn")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Adds a prefix in the Network Data. */
+ public void addPrefixInNetworkData(IpPrefix ipPrefix, String flags, String preference) {
+ executeCommand("prefix add " + ipPrefix + " " + flags + " " + preference);
+ executeCommand("netdata register");
+ }
+
+ public int getTrelPort() {
+ return Integer.parseInt(executeCommandAndParse("trel port").get(0));
+ }
+
+ public String getExtendedAddr() {
+ return executeCommandAndParse("extaddr").get(0);
+ }
+
+ public String getExtendedPanId() {
+ return executeCommandAndParse("extpanid").get(0);
+ }
+
public String executeCommand(String cmd) {
return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestDnsServer.kt b/thread/tests/integration/src/android/net/thread/utils/TestDnsServer.kt
new file mode 100644
index 0000000..f97c0f2
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/TestDnsServer.kt
@@ -0,0 +1,132 @@
+/*
+ * 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 android.net.thread.utils
+
+import android.system.OsConstants.IPPROTO_IP
+import android.system.OsConstants.IPPROTO_UDP
+import com.android.net.module.util.DnsPacket
+import com.android.net.module.util.PacketBuilder
+import com.android.net.module.util.structs.Ipv4Header
+import com.android.net.module.util.structs.UdpHeader
+import com.android.testutils.PollPacketReader
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+
+/**
+ * A class that simulates a DNS server.
+ *
+ * <p>The server responds to DNS requests with the given {@code answerRecords}.
+ *
+ * @param packetReader the packet reader to poll DNS requests from
+ * @param serverAddress the address of the DNS server
+ * @param answerRecords the records to respond to the DNS requests
+ */
+class TestDnsServer(
+ private val packetReader: PollPacketReader,
+ private val serverAddress: InetAddress,
+ private val serverAnswers: List<DnsPacket.DnsRecord>,
+) : TestUdpServer(packetReader, InetSocketAddress(serverAddress, DNS_UDP_PORT)) {
+ companion object {
+ private val TAG = TestDnsServer::class.java.simpleName
+ private const val DNS_UDP_PORT = 53
+ }
+
+ private class TestDnsPacket : DnsPacket {
+
+ constructor(buf: ByteArray) : super(buf)
+
+ constructor(
+ header: DnsHeader,
+ qd: List<DnsRecord>,
+ an: List<DnsRecord>,
+ ) : super(header, qd, an) {}
+
+ val header = super.mHeader
+ val records = super.mRecords
+ }
+
+ override fun buildResponse(
+ requestIpv4Header: Ipv4Header,
+ requestUdpHeader: UdpHeader,
+ requestUdpPayload: ByteArray,
+ ): ByteBuffer? {
+ val requestDnsPacket = TestDnsPacket(requestUdpPayload)
+ val requestDnsHeader = requestDnsPacket.header
+
+ val answerRecords =
+ buildDnsAnswerRecords(requestDnsPacket.records[DnsPacket.QDSECTION], serverAnswers)
+ // TODO: return NXDOMAIN if no answer is found.
+ val responseFlags = 1 shl 15 // QR bit
+ val responseDnsHeader =
+ DnsPacket.DnsHeader(
+ requestDnsHeader.id,
+ responseFlags,
+ requestDnsPacket.records[DnsPacket.QDSECTION].size,
+ answerRecords.size,
+ )
+ val responseDnsPacket =
+ TestDnsPacket(
+ responseDnsHeader,
+ requestDnsPacket.records[DnsPacket.QDSECTION],
+ answerRecords,
+ )
+
+ val buf =
+ PacketBuilder.allocate(
+ false /* hasEther */,
+ IPPROTO_IP,
+ IPPROTO_UDP,
+ responseDnsPacket.bytes.size,
+ )
+
+ val packetBuilder = PacketBuilder(buf)
+ packetBuilder.writeIpv4Header(
+ requestIpv4Header.tos,
+ requestIpv4Header.id,
+ requestIpv4Header.flagsAndFragmentOffset,
+ 0x40 /* ttl */,
+ IPPROTO_UDP.toByte(),
+ requestIpv4Header.dstIp, /* srcIp */
+ requestIpv4Header.srcIp, /* dstIp */
+ )
+ packetBuilder.writeUdpHeader(
+ requestUdpHeader.dstPort.toShort() /* srcPort */,
+ requestUdpHeader.srcPort.toShort(), /* dstPort */
+ )
+ buf.put(responseDnsPacket.bytes)
+
+ return packetBuilder.finalizePacket()
+ }
+
+ private fun buildDnsAnswerRecords(
+ questions: List<DnsPacket.DnsRecord>,
+ serverAnswers: List<DnsPacket.DnsRecord>,
+ ): List<DnsPacket.DnsRecord> {
+ val answers = ArrayList<DnsPacket.DnsRecord>()
+ for (answer in serverAnswers) {
+ if (
+ questions.any {
+ answer.dName.equals(it.dName, ignoreCase = true) && answer.nsType == it.nsType
+ }
+ ) {
+ answers.add(answer)
+ }
+ }
+ return answers
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestTunNetworkUtils.kt b/thread/tests/integration/src/android/net/thread/utils/TestTunNetworkUtils.kt
new file mode 100644
index 0000000..1667980
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/TestTunNetworkUtils.kt
@@ -0,0 +1,93 @@
+/*
+ * 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 android.net.thread.utils
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.LinkProperties
+import android.net.MacAddress
+import android.net.RouteInfo
+import com.android.testutils.PollPacketReader
+import com.android.testutils.TestNetworkTracker
+import com.android.testutils.initTestNetwork
+import com.android.testutils.runAsShell
+import java.time.Duration
+
+object TestTunNetworkUtils {
+ private val networkTrackers = mutableListOf<TestNetworkTracker>()
+
+ @JvmStatic
+ @JvmOverloads
+ fun setUpInfraNetwork(
+ context: Context,
+ controller: ThreadNetworkControllerWrapper,
+ lp: LinkProperties = defaultLinkProperties(),
+ ): TestNetworkTracker {
+ val infraNetworkTracker: TestNetworkTracker =
+ runAsShell(
+ MANAGE_TEST_NETWORKS,
+ supplier = { initTestNetwork(context, lp, setupTimeoutMs = 5000) },
+ )
+ val infraNetworkName: String = infraNetworkTracker.testIface.getInterfaceName()
+ controller.setTestNetworkAsUpstreamAndWait(infraNetworkName)
+ networkTrackers.add(infraNetworkTracker)
+
+ return infraNetworkTracker
+ }
+
+ @JvmStatic
+ fun tearDownInfraNetwork(testNetworkTracker: TestNetworkTracker) {
+ runAsShell(MANAGE_TEST_NETWORKS) { testNetworkTracker.teardown() }
+ }
+
+ @JvmStatic
+ fun tearDownAllInfraNetworks() {
+ networkTrackers.forEach { tearDownInfraNetwork(it) }
+ networkTrackers.clear()
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ fun startInfraDeviceAndWaitForOnLinkAddr(
+ pollPacketReader: PollPacketReader,
+ macAddress: MacAddress = MacAddress.fromString("1:2:3:4:5:6"),
+ ): InfraNetworkDevice {
+ val infraDevice = InfraNetworkDevice(macAddress, pollPacketReader)
+ infraDevice.runSlaac(Duration.ofSeconds(60))
+ requireNotNull(infraDevice.ipv6Addr)
+ return infraDevice
+ }
+
+ private fun defaultLinkProperties(): LinkProperties {
+ val lp = LinkProperties()
+ // TODO: use a fake DNS server
+ lp.setDnsServers(listOf(parseNumericAddress("8.8.8.8")))
+ // NAT64 feature requires the infra network to have an IPv4 default route.
+ lp.addRoute(
+ RouteInfo(
+ IpPrefix("0.0.0.0/0") /* destination */,
+ null /* gateway */,
+ null /* iface */,
+ RouteInfo.RTN_UNICAST,
+ 1500, /* mtu */
+ )
+ )
+ return lp
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestUdpEchoServer.kt b/thread/tests/integration/src/android/net/thread/utils/TestUdpEchoServer.kt
new file mode 100644
index 0000000..9fcd6a4
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/TestUdpEchoServer.kt
@@ -0,0 +1,74 @@
+/*
+ * 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 android.net.thread.utils
+
+import android.system.OsConstants.IPPROTO_IP
+import android.system.OsConstants.IPPROTO_UDP
+import com.android.net.module.util.PacketBuilder
+import com.android.net.module.util.structs.Ipv4Header
+import com.android.net.module.util.structs.UdpHeader
+import com.android.testutils.PollPacketReader
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+
+/**
+ * A class that simulates a UDP echo server that replies to incoming UDP message with the same
+ * payload.
+ *
+ * @param packetReader the packet reader to poll UDP requests from
+ * @param serverAddress the address and port of the UDP server
+ */
+class TestUdpEchoServer(
+ private val packetReader: PollPacketReader,
+ private val serverAddress: InetSocketAddress,
+) : TestUdpServer(packetReader, serverAddress) {
+ companion object {
+ private val TAG = TestUdpEchoServer::class.java.simpleName
+ }
+
+ override fun buildResponse(
+ requestIpv4Header: Ipv4Header,
+ requestUdpHeader: UdpHeader,
+ requestUdpPayload: ByteArray,
+ ): ByteBuffer? {
+ val buf =
+ PacketBuilder.allocate(
+ false /* hasEther */,
+ IPPROTO_IP,
+ IPPROTO_UDP,
+ requestUdpPayload.size,
+ )
+
+ val packetBuilder = PacketBuilder(buf)
+ packetBuilder.writeIpv4Header(
+ requestIpv4Header.tos,
+ requestIpv4Header.id,
+ requestIpv4Header.flagsAndFragmentOffset,
+ 0x40 /* ttl */,
+ IPPROTO_UDP.toByte(),
+ requestIpv4Header.dstIp, /* srcIp */
+ requestIpv4Header.srcIp, /* dstIp */
+ )
+ packetBuilder.writeUdpHeader(
+ requestUdpHeader.dstPort.toShort() /* srcPort */,
+ requestUdpHeader.srcPort.toShort(), /* dstPort */
+ )
+ buf.put(requestUdpPayload)
+
+ return packetBuilder.finalizePacket()
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestUdpServer.kt b/thread/tests/integration/src/android/net/thread/utils/TestUdpServer.kt
new file mode 100644
index 0000000..fb0942e
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/TestUdpServer.kt
@@ -0,0 +1,98 @@
+/*
+ * 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 android.net.thread.utils
+
+import android.net.thread.utils.IntegrationTestUtils.pollForPacket
+import com.android.net.module.util.Struct
+import com.android.net.module.util.structs.Ipv4Header
+import com.android.net.module.util.structs.UdpHeader
+import com.android.testutils.PollPacketReader
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+import kotlin.concurrent.thread
+
+/**
+ * A class that simulates a UDP server that replies to incoming UDP messages.
+ *
+ * @param packetReader the packet reader to poll UDP requests from
+ * @param serverAddress the address and port of the UDP server
+ */
+abstract class TestUdpServer(
+ private val packetReader: PollPacketReader,
+ private val serverAddress: InetSocketAddress,
+) {
+ private val TAG = TestUdpServer::class.java.simpleName
+ private var workerThread: Thread? = null
+
+ /**
+ * Starts the UDP server to respond to UDP messages.
+ *
+ * <p> The server polls the UDP messages from the {@code packetReader} and responds with a
+ * message built by {@code buildResponse}. The server will automatically stop when it fails to
+ * poll a UDP request within the timeout (3000 ms, as defined in IntegrationTestUtils).
+ */
+ fun start() {
+ workerThread = thread {
+ var requestPacket: ByteArray
+ while (true) {
+ requestPacket = pollForUdpPacket() ?: break
+ val buf = ByteBuffer.wrap(requestPacket)
+ packetReader.sendResponse(buildResponse(buf) ?: break)
+ }
+ }
+ }
+
+ /** Stops the UDP server. */
+ fun stop() {
+ workerThread?.join()
+ }
+
+ /**
+ * Builds the UDP response for the given UDP request.
+ *
+ * @param ipv4Header the IPv4 header of the UDP request
+ * @param udpHeader the UDP header of the UDP request
+ * @param udpPayload the payload of the UDP request
+ * @return the UDP response
+ */
+ abstract fun buildResponse(
+ requestIpv4Header: Ipv4Header,
+ requestUdpHeader: UdpHeader,
+ requestUdpPayload: ByteArray,
+ ): ByteBuffer?
+
+ private fun pollForUdpPacket(): ByteArray? {
+ val filter =
+ fun(packet: ByteArray): Boolean {
+ val buf = ByteBuffer.wrap(packet)
+ val ipv4Header = Struct.parse(Ipv4Header::class.java, buf) ?: return false
+ val udpHeader = Struct.parse(UdpHeader::class.java, buf) ?: return false
+ return ipv4Header.dstIp == serverAddress.address &&
+ udpHeader.dstPort == serverAddress.port
+ }
+ return pollForPacket(packetReader, filter)
+ }
+
+ private fun buildResponse(requestPacket: ByteBuffer): ByteBuffer? {
+ val requestIpv4Header = Struct.parse(Ipv4Header::class.java, requestPacket) ?: return null
+ val requestUdpHeader = Struct.parse(UdpHeader::class.java, requestPacket) ?: return null
+ val remainingRequestPacket = ByteArray(requestPacket.remaining())
+ requestPacket.get(remainingRequestPacket)
+
+ return buildResponse(requestIpv4Header, requestUdpHeader, remainingRequestPacket)
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
index 7e84233..b6114f3 100644
--- a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
+++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
@@ -29,6 +29,7 @@
import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.StateCallback;
import android.net.thread.ThreadNetworkException;
@@ -36,10 +37,12 @@
import android.os.OutcomeReceiver;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
/** A helper class which provides synchronous API wrappers for {@link ThreadNetworkController}. */
public final class ThreadNetworkControllerWrapper {
@@ -47,9 +50,13 @@
public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
private static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2);
+ private static final Duration CONFIG_TIMEOUT = Duration.ofSeconds(1);
private final ThreadNetworkController mController;
+ private final List<Integer> mDeviceRoleUpdates = new ArrayList<>();
+ @Nullable private StateCallback mStateCallback;
+
/**
* Returns a new {@link ThreadNetworkControllerWrapper} instance or {@code null} if Thread
* feature is not supported on this device.
@@ -68,6 +75,15 @@
}
/**
+ * Returns the underlying {@link ThreadNetworkController} object or {@code null} if the current
+ * platform doesn't support it.
+ */
+ @Nullable
+ public ThreadNetworkController get() {
+ return mController;
+ }
+
+ /**
* Returns the Thread enabled state.
*
* <p>The value can be one of {@code ThreadNetworkController#STATE_*}.
@@ -191,6 +207,36 @@
future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
}
+ public ThreadConfiguration getConfiguration() throws Exception {
+ CompletableFuture<ThreadConfiguration> future = new CompletableFuture<>();
+ Consumer<ThreadConfiguration> callback = future::complete;
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mController.registerConfigurationCallback(directExecutor(), callback));
+ future.get(CONFIG_TIMEOUT.toSeconds(), SECONDS);
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mController.unregisterConfigurationCallback(callback));
+ return future.getNow(null);
+ }
+
+ public void setConfigurationAndWait(ThreadConfiguration config) throws Exception {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setConfiguration(
+ config, directExecutor(), newOutcomeReceiver(future)));
+ future.get(CONFIG_TIMEOUT.toSeconds(), SECONDS);
+ }
+
+ public void setNat64EnabledAndWait(boolean enabled) throws Exception {
+ final ThreadConfiguration config = getConfiguration();
+ final ThreadConfiguration newConfig =
+ new ThreadConfiguration.Builder(config).setNat64Enabled(enabled).build();
+ setConfigurationAndWait(newConfig);
+ }
+
private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
CompletableFuture<V> future) {
return new OutcomeReceiver<V, ThreadNetworkException>() {
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 9404d1b..53b1eca 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -35,6 +35,7 @@
static_libs: [
"androidx.test.rules",
"frameworks-base-testutils",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
"framework-location.stubs.module_lib",
@@ -50,10 +51,10 @@
"service-thread-pre-jarjar",
],
libs: [
- "android.test.base",
- "android.test.runner",
+ "android.test.base.stubs.system",
+ "android.test.runner.stubs.system",
"ServiceConnectivityResources",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
],
jni_libs: [
"libservice-thread-jni",
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index ac74372..e3c83f1 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -17,15 +17,18 @@
package android.net.thread;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
-import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
import static android.os.Process.SYSTEM_UID;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
@@ -134,6 +137,24 @@
return (IOperationalDatasetCallback) invocation.getArguments()[0];
}
+ private static IOperationReceiver getActivateEphemeralKeyModeReceiver(
+ InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
+ private static IOperationReceiver getDeactivateEphemeralKeyModeReceiver(
+ InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[0];
+ }
+
+ private static IOperationReceiver getSetConfigurationReceiver(InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
+ private static IConfigurationReceiver getConfigurationReceiver(InvocationOnMock invocation) {
+ return (IConfigurationReceiver) invocation.getArguments()[0];
+ }
+
@Test
public void registerStateCallback_callbackIsInvokedWithCallingAppIdentity() throws Exception {
setBinderUid(SYSTEM_UID);
@@ -394,7 +415,7 @@
doAnswer(
invoke -> {
getSetChannelMaxPowersReceiver(invoke)
- .onError(ERROR_UNSUPPORTED_OPERATION, "");
+ .onError(ERROR_UNSUPPORTED_FEATURE, "");
return null;
})
.when(mMockService)
@@ -440,4 +461,152 @@
assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
assertThat(callbackUid.get()).isEqualTo(Process.myUid());
}
+
+ @Test
+ public void activateEphemeralKeyMode_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+ Duration lifetime = Duration.ofSeconds(100);
+ doAnswer(
+ invoke -> {
+ getActivateEphemeralKeyModeReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .activateEphemeralKeyMode(anyLong(), any(IOperationReceiver.class));
+ mController.activateEphemeralKeyMode(
+ lifetime, Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getActivateEphemeralKeyModeReceiver(invoke)
+ .onError(ERROR_FAILED_PRECONDITION, "");
+ return null;
+ })
+ .when(mMockService)
+ .activateEphemeralKeyMode(anyLong(), any(IOperationReceiver.class));
+ mController.activateEphemeralKeyMode(
+ lifetime,
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+
+ @Test
+ public void deactivateEphemeralKeyMode_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+ doAnswer(
+ invoke -> {
+ getDeactivateEphemeralKeyModeReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .deactivateEphemeralKeyMode(any(IOperationReceiver.class));
+ mController.deactivateEphemeralKeyMode(
+ Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getDeactivateEphemeralKeyModeReceiver(invoke)
+ .onError(ERROR_INTERNAL_ERROR, "");
+ return null;
+ })
+ .when(mMockService)
+ .deactivateEphemeralKeyMode(any(IOperationReceiver.class));
+ mController.deactivateEphemeralKeyMode(
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+
+ @Test
+ public void setConfiguration_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+ doAnswer(
+ invoke -> {
+ getSetConfigurationReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .setConfiguration(any(ThreadConfiguration.class), any(IOperationReceiver.class));
+ mController.setConfiguration(
+ new ThreadConfiguration.Builder().build(),
+ Runnable::run,
+ v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getSetConfigurationReceiver(invoke).onError(ERROR_INTERNAL_ERROR, "");
+ return null;
+ })
+ .when(mMockService)
+ .setConfiguration(any(ThreadConfiguration.class), any(IOperationReceiver.class));
+ mController.setConfiguration(
+ new ThreadConfiguration.Builder().build(),
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+
+ @Test
+ public void registerConfigurationCallback_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger callbackUid = new AtomicInteger(0);
+ doAnswer(
+ invoke -> {
+ getConfigurationReceiver(invoke)
+ .onConfigurationChanged(
+ new ThreadConfiguration.Builder().build());
+ return null;
+ })
+ .when(mMockService)
+ .registerConfigurationCallback(any(IConfigurationReceiver.class));
+
+ mController.registerConfigurationCallback(
+ Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+
+ assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(callbackUid.get()).isEqualTo(Process.myUid());
+ }
}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkSpecifierTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkSpecifierTest.java
new file mode 100644
index 0000000..c83cb7a
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkSpecifierTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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 android.net.thread;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Tests for {@link ThreadNetworkSpecifier}. */
+@SmallTest
+@RunWith(Parameterized.class)
+public final class ThreadNetworkSpecifierTest {
+ public final byte[] mExtendedPanId;
+ public final OperationalDatasetTimestamp mActiveTimestamp;
+ public final boolean mRouterEligibleForLeader;
+
+ @Parameterized.Parameters
+ public static Collection specifierArguments() {
+ var timestampNow = OperationalDatasetTimestamp.fromInstant(Instant.now());
+ return Arrays.asList(
+ new Object[][] {
+ {new byte[] {0, 1, 2, 3, 4, 5, 6, 7}, null, false},
+ {new byte[] {1, 1, 1, 1, 2, 2, 2, 2}, timestampNow, true},
+ {new byte[] {1, 1, 1, 1, 2, 2, 2, 2}, timestampNow, false},
+ });
+ }
+
+ public ThreadNetworkSpecifierTest(
+ byte[] extendedPanId,
+ OperationalDatasetTimestamp activeTimestamp,
+ boolean routerEligibleForLeader) {
+ mExtendedPanId = extendedPanId.clone();
+ mActiveTimestamp = activeTimestamp;
+ mRouterEligibleForLeader = routerEligibleForLeader;
+ }
+
+ @Test
+ public void parcelable_parcelingIsLossLess() {
+ ThreadNetworkSpecifier specifier =
+ new ThreadNetworkSpecifier.Builder(mExtendedPanId)
+ .setActiveTimestamp(mActiveTimestamp)
+ .setRouterEligibleForLeader(mRouterEligibleForLeader)
+ .build();
+ assertParcelingIsLossless(specifier);
+ }
+
+ @Test
+ public void builder_correctValuesAreSet() {
+ ThreadNetworkSpecifier specifier =
+ new ThreadNetworkSpecifier.Builder(mExtendedPanId)
+ .setActiveTimestamp(mActiveTimestamp)
+ .setRouterEligibleForLeader(mRouterEligibleForLeader)
+ .build();
+
+ assertThat(specifier.getExtendedPanId()).isEqualTo(mExtendedPanId);
+ assertThat(specifier.getActiveTimestamp()).isEqualTo(mActiveTimestamp);
+ assertThat(specifier.isRouterEligibleForLeader()).isEqualTo(mRouterEligibleForLeader);
+ }
+
+ @Test
+ public void builderConstructor_specifiersAreEqual() {
+ ThreadNetworkSpecifier specifier1 =
+ new ThreadNetworkSpecifier.Builder(mExtendedPanId)
+ .setActiveTimestamp(mActiveTimestamp)
+ .setRouterEligibleForLeader(mRouterEligibleForLeader)
+ .build();
+
+ ThreadNetworkSpecifier specifier2 = new ThreadNetworkSpecifier.Builder(specifier1).build();
+
+ assertThat(specifier1).isEqualTo(specifier2);
+ }
+
+ @Test
+ public void equalsTester() {
+ var timestampNow = OperationalDatasetTimestamp.fromInstant(Instant.now());
+ new EqualsTester()
+ .addEqualityGroup(
+ new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+ .setActiveTimestamp(timestampNow)
+ .setRouterEligibleForLeader(true)
+ .build(),
+ new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+ .setActiveTimestamp(timestampNow)
+ .setRouterEligibleForLeader(true)
+ .build())
+ .addEqualityGroup(
+ new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+ .setActiveTimestamp(null)
+ .setRouterEligibleForLeader(false)
+ .build(),
+ new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+ .setActiveTimestamp(null)
+ .setRouterEligibleForLeader(false)
+ .build())
+ .addEqualityGroup(
+ new ThreadNetworkSpecifier.Builder(new byte[] {1, 1, 1, 1, 2, 2, 2, 2})
+ .setActiveTimestamp(null)
+ .setRouterEligibleForLeader(false)
+ .build(),
+ new ThreadNetworkSpecifier.Builder(new byte[] {1, 1, 1, 1, 2, 2, 2, 2})
+ .setActiveTimestamp(null)
+ .setRouterEligibleForLeader(false)
+ .build())
+ .testEquals();
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
index b32986d..d52191a 100644
--- a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -34,6 +34,7 @@
import android.net.DnsResolver;
import android.net.InetAddresses;
+import android.net.LinkProperties;
import android.net.Network;
import android.net.nsd.DiscoveryRequest;
import android.net.nsd.NsdManager;
@@ -61,6 +62,7 @@
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -584,6 +586,7 @@
verify(mResolveServiceCallback, times(1))
.onServiceResolved(
eq("test-host"),
+ eq(0),
eq("test"),
eq("_test._tcp"),
eq(12345),
@@ -811,7 +814,10 @@
private void prepareTest() {
mTestLooper = new TestLooper();
Handler handler = new Handler(mTestLooper.getLooper());
- mNsdPublisher = new NsdPublisher(mMockNsdManager, mMockDnsResolver, handler);
+ HashMap<Network, LinkProperties> networkToLinkProperties = new HashMap<>();
+ mNsdPublisher =
+ new NsdPublisher(
+ mMockNsdManager, mMockDnsResolver, handler, networkToLinkProperties);
mNsdPublisher.setNetworkForHostResolution(mNetwork);
}
}
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 eaf11b1..dcbb3f5 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,6 +16,14 @@
package com.android.server.thread;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
@@ -24,9 +32,11 @@
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_TESTING;
import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
@@ -37,7 +47,11 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNotNull;
+import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
@@ -55,11 +69,16 @@
import android.content.Intent;
import android.content.res.Resources;
import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkProvider;
+import android.net.NetworkRequest;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationReceiver;
+import android.net.thread.IOutputReceiver;
import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkException;
import android.os.Handler;
@@ -69,6 +88,7 @@
import android.os.SystemClock;
import android.os.UserManager;
import android.os.test.TestLooper;
+import android.provider.DeviceConfig;
import android.util.AtomicFile;
import androidx.test.annotation.UiThreadTest;
@@ -78,11 +98,15 @@
import com.android.connectivity.resources.R;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.IOtStatusReceiver;
import com.android.server.thread.openthread.MeshcopTxtAttributes;
+import com.android.server.thread.openthread.OtDaemonConfiguration;
import com.android.server.thread.openthread.testing.FakeOtDaemon;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -101,6 +125,7 @@
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
@@ -150,8 +175,10 @@
private static final byte[] TEST_VENDOR_OUI_BYTES = new byte[] {(byte) 0xAC, (byte) 0xDE, 0x48};
private static final String TEST_VENDOR_NAME = "test vendor";
private static final String TEST_MODEL_NAME = "test model";
+ private static final LinkAddress TEST_NAT64_CIDR = new LinkAddress("192.168.255.0/24");
@Mock private ConnectivityManager mMockConnectivityManager;
+ @Mock private RoutingCoordinatorManager mMockRoutingCoordinatorManager;
@Mock private NetworkAgent mMockNetworkAgent;
@Mock private TunInterfaceController mMockTunIfController;
@Mock private ParcelFileDescriptor mMockTunFd;
@@ -161,6 +188,7 @@
@Mock private IBinder mIBinder;
@Mock Resources mResources;
@Mock ConnectivityResources mConnectivityResources;
+ @Mock Map<Network, LinkProperties> mMockNetworkToLinkProperties;
private Context mContext;
private TestLooper mTestLooper;
@@ -181,13 +209,22 @@
.when(mContext)
.enforceCallingOrSelfPermission(
eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), anyString());
+ doNothing()
+ .when(mContext)
+ .enforceCallingOrSelfPermission(eq(PERMISSION_THREAD_NETWORK_TESTING), anyString());
+ doNothing()
+ .when(mContext)
+ .enforceCallingOrSelfPermission(eq(NETWORK_SETTINGS), anyString());
mTestLooper = new TestLooper();
final Handler handler = new Handler(mTestLooper.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
- mFakeOtDaemon = new FakeOtDaemon(handler);
+ when(mMockRoutingCoordinatorManager.requestDownstreamAddress(any()))
+ .thenReturn(TEST_NAT64_CIDR);
+
+ mFakeOtDaemon = spy(new FakeOtDaemon(handler));
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
@@ -214,16 +251,26 @@
networkProvider,
() -> mFakeOtDaemon,
mMockConnectivityManager,
+ mMockRoutingCoordinatorManager,
mMockTunIfController,
mMockInfraIfController,
mPersistentSettings,
mMockNsdPublisher,
mMockUserManager,
mConnectivityResources,
- () -> DEFAULT_COUNTRY_CODE);
+ () -> DEFAULT_COUNTRY_CODE,
+ mMockNetworkToLinkProperties);
mService.setTestNetworkAgent(mMockNetworkAgent);
}
+ @After
+ public void tearDown() throws Exception {
+ runAsShell(
+ WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG,
+ () -> DeviceConfig.deleteProperty("thread_network", "TrelFeature__enabled"));
+ }
+
@Test
public void initialize_tunInterfaceAndNsdPublisherSetToOtDaemon() throws Exception {
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
@@ -259,6 +306,66 @@
}
@Test
+ public void initialize_nat64Disabled_doesNotRequestNat64CidrAndConfiguresOtDaemon()
+ throws Exception {
+ ThreadConfiguration config =
+ new ThreadConfiguration.Builder().setNat64Enabled(false).build();
+ mPersistentSettings.putConfiguration(config);
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ verify(mMockRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
+ verify(mFakeOtDaemon, times(1)).setNat64Cidr(isNull(), any());
+ verify(mFakeOtDaemon, never()).setNat64Cidr(isNotNull(), any());
+ }
+
+ @Test
+ public void initialize_nat64Enabled_requestsNat64CidrAndConfiguresAtOtDaemon()
+ throws Exception {
+ ThreadConfiguration config =
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+ mPersistentSettings.putConfiguration(config);
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ verify(mMockRoutingCoordinatorManager, times(1)).requestDownstreamAddress(any());
+ verify(mFakeOtDaemon, times(1))
+ .setConfiguration(
+ new OtDaemonConfiguration.Builder().setNat64Enabled(true).build(),
+ null /* receiver */);
+ verify(mFakeOtDaemon, times(1)).setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any());
+ }
+
+ @Test
+ public void initialize_trelFeatureDisabled_trelDisabledAtOtDaemon() throws Exception {
+ runAsShell(
+ WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG,
+ () ->
+ DeviceConfig.setProperty(
+ "thread_network", "TrelFeature__enabled", "false", false));
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.isTrelEnabled()).isFalse();
+ }
+
+ @Test
+ public void initialize_trelFeatureEnabled_setTrelEnabledAtOtDamon() throws Exception {
+ runAsShell(
+ WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG,
+ () ->
+ DeviceConfig.setProperty(
+ "thread_network", "TrelFeature__enabled", "true", false));
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.isTrelEnabled()).isTrue();
+ }
+
+ @Test
public void getMeshcopTxtAttributes_emptyVendorName_accepted() {
when(mResources.getString(eq(R.string.config_thread_vendor_name))).thenReturn("");
@@ -716,13 +823,10 @@
ThreadConfiguration config1 =
new ThreadConfiguration.Builder()
.setNat64Enabled(false)
- .setDhcp6PdEnabled(false)
+ .setDhcpv6PdEnabled(false)
.build();
ThreadConfiguration config2 =
- new ThreadConfiguration.Builder()
- .setNat64Enabled(true)
- .setDhcp6PdEnabled(true)
- .build();
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build();
ThreadConfiguration config3 =
new ThreadConfiguration.Builder(config2).build(); // Same as config2
@@ -737,4 +841,170 @@
inOrder.verify(mockReceiver2).onSuccess();
inOrder.verify(mockReceiver3).onSuccess();
}
+
+ @Test
+ public void setConfiguration_enablesNat64_requestsNat64CidrAndConfiguresOtdaemon()
+ throws Exception {
+ mService.initialize();
+ mTestLooper.dispatchAll();
+ clearInvocations(mMockRoutingCoordinatorManager, mFakeOtDaemon);
+
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ mService.setConfiguration(
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build(), mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ verify(mMockRoutingCoordinatorManager, times(1)).requestDownstreamAddress(any());
+ verify(mFakeOtDaemon, times(1))
+ .setConfiguration(
+ eq(new OtDaemonConfiguration.Builder().setNat64Enabled(true).build()),
+ any(IOtStatusReceiver.class));
+ verify(mFakeOtDaemon, times(1))
+ .setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any(IOtStatusReceiver.class));
+ }
+
+ @Test
+ public void setConfiguration_enablesNat64_otDaemonRemoteFailure_serviceDoesNotCrash()
+ throws Exception {
+ mService.initialize();
+ mTestLooper.dispatchAll();
+ clearInvocations(mMockRoutingCoordinatorManager, mFakeOtDaemon);
+ mFakeOtDaemon.setSetNat64CidrException(
+ new RemoteException("ot-daemon setNat64Cidr() throws"));
+
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ mService.setConfiguration(
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build(), mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mFakeOtDaemon, times(1))
+ .setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any(IOtStatusReceiver.class));
+ }
+
+ @Test
+ public void setConfiguration_disablesNat64_releasesNat64CidrAndConfiguresOtdaemon()
+ throws Exception {
+ mPersistentSettings.putConfiguration(
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build());
+ mService.initialize();
+ mTestLooper.dispatchAll();
+ clearInvocations(mMockRoutingCoordinatorManager, mFakeOtDaemon);
+
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ mService.setConfiguration(
+ new ThreadConfiguration.Builder().setNat64Enabled(false).build(), mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ verify(mMockRoutingCoordinatorManager, times(1)).releaseDownstream(any());
+ verify(mMockRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
+ verify(mFakeOtDaemon, times(1))
+ .setConfiguration(
+ eq(new OtDaemonConfiguration.Builder().setNat64Enabled(false).build()),
+ any(IOtStatusReceiver.class));
+ verify(mFakeOtDaemon, times(1)).setNat64Cidr(isNull(), any(IOtStatusReceiver.class));
+ verify(mFakeOtDaemon, never()).setNat64Cidr(isNotNull(), any(IOtStatusReceiver.class));
+ }
+
+ @Test
+ public void initialize_upstreamNetworkRequestHasCertainTransportTypesAndCapabilities() {
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NetworkRequest> networkRequestCaptor =
+ ArgumentCaptor.forClass(NetworkRequest.class);
+ verify(mMockConnectivityManager, atLeastOnce())
+ .registerNetworkCallback(
+ networkRequestCaptor.capture(),
+ any(ConnectivityManager.NetworkCallback.class),
+ any(Handler.class));
+ List<NetworkRequest> upstreamNetworkRequests =
+ networkRequestCaptor.getAllValues().stream()
+ .filter(nr -> !nr.hasTransport(TRANSPORT_THREAD))
+ .toList();
+ assertThat(upstreamNetworkRequests.size()).isEqualTo(1);
+ NetworkRequest upstreamNetworkRequest = upstreamNetworkRequests.get(0);
+ assertThat(upstreamNetworkRequest.hasTransport(TRANSPORT_WIFI)).isTrue();
+ assertThat(upstreamNetworkRequest.hasTransport(TRANSPORT_ETHERNET)).isTrue();
+ assertThat(upstreamNetworkRequest.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+ assertThat(upstreamNetworkRequest.hasCapability(NET_CAPABILITY_INTERNET)).isTrue();
+ }
+
+ @Test
+ public void setTestNetworkAsUpstream_upstreamNetworkRequestAlwaysDisallowsVpn() {
+ mService.initialize();
+ mTestLooper.dispatchAll();
+ clearInvocations(mMockConnectivityManager);
+
+ final IOperationReceiver mockReceiver1 = mock(IOperationReceiver.class);
+ final IOperationReceiver mockReceiver2 = mock(IOperationReceiver.class);
+ mService.setTestNetworkAsUpstream("test-network", mockReceiver1);
+ mService.setTestNetworkAsUpstream(null, mockReceiver2);
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NetworkRequest> networkRequestCaptor =
+ ArgumentCaptor.forClass(NetworkRequest.class);
+ verify(mMockConnectivityManager, times(2))
+ .registerNetworkCallback(
+ networkRequestCaptor.capture(),
+ any(ConnectivityManager.NetworkCallback.class),
+ any(Handler.class));
+ assertThat(networkRequestCaptor.getAllValues().size()).isEqualTo(2);
+ NetworkRequest networkRequest1 = networkRequestCaptor.getAllValues().get(0);
+ NetworkRequest networkRequest2 = networkRequestCaptor.getAllValues().get(1);
+ assertThat(networkRequest1.getNetworkSpecifier()).isNotNull();
+ assertThat(networkRequest1.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+ assertThat(networkRequest2.getNetworkSpecifier()).isNull();
+ assertThat(networkRequest2.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+ }
+
+ @Test
+ public void runOtCtlCommand_noPermission_throwsSecurityException() {
+ doThrow(new SecurityException(""))
+ .when(mContext)
+ .enforceCallingOrSelfPermission(eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), any());
+ doThrow(new SecurityException(""))
+ .when(mContext)
+ .enforceCallingOrSelfPermission(eq(PERMISSION_THREAD_NETWORK_TESTING), any());
+
+ assertThrows(
+ SecurityException.class,
+ () -> mService.runOtCtlCommand("", false, new IOutputReceiver.Default()));
+ }
+
+ @Test
+ public void runOtCtlCommand_otDaemonRemoteFailure_receiverOnErrorIsCalled() throws Exception {
+ mService.initialize();
+ final IOutputReceiver mockReceiver = mock(IOutputReceiver.class);
+ mFakeOtDaemon.setRunOtCtlCommandException(
+ new RemoteException("ot-daemon runOtCtlCommand() throws"));
+
+ mService.runOtCtlCommand("ot-ctl state", false, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
+ }
+
+ @Test
+ public void activateEphemeralKeyMode_succeed() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+ mService.activateEphemeralKeyMode(1_000L, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ }
+
+ @Test
+ public void deactivateEphemeralKeyMode_succeed() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+ mService.deactivateEphemeralKeyMode(mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ }
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
index dfb3129..640b0f1 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -20,12 +20,15 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -35,8 +38,13 @@
import android.content.Context;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IConfigurationReceiver;
+import android.net.thread.IOperationReceiver;
+import android.net.thread.IOutputReceiver;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.os.Binder;
+import android.os.Process;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
@@ -47,6 +55,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -95,6 +104,9 @@
mShellCommand = new ThreadNetworkShellCommand(mContext, mControllerService, mCountryCode);
mShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
+
+ // by default emulate shell uid.
+ BinderUtil.setUid(Process.SHELL_UID);
}
@After
@@ -102,16 +114,20 @@
validateMockitoUsage();
}
- @Test
- public void getCountryCode_testingPermissionIsChecked() {
- when(mCountryCode.getCountryCode()).thenReturn("US");
-
+ private void runShellCommand(String... args) {
mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
new FileDescriptor(),
- new String[] {"get-country-code"});
+ args);
+ }
+
+ @Test
+ public void getCountryCode_testingPermissionIsChecked() {
+ when(mCountryCode.getCountryCode()).thenReturn("US");
+
+ runShellCommand("get-country-code");
verify(mContext, times(1))
.enforceCallingOrSelfPermission(
@@ -122,24 +138,14 @@
public void getCountryCode_currentCountryCodePrinted() {
when(mCountryCode.getCountryCode()).thenReturn("US");
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"get-country-code"});
+ runShellCommand("get-country-code");
verify(mOutputWriter).println(contains("US"));
}
@Test
public void forceSetCountryCodeEnabled_testingPermissionIsChecked() {
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-country-code", "enabled", "US"});
+ runShellCommand("force-country-code", "enabled", "US");
verify(mContext, times(1))
.enforceCallingOrSelfPermission(
@@ -148,36 +154,21 @@
@Test
public void forceSetCountryCodeEnabled_countryCodeIsOverridden() {
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-country-code", "enabled", "US"});
+ runShellCommand("force-country-code", "enabled", "US");
verify(mCountryCode).setOverrideCountryCode(eq("US"));
}
@Test
public void forceSetCountryCodeDisabled_overriddenCountryCodeIsCleared() {
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-country-code", "disabled"});
+ runShellCommand("force-country-code", "disabled");
verify(mCountryCode).clearOverrideCountryCode();
}
@Test
public void forceStopOtDaemon_testingPermissionIsChecked() {
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-stop-ot-daemon", "enabled"});
+ runShellCommand("force-stop-ot-daemon", "enabled");
verify(mContext, times(1))
.enforceCallingOrSelfPermission(
@@ -190,12 +181,7 @@
.when(mControllerService)
.forceStopOtDaemonForTest(eq(true), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-stop-ot-daemon", "enabled"});
+ runShellCommand("force-stop-ot-daemon", "enabled");
verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any());
verify(mOutputWriter, never()).println();
@@ -205,12 +191,7 @@
public void forceStopOtDaemon_serviceApiTimeout_failedWithTimeoutError() {
doNothing().when(mControllerService).forceStopOtDaemonForTest(eq(true), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"force-stop-ot-daemon", "enabled"});
+ runShellCommand("force-stop-ot-daemon", "enabled");
verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any());
verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
@@ -221,12 +202,7 @@
public void join_controllerServiceJoinIsCalled() {
doNothing().when(mControllerService).join(any(), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"join", DEFAULT_ACTIVE_DATASET_TLVS});
+ runShellCommand("join", DEFAULT_ACTIVE_DATASET_TLVS);
var activeDataset =
ActiveOperationalDataset.fromThreadTlvs(
@@ -239,12 +215,7 @@
public void join_invalidDataset_controllerServiceJoinIsNotCalled() {
doNothing().when(mControllerService).join(any(), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"join", "000102"});
+ runShellCommand("join", "000102");
verify(mControllerService, never()).join(any(), any());
verify(mErrorWriter, times(1)).println(contains("Invalid dataset argument"));
@@ -254,12 +225,7 @@
public void migrate_controllerServiceMigrateIsCalled() {
doNothing().when(mControllerService).scheduleMigration(any(), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"migrate", DEFAULT_ACTIVE_DATASET_TLVS, "300"});
+ runShellCommand("migrate", DEFAULT_ACTIVE_DATASET_TLVS, "300");
ArgumentCaptor<PendingOperationalDataset> captor =
ArgumentCaptor.forClass(PendingOperationalDataset.class);
@@ -276,12 +242,7 @@
public void migrate_invalidDataset_controllerServiceMigrateIsNotCalled() {
doNothing().when(mControllerService).scheduleMigration(any(), any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"migrate", "000102", "300"});
+ runShellCommand("migrate", "000102", "300");
verify(mControllerService, never()).scheduleMigration(any(), any());
verify(mErrorWriter, times(1)).println(contains("Invalid dataset argument"));
@@ -291,14 +252,179 @@
public void leave_controllerServiceLeaveIsCalled() {
doNothing().when(mControllerService).leave(any());
- mShellCommand.exec(
- new Binder(),
- new FileDescriptor(),
- new FileDescriptor(),
- new FileDescriptor(),
- new String[] {"leave"});
+ runShellCommand("leave");
verify(mControllerService, times(1)).leave(any());
verify(mErrorWriter, never()).println();
}
+
+ @Test
+ public void handleOtCtlCommand_testingPermissionIsChecked() {
+ BinderUtil.setUid(Process.ROOT_UID);
+ doAnswer(
+ invocation -> {
+ IOutputReceiver receiver = invocation.getArgument(1);
+ receiver.onComplete();
+ return null;
+ })
+ .when(mControllerService)
+ .runOtCtlCommand(anyString(), anyBoolean(), any());
+
+ runShellCommand("ot-ctl", "state");
+
+ verify(mContext, times(1))
+ .enforceCallingOrSelfPermission(
+ eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
+ }
+
+ @Test
+ public void handleOtCtlCommand_failsWithNonRootProcess() {
+ runShellCommand("ot-ctl", "state");
+
+ verify(mErrorWriter, times(1)).println(contains("No access to ot-ctl command"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void handleOtCtlCommand_nonInteractive_serviceTimeout_failsWithTimeoutError() {
+ BinderUtil.setUid(Process.ROOT_UID);
+ doNothing().when(mControllerService).runOtCtlCommand(anyString(), eq(false), any());
+
+ runShellCommand("ot-ctl", "state");
+
+ verify(mControllerService, times(1)).runOtCtlCommand(anyString(), eq(false), any());
+ verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void handleOtCtlCommand_nonInteractive_state_outputIsPrinted() {
+ BinderUtil.setUid(Process.ROOT_UID);
+ doAnswer(
+ invocation -> {
+ IOutputReceiver receiver = invocation.getArgument(2);
+
+ receiver.onOutput("leader");
+ receiver.onOutput("\r\n");
+ receiver.onOutput("Done");
+ receiver.onOutput("\r\n");
+
+ receiver.onComplete();
+ return null;
+ })
+ .when(mControllerService)
+ .runOtCtlCommand(eq("state"), eq(false), any());
+
+ runShellCommand("ot-ctl", "state");
+
+ InOrder inOrder = inOrder(mOutputWriter);
+ inOrder.verify(mOutputWriter).print("leader");
+ inOrder.verify(mOutputWriter).print("\r\n");
+ inOrder.verify(mOutputWriter).print("Done");
+ inOrder.verify(mOutputWriter).print("\r\n");
+ }
+
+ @Test
+ public void config_getConfig_testingPermissionIsChecked() {
+ runShellCommand("config");
+
+ verify(mContext, times(1))
+ .enforceCallingOrSelfPermission(
+ eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
+ }
+
+ @Test
+ public void config_getConfig_serviceTimeOut_failsWithTimeoutError() {
+ runShellCommand("config");
+
+ verify(mControllerService, times(1)).registerConfigurationCallback(any());
+ verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void config_getConfig_expectedValueIsPrinted() {
+ doAnswer(
+ inv -> {
+ ((IConfigurationReceiver) inv.getArgument(0))
+ .onConfigurationChanged(
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(true)
+ .build());
+ return null;
+ })
+ .when(mControllerService)
+ .registerConfigurationCallback(any());
+
+ runShellCommand("config");
+
+ verify(mErrorWriter, never()).println();
+ verify(mOutputWriter, times(1)).println(contains("nat64Enabled=true"));
+ }
+
+ @Test
+ public void config_setConfig_testingPermissionIsChecked() {
+ runShellCommand("config", "nat64", "enabled");
+
+ verify(mContext, times(1))
+ .enforceCallingOrSelfPermission(
+ eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
+ }
+
+ @Test
+ public void config_setConfig_serviceTimeOut_failedWithTimeoutError() {
+ runShellCommand("config", "nat64", "enabled");
+
+ verify(mControllerService, times(1)).registerConfigurationCallback(any());
+ verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void config_invalidArgument_failsWithInvalidArgumentError() {
+ doAnswer(
+ inv -> {
+ ((IConfigurationReceiver) inv.getArgument(0))
+ .onConfigurationChanged(
+ new ThreadConfiguration.Builder().build());
+ return null;
+ })
+ .when(mControllerService)
+ .registerConfigurationCallback(any());
+
+ runShellCommand("config", "invalidName", "invalidValue");
+
+ verify(mErrorWriter, atLeastOnce()).println(contains("Invalid config"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void config_setConfig_expectedValueIsSet() {
+ doAnswer(
+ inv -> {
+ ((IConfigurationReceiver) inv.getArgument(0))
+ .onConfigurationChanged(
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(false)
+ .build());
+ return null;
+ })
+ .when(mControllerService)
+ .registerConfigurationCallback(any());
+ doAnswer(
+ inv -> {
+ ((IOperationReceiver) inv.getArgument(0)).onSuccess();
+ return null;
+ })
+ .when(mControllerService)
+ .setConfiguration(any(), any());
+
+ runShellCommand("config", "nat64", "enabled");
+
+ verify(mControllerService, times(1))
+ .setConfiguration(
+ eq(new ThreadConfiguration.Builder().setNat64Enabled(true).build()), any());
+ verify(mErrorWriter, never()).println();
+ verify(mOutputWriter, never()).println();
+ }
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
index c932ac8..ba489d9 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -152,7 +152,7 @@
ThreadConfiguration configuration =
new ThreadConfiguration.Builder()
.setNat64Enabled(true)
- .setDhcp6PdEnabled(true)
+ .setDhcpv6PdEnabled(true)
.build();
mThreadPersistentSettings.putConfiguration(configuration);
@@ -164,13 +164,13 @@
ThreadConfiguration configuration1 =
new ThreadConfiguration.Builder()
.setNat64Enabled(false)
- .setDhcp6PdEnabled(false)
+ .setDhcpv6PdEnabled(false)
.build();
mThreadPersistentSettings.putConfiguration(configuration1);
ThreadConfiguration configuration2 =
new ThreadConfiguration.Builder()
.setNat64Enabled(true)
- .setDhcp6PdEnabled(true)
+ .setDhcpv6PdEnabled(true)
.build();
assertThat(mThreadPersistentSettings.putConfiguration(configuration2)).isTrue();
@@ -188,9 +188,9 @@
}
@Test
- public void putConfiguration_dhcp6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
+ public void putConfiguration_dhcpv6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
ThreadConfiguration configuration =
- new ThreadConfiguration.Builder().setDhcp6PdEnabled(true).build();
+ new ThreadConfiguration.Builder().setDhcpv6PdEnabled(true).build();
mThreadPersistentSettings.putConfiguration(configuration);
assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
diff --git a/thread/tests/utils/src/android/net/thread/utils/ThreadStateListener.java b/thread/tests/utils/src/android/net/thread/utils/ThreadStateListener.java
new file mode 100644
index 0000000..21eb7d9
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/ThreadStateListener.java
@@ -0,0 +1,96 @@
+/*
+ * 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 android.net.thread.utils;
+
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import android.annotation.Nullable;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkController.StateCallback;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.ArrayTrackRecord;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A listener for sequential Thread state updates.
+ *
+ * <p>This is a wrapper around {@link ThreadNetworkController#registerStateCallback} to make
+ * synchronized access to Thread state updates easier.
+ */
+@VisibleForTesting
+public final class ThreadStateListener {
+ private static final List<ThreadStateListener> sListeners = new ArrayList<>();
+ private final ArrayTrackRecord<Integer> mDeviceRoleUpdates = new ArrayTrackRecord<>();
+ private final ArrayTrackRecord<Integer>.ReadHead mReadHead = mDeviceRoleUpdates.newReadHead();
+ private final ThreadNetworkController mController;
+ private final StateCallback mCallback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int newRole) {
+ mDeviceRoleUpdates.add(newRole);
+ }
+ // Add more state update trackers here
+ };
+
+ /** Creates a new {@link ThreadStateListener} object and starts listening for state updates. */
+ public static ThreadStateListener startListener(ThreadNetworkController controller) {
+ var listener = new ThreadStateListener(controller);
+ sListeners.add(listener);
+ listener.start();
+ return listener;
+ }
+
+ /** Stops all listeners created by {@link #startListener}. */
+ public static void stopAllListeners() {
+ for (var listener : sListeners) {
+ listener.stop();
+ }
+ sListeners.clear();
+ }
+
+ private ThreadStateListener(ThreadNetworkController controller) {
+ mController = controller;
+ }
+
+ private void start() {
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> mController.registerStateCallback(directExecutor(), mCallback));
+ }
+
+ private void stop() {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
+ }
+
+ /**
+ * Polls for any role in {@code roles} starting after call to {@link #startListener}.
+ *
+ * <p>Returns the matched device role or {@code null} if timeout.
+ */
+ @Nullable
+ public Integer pollForAnyRoleOf(List<Integer> roles, Duration timeout) {
+ return mReadHead.poll(timeout.toMillis(), newRole -> (roles.contains(newRole)));
+ }
+}
diff --git a/tools/Android.bp b/tools/Android.bp
index 2c2ed14..1351eb7 100644
--- a/tools/Android.bp
+++ b/tools/Android.bp
@@ -81,7 +81,7 @@
"gen_jarjar.py",
"gen_jarjar_test.py",
],
- data: [
+ device_common_data: [
"testdata/test-jarjar-excludes.txt",
// txt with Test classes to test they aren't included when added to jarjar excludes
"testdata/test-jarjar-excludes-testclass.txt",