Merge "ConnectivityMultiDevicesTest#test_mdns_via_wifip2p" into main
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 078ccde..4b73639 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,4 +1,5 @@
jchalard@google.com
+jimictw@google.com
junyulai@google.com
lorenzo@google.com
maze@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 39009cb..c301397 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,15 +1,13 @@
[Builtin Hooks]
bpfmt = true
clang_format = true
-ktfmt = true
[Builtin Hooks Options]
clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp,hpp
-ktfmt = --kotlinlang-style
[Hook Scripts]
checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
-ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --disabled-rules comment-wrapping -f ${PREUPLOAD_FILES}
hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index c1bc31e..8592af2 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -77,7 +77,7 @@
"name": "libnetworkstats_test"
},
{
- "name": "CtsTetheringTestLatestSdk",
+ "name": "CtsTetheringTest",
"options": [
{
"exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
@@ -226,6 +226,9 @@
},
{
"name": "FrameworksNetIntegrationTests"
+ },
+ {
+ "name": "CtsTetheringTest"
}
],
"postsubmit": [
@@ -391,7 +394,7 @@
"name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
},
{
- "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "name": "CtsTetheringTest[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
"options": [
{
"exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
@@ -406,7 +409,7 @@
"keywords": ["sim"]
},
{
- "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "name": "CtsTetheringTest[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
"keywords": ["sim"],
"options": [
{
@@ -427,6 +430,9 @@
"automotive-mumd-presubmit": [
{
"name": "CtsNetTestCases"
+ },
+ {
+ "name": "CtsNetTestCasesUpdateStatsPermission"
}
],
"imports": [
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 5cf5528..091849b 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -58,11 +58,13 @@
":framework-connectivity-shared-srcs",
":services-tethering-shared-srcs",
":statslog-connectivity-java-gen",
+ ":statslog-framework-connectivity-java-gen",
":statslog-tethering-java-gen",
],
static_libs: [
"androidx.annotation_annotation",
"connectivity-net-module-utils-bpf",
+ "com.android.net.flags-aconfig-java",
"modules-utils-build",
"modules-utils-statemachine",
"networkstack-client",
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index 2a6f6d5..32442f5 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -34,8 +34,10 @@
<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.QUERY_USERS"/>
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 0c05354..19dd492 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -155,7 +155,10 @@
"framework-connectivity",
"framework-connectivity-t",
"framework-tethering",
- ],
+ ] + select(release_flag("RELEASE_MOVE_VCN_TO_MAINLINE"), {
+ true: ["framework-connectivity-b"],
+ default: [],
+ }),
apex_available: ["com.android.tethering"],
// The bootclasspath_fragments that provide APIs on which this depends.
@@ -195,6 +198,7 @@
"android.net.http",
"android.net.netstats",
"android.net.util",
+ "android.net.vcn",
],
// The following packages and all their subpackages currently only
diff --git a/Tethering/apex/permissions/permissions.xml b/Tethering/apex/permissions/permissions.xml
index fcb287e..4051877 100644
--- a/Tethering/apex/permissions/permissions.xml
+++ b/Tethering/apex/permissions/permissions.xml
@@ -20,8 +20,8 @@
<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.QUERY_USERS"/>
<permission name="android.permission.READ_NETWORK_USAGE_HISTORY"/>
<permission name="android.permission.TETHER_PRIVILEGED"/>
<permission name="android.permission.UPDATE_APP_OPS_STATS"/>
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 2f3307a..e2498e4 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -33,6 +33,10 @@
// 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",
diff --git a/Tethering/common/TetheringLib/api/current.txt b/Tethering/common/TetheringLib/api/current.txt
index d802177..932e801 100644
--- a/Tethering/common/TetheringLib/api/current.txt
+++ b/Tethering/common/TetheringLib/api/current.txt
@@ -1 +1,71 @@
// Signature format: 2.0
+package android.net {
+
+ 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;
+ }
+
+ public class TetheringManager {
+ method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback);
+ method @RequiresPermission(value=android.Manifest.permission.TETHER_PRIVILEGED, conditional=true) public void startTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @RequiresPermission(value=android.Manifest.permission.TETHER_PRIVILEGED, conditional=true) public void stopTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StopTetheringCallback);
+ method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
+ field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1
+ field public static final int TETHERING_WIFI = 0; // 0x0
+ field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc
+ field public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9; // 0x9
+ field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public static final int TETHER_ERROR_DUPLICATE_REQUEST = 18; // 0x12
+ field public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8; // 0x8
+ field public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13; // 0xd
+ field public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; // 0xa
+ field public static final int TETHER_ERROR_INTERNAL_ERROR = 5; // 0x5
+ field public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15; // 0xf
+ field public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14; // 0xe
+ field public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
+ field public static final int TETHER_ERROR_PROVISIONING_FAILED = 11; // 0xb
+ field public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2; // 0x2
+ field public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6; // 0x6
+ field public static final int TETHER_ERROR_UNAVAIL_IFACE = 4; // 0x4
+ field public static final int TETHER_ERROR_UNKNOWN_IFACE = 1; // 0x1
+ field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public static final int TETHER_ERROR_UNKNOWN_REQUEST = 17; // 0x11
+ field public static final int TETHER_ERROR_UNKNOWN_TYPE = 16; // 0x10
+ field public static final int TETHER_ERROR_UNSUPPORTED = 3; // 0x3
+ field public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7; // 0x7
+ }
+
+ public static interface TetheringManager.StartTetheringCallback {
+ method public default void onTetheringFailed(int);
+ method public default void onTetheringStarted();
+ }
+
+ @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public static interface TetheringManager.StopTetheringCallback {
+ method public default void onStopTetheringFailed(int);
+ method public default void onStopTetheringSucceeded();
+ }
+
+ public static interface TetheringManager.TetheringEventCallback {
+ method public default void onTetheredInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
+ }
+
+ public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public int describeContents();
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
+ 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 {
+ ctor public TetheringManager.TetheringRequest.Builder(int);
+ method @NonNull public android.net.TetheringManager.TetheringRequest build();
+ 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);
+ }
+
+}
+
diff --git a/Tethering/common/TetheringLib/api/module-lib-current.txt b/Tethering/common/TetheringLib/api/module-lib-current.txt
index a680590..3ba8e1b 100644
--- a/Tethering/common/TetheringLib/api/module-lib-current.txt
+++ b/Tethering/common/TetheringLib/api/module-lib-current.txt
@@ -22,7 +22,7 @@
method public boolean isTetheringSupported(@NonNull String);
method public void requestLatestTetheringEntitlementResult(int, @NonNull android.os.ResultReceiver, boolean);
method @Deprecated public int setUsbTethering(boolean);
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(int, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
+ method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
method @Deprecated public int tether(@NonNull String);
method @Deprecated public int untether(@NonNull String);
}
@@ -47,8 +47,13 @@
}
public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
- method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @Nullable public String getPackageName();
- method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public int getUid();
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public String getInterfaceName();
+ 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();
+ }
+
+ public static class TetheringManager.TetheringRequest.Builder {
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public android.net.TetheringManager.TetheringRequest.Builder setInterfaceName(@Nullable String);
}
}
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 3efaac2..c0c0abc 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -19,24 +19,11 @@
field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheredClient.AddressInfo> CREATOR;
}
- public final class TetheringInterface implements android.os.Parcelable {
- ctor public TetheringInterface(int, @NonNull String);
- method public int describeContents();
- method @NonNull public String getInterface();
- 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;
- }
-
public class TetheringManager {
- method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback);
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener);
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopAllTethering();
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopTethering(int);
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
+ method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener);
+ method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopAllTethering();
+ method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopTethering(int);
field @Deprecated public static final String ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED";
- field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1
field public static final int CONNECTIVITY_SCOPE_LOCAL = 2; // 0x2
field public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
field public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
@@ -48,25 +35,7 @@
field public static final int TETHERING_NCM = 4; // 0x4
field public static final int TETHERING_USB = 1; // 0x1
field @FlaggedApi("com.android.net.flags.tethering_request_virtual") public static final int TETHERING_VIRTUAL = 7; // 0x7
- field public static final int TETHERING_WIFI = 0; // 0x0
field public static final int TETHERING_WIFI_P2P = 3; // 0x3
- field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc
- field public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9; // 0x9
- field public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8; // 0x8
- field public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13; // 0xd
- field public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; // 0xa
- field public static final int TETHER_ERROR_INTERNAL_ERROR = 5; // 0x5
- field public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15; // 0xf
- field public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14; // 0xe
- field public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
- field public static final int TETHER_ERROR_PROVISIONING_FAILED = 11; // 0xb
- field public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2; // 0x2
- field public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6; // 0x6
- field public static final int TETHER_ERROR_UNAVAIL_IFACE = 4; // 0x4
- field public static final int TETHER_ERROR_UNKNOWN_IFACE = 1; // 0x1
- field public static final int TETHER_ERROR_UNKNOWN_TYPE = 16; // 0x10
- field public static final int TETHER_ERROR_UNSUPPORTED = 3; // 0x3
- field public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7; // 0x7
field public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2; // 0x2
field public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1; // 0x1
field public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0; // 0x0
@@ -76,11 +45,6 @@
method public void onTetheringEntitlementResult(int);
}
- public static interface TetheringManager.StartTetheringCallback {
- method public default void onTetheringFailed(int);
- method public default void onTetheringStarted();
- }
-
public static interface TetheringManager.TetheringEventCallback {
method public default void onClientsChanged(@NonNull java.util.Collection<android.net.TetheredClient>);
method public default void onError(@NonNull String, int);
@@ -91,31 +55,23 @@
method public default void onTetherableInterfacesChanged(@NonNull java.util.List<java.lang.String>);
method public default void onTetherableInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
method public default void onTetheredInterfacesChanged(@NonNull java.util.List<java.lang.String>);
- method public default void onTetheredInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
method public default void onTetheringSupported(boolean);
method public default void onUpstreamChanged(@Nullable android.net.Network);
}
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 @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_request_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;
}
public static class TetheringManager.TetheringRequest.Builder {
- ctor public TetheringManager.TetheringRequest.Builder(int);
- method @NonNull public android.net.TetheringManager.TetheringRequest build();
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_request_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/ITetheringConnector.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
index 77e78bd..7d244e2 100644
--- a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
@@ -18,6 +18,7 @@
import android.net.IIntResultListener;
import android.net.ITetheringEventCallback;
import android.net.TetheringRequestParcel;
+import android.net.TetheringManager.TetheringRequest;
import android.os.ResultReceiver;
/** @hide */
@@ -37,6 +38,9 @@
void stopTethering(int type, String callerPkg, String callingAttributionTag,
IIntResultListener receiver);
+ void stopTetheringRequest(in TetheringRequest request, String callerPkg,
+ String callingAttributionTag, IIntResultListener receiver);
+
void requestLatestTetheringEntitlementResult(int type, in ResultReceiver receiver,
boolean showEntitlementUi, String callerPkg, String callingAttributionTag);
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
index 84cdef1..250179a 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
@@ -16,78 +16,113 @@
package android.net;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SystemApi;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
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;
/**
* The mapping of tethering interface and type.
- * @hide
*/
-@SystemApi
+@SuppressLint("UnflaggedApi")
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. */
+ @SuppressLint("UnflaggedApi")
public int getType() {
return mType;
}
/** Get tethering interface. */
@NonNull
+ @SuppressLint("UnflaggedApi")
public String getInterface() {
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
+ @SuppressLint("UnflaggedApi")
+ public SoftApConfiguration getSoftApConfiguration() {
+ return mSoftApConfig;
+ }
+
@Override
+ @SuppressLint("UnflaggedApi")
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mType);
dest.writeString(mInterface);
+ dest.writeParcelable(mSoftApConfig, flags);
}
@Override
+ @SuppressLint("UnflaggedApi")
public int hashCode() {
- return Objects.hash(mType, mInterface);
+ return Objects.hash(mType, mInterface, mSoftApConfig);
}
@Override
+ @SuppressLint("UnflaggedApi")
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
+ @SuppressLint("UnflaggedApi")
public int describeContents() {
return 0;
}
@NonNull
+ @SuppressLint("UnflaggedApi")
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
@Override
+ @SuppressLint("UnflaggedApi")
public TetheringInterface[] newArray(int size) {
return new TetheringInterface[size];
}
@@ -97,6 +132,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 7c7a4e0..0ac97f0 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -28,6 +28,7 @@
import android.content.Context;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
+import android.os.Build;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.IBinder;
@@ -54,6 +55,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,9 +64,8 @@
* <p> The primary responsibilities of this class are to provide the APIs for applications to
* start tethering, stop tethering, query configuration and query status.
*
- * @hide
*/
-@SystemApi
+@SuppressLint({"NotCloseable", "UnflaggedApi"})
public class TetheringManager {
private static final String TAG = TetheringManager.class.getSimpleName();
private static final int DEFAULT_TIMEOUT_MS = 60_000;
@@ -94,36 +95,46 @@
* {@code TetheringManager.EXTRA_ERRORED_TETHER} to indicate
* the current state of tethering. Each include a list of
* interface names in that state (may be empty).
+ * @hide
*
* @deprecated New client should use TetheringEventCallback instead.
*/
@Deprecated
+ @SystemApi
public static final String ACTION_TETHER_STATE_CHANGED =
"android.net.conn.TETHER_STATE_CHANGED";
/**
* gives a String[] listing all the interfaces configured for
* tethering and currently available for tethering.
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_AVAILABLE_TETHER = "availableArray";
/**
* gives a String[] listing all the interfaces currently in local-only
* mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
/**
* gives a String[] listing all the interfaces currently tethered
* (ie, has DHCPv4 support and packets potentially forwarded/NATed)
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
/**
* gives a String[] listing all the interfaces we tried to tether and
* failed. Use {@link #getLastTetherError} to find the error code
* for any interfaces listed here.
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_ERRORED_TETHER = "erroredArray";
/** @hide */
@@ -135,6 +146,7 @@
TETHERING_WIFI_P2P,
TETHERING_NCM,
TETHERING_ETHERNET,
+ TETHERING_VIRTUAL,
})
public @interface TetheringType {
}
@@ -142,44 +154,57 @@
/**
* Invalid tethering type.
* @see #startTethering.
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_INVALID = -1;
/**
* Wifi tethering type.
* @see #startTethering.
*/
+ @SuppressLint("UnflaggedApi")
public static final int TETHERING_WIFI = 0;
/**
* USB tethering type.
* @see #startTethering.
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_USB = 1;
/**
* Bluetooth tethering type.
* @see #startTethering.
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_BLUETOOTH = 2;
/**
* Wifi P2p tethering type.
* Wifi P2p tethering is set through events automatically, and don't
* need to start from #startTethering.
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_WIFI_P2P = 3;
/**
* Ncm local tethering type.
* @see #startTethering(TetheringRequest, Executor, StartTetheringCallback)
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_NCM = 4;
/**
* Ethernet tethering type.
* @see #startTethering(TetheringRequest, Executor, StartTetheringCallback)
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_ETHERNET = 5;
/**
@@ -208,6 +233,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 = {
@@ -238,30 +277,67 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
TETHER_ERROR_SERVICE_UNAVAIL,
+ TETHER_ERROR_UNSUPPORTED,
TETHER_ERROR_INTERNAL_ERROR,
TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION,
TETHER_ERROR_UNKNOWN_TYPE,
+ TETHER_ERROR_DUPLICATE_REQUEST,
})
public @interface StartTetheringError {
}
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ TETHER_ERROR_NO_ERROR,
+ TETHER_ERROR_UNKNOWN_REQUEST,
+ })
+ public @interface StopTetheringError {
+ }
+
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_NO_ERROR = 0;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_UNKNOWN_IFACE = 1;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_UNSUPPORTED = 3;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_UNAVAIL_IFACE = 4;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_INTERNAL_ERROR = 5;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_PROVISIONING_FAILED = 11;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_UNKNOWN_TYPE = 16;
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ public static final int TETHER_ERROR_UNKNOWN_REQUEST = 17;
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ public static final int TETHER_ERROR_DUPLICATE_REQUEST = 18;
+ /**
+ * Never used outside Tethering.java.
+ * @hide
+ */
+ public static final int TETHER_ERROR_BLUETOOTH_SERVICE_PENDING = 19;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -273,11 +349,23 @@
public @interface TetherOffloadStatus {
}
- /** Tethering offload status is stopped. */
+ /**
+ * Tethering offload status is stopped.
+ * @hide
+ */
+ @SystemApi
public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0;
- /** Tethering offload status is started. */
+ /**
+ * Tethering offload status is started.
+ * @hide
+ */
+ @SystemApi
public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1;
- /** Fail to start tethering offload. */
+ /**
+ * Fail to start tethering offload.
+ * @hide
+ */
+ @SystemApi
public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2;
/**
@@ -306,7 +394,9 @@
// up and be sent from a worker thread; later, they are always sent from the caller thread.
// Considering that it's just oneway binder calls, and ordering is preserved, this seems
// better than inconsistent behavior persisting after boot.
- if (connector != null) {
+ // If system server restarted, mConnectorSupplier might temporarily return a stale (i.e.
+ // dead) version of TetheringService.
+ if (connector != null && connector.isBinderAlive()) {
mConnector = ITetheringConnector.Stub.asInterface(connector);
} else {
startPollingForConnector();
@@ -341,9 +431,8 @@
} catch (InterruptedException e) {
// Not much to do here, the system needs to wait for the connector
}
-
final IBinder connector = mConnectorSupplier.get();
- if (connector != null) {
+ if (connector != null && connector.isBinderAlive()) {
onTetheringConnected(ITetheringConnector.Stub.asInterface(connector));
return;
}
@@ -574,6 +663,13 @@
}
}
+ private void unsupportedAfterV() {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ throw new UnsupportedOperationException("Not supported after SDK version "
+ + Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ }
+ }
+
/**
* Attempt to tether the named interface. This will setup a dhcp server
* on the interface, forward and NAT IP v4 packets and forward DNS requests
@@ -583,8 +679,10 @@
* access will of course fail until an upstream network interface becomes
* active.
*
- * @deprecated The only usages is PanService. It uses this for legacy reasons
- * and will migrate away as soon as possible.
+ * @deprecated Legacy tethering API. Callers should instead use
+ * {@link #startTethering(int, Executor, StartTetheringCallback)}.
+ * On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will
+ * throw an UnsupportedOperationException.
*
* @param iface the interface name to tether.
* @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -594,6 +692,8 @@
@Deprecated
@SystemApi(client = MODULE_LIBRARIES)
public int tether(@NonNull final String iface) {
+ unsupportedAfterV();
+
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "tether caller:" + callerPkg);
final RequestDispatcher dispatcher = new RequestDispatcher();
@@ -617,14 +717,18 @@
/**
* Stop tethering the named interface.
*
- * @deprecated The only usages is PanService. It uses this for legacy reasons
- * and will migrate away as soon as possible.
+ * @deprecated Legacy tethering API. Callers should instead use
+ * {@link #stopTethering(int)}.
+ * On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will
+ * throw an UnsupportedOperationException.
*
* {@hide}
*/
@Deprecated
@SystemApi(client = MODULE_LIBRARIES)
public int untether(@NonNull final String iface) {
+ unsupportedAfterV();
+
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "untether caller:" + callerPkg);
@@ -671,11 +775,14 @@
* Indicates that this tethering connection will provide connectivity beyond this device (e.g.,
* global Internet access).
*/
+ @SuppressLint("UnflaggedApi")
public static final int CONNECTIVITY_SCOPE_GLOBAL = 1;
/**
* Indicates that this tethering connection will only provide local connectivity.
+ * @hide
*/
+ @SystemApi
public static final int CONNECTIVITY_SCOPE_LOCAL = 2;
/**
@@ -689,17 +796,69 @@
})
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.
*/
+ @SuppressLint("UnflaggedApi")
public static final class TetheringRequest implements Parcelable {
+ /**
+ * Tethering started by an explicit call to startTethering.
+ * @hide
+ */
+ public static final int REQUEST_TYPE_EXPLICIT = 0;
+
+ /**
+ * Tethering implicitly started by broadcasts (LOHS and P2P). Can never be pending.
+ * @hide
+ */
+ public static final int REQUEST_TYPE_IMPLICIT = 1;
+
+ /**
+ * Tethering started by the legacy tether() call. Can only happen on V-.
+ * @hide
+ */
+ public static final int REQUEST_TYPE_LEGACY = 2;
+
+ /**
+ * Tethering started but there was no pending request found. This may happen if Tethering is
+ * started and immediately stopped before the link layer goes up, or if we get a link layer
+ * event without a prior call to startTethering (e.g. adb shell cmd wifi start-softap).
+ * @hide
+ */
+ public static final int REQUEST_TYPE_PLACEHOLDER = 3;
+
+ /**
+ * Type of request, used to keep track of whether the request was explicitly sent by
+ * startTethering, implicitly created by broadcasts, or via legacy tether().
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "TYPE_", value = {
+ REQUEST_TYPE_EXPLICIT,
+ REQUEST_TYPE_IMPLICIT,
+ REQUEST_TYPE_LEGACY,
+ REQUEST_TYPE_PLACEHOLDER,
+ })
+ public @interface RequestType {}
+
/** A configuration set for TetheringRequest. */
private final TetheringRequestParcel mRequestParcel;
/**
* @hide
*/
- @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
public TetheringRequest(@NonNull final TetheringRequestParcel request) {
mRequestParcel = request;
}
@@ -708,7 +867,7 @@
mRequestParcel = in.readParcelable(TetheringRequestParcel.class.getClassLoader());
}
- @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
@NonNull
public static final Creator<TetheringRequest> CREATOR = new Creator<>() {
@Override
@@ -722,23 +881,25 @@
}
};
- @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
@Override
public int describeContents() {
return 0;
}
- @FlaggedApi(Flags.FLAG_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);
}
/** Builder used to create TetheringRequest. */
+ @SuppressLint({"UnflaggedApi", "StaticFinalBuilder"})
public static class Builder {
private final TetheringRequestParcel mBuilderParcel;
/** Default constructor of Builder. */
+ @SuppressLint("UnflaggedApi")
public Builder(@TetheringType final int type) {
mBuilderParcel = new TetheringRequestParcel();
mBuilderParcel.tetheringType = type;
@@ -749,6 +910,8 @@
mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type);
mBuilderParcel.uid = Process.INVALID_UID;
mBuilderParcel.softApConfig = null;
+ mBuilderParcel.interfaceName = null;
+ mBuilderParcel.requestType = REQUEST_TYPE_EXPLICIT;
}
/**
@@ -759,7 +922,9 @@
*
* @param localIPv4Address The preferred local IPv4 link address to use.
* @param clientAddress The static client address.
+ * @hide
*/
+ @SystemApi
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@NonNull
public Builder setStaticIpv4Addresses(@NonNull final LinkAddress localIPv4Address,
@@ -775,7 +940,11 @@
return this;
}
- /** Start tethering without entitlement checks. */
+ /**
+ * Start tethering without entitlement checks.
+ * @hide
+ */
+ @SystemApi
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@NonNull
public Builder setExemptFromEntitlementCheck(boolean exempt) {
@@ -786,7 +955,9 @@
/**
* If an entitlement check is needed, sets whether to show the entitlement UI or to
* perform a silent entitlement check. By default, the entitlement UI is shown.
+ * @hide
*/
+ @SystemApi
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@NonNull
public Builder setShouldShowEntitlementUi(boolean showUi) {
@@ -795,8 +966,39 @@
}
/**
- * Sets the connectivity scope to be provided by this tethering downstream.
+ * Sets the name of the interface. Currently supported only for
+ * - {@link #TETHERING_VIRTUAL}.
+ * - {@link #TETHERING_WIFI} (for Local-only Hotspot)
+ * - {@link #TETHERING_WIFI_P2P}
+ * @hide
*/
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ })
+ @NonNull
+ @SystemApi(client = MODULE_LIBRARIES)
+ public Builder setInterfaceName(@Nullable final String interfaceName) {
+ switch (mBuilderParcel.tetheringType) {
+ case TETHERING_VIRTUAL:
+ case TETHERING_WIFI_P2P:
+ case TETHERING_WIFI:
+ break;
+ default:
+ throw new IllegalArgumentException("Interface name cannot be set for"
+ + " tethering type " + interfaceName);
+ }
+ mBuilderParcel.interfaceName = interfaceName;
+ return this;
+ }
+
+ /**
+ * Sets the connectivity scope to be provided by this tethering downstream.
+ * @hide
+ */
+ @SystemApi
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@NonNull
public Builder setConnectivityScope(@ConnectivityScope int scope) {
@@ -816,11 +1018,13 @@
* 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.
+ * </p>
+ * Non-system callers using TETHERING_WIFI must specify a SoftApConfiguration.
*
* @param softApConfig SoftApConfiguration to use.
* @throws IllegalArgumentException if the tethering type isn't TETHERING_WIFI.
*/
- @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@NonNull
public Builder setSoftApConfiguration(@Nullable SoftApConfiguration softApConfig) {
@@ -834,6 +1038,7 @@
/** Build {@link TetheringRequest} with the currently set configuration. */
@NonNull
+ @SuppressLint("UnflaggedApi")
public TetheringRequest build() {
return new TetheringRequest(mBuilderParcel);
}
@@ -842,7 +1047,9 @@
/**
* Get the local IPv4 address, if one was configured with
* {@link Builder#setStaticIpv4Addresses}.
+ * @hide
*/
+ @SystemApi
@Nullable
public LinkAddress getLocalIpv4Address() {
return mRequestParcel.localIPv4Address;
@@ -851,35 +1058,64 @@
/**
* Get the static IPv4 address of the client, if one was configured with
* {@link Builder#setStaticIpv4Addresses}.
+ * @hide
*/
+ @SystemApi
@Nullable
public LinkAddress getClientStaticIpv4Address() {
return mRequestParcel.staticClientAddress;
}
- /** Get tethering type. */
+ /**
+ * Get tethering type.
+ * @hide
+ */
+ @SystemApi
@TetheringType
public int getTetheringType() {
return mRequestParcel.tetheringType;
}
- /** Get connectivity type */
+ /**
+ * Get connectivity type
+ * @hide
+ */
+ @SystemApi
@ConnectivityScope
public int getConnectivityScope() {
return mRequestParcel.connectivityScope;
}
- /** Check if exempt from entitlement check. */
+ /**
+ * Check if exempt from entitlement check.
+ * @hide
+ */
+ @SystemApi
public boolean isExemptFromEntitlementCheck() {
return mRequestParcel.exemptFromEntitlementCheck;
}
- /** Check if show entitlement ui. */
+ /**
+ * Check if show entitlement ui.
+ * @hide
+ */
+ @SystemApi
public boolean getShouldShowEntitlementUi() {
return mRequestParcel.showProvisioningUi;
}
/**
+ * Get interface name.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ @Nullable
+ @SystemApi(client = MODULE_LIBRARIES)
+ public String getInterfaceName() {
+ return mRequestParcel.interfaceName;
+ }
+
+ /**
* Check whether the two addresses are ipv4 and in the same prefix.
* @hide
*/
@@ -915,7 +1151,7 @@
/**
* Get the desired SoftApConfiguration of the request, if one was specified.
*/
- @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
@Nullable
public SoftApConfiguration getSoftApConfiguration() {
return mRequestParcel.softApConfig;
@@ -944,7 +1180,7 @@
* {@link Process#INVALID_UID} if unset.
* @hide
*/
- @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
@SystemApi(client = MODULE_LIBRARIES)
public int getUid() {
return mRequestParcel.uid;
@@ -955,7 +1191,7 @@
* unset.
* @hide
*/
- @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
@SystemApi(client = MODULE_LIBRARIES)
@Nullable
public String getPackageName() {
@@ -970,53 +1206,112 @@
return mRequestParcel;
}
- /** String of TetheringRequest detail. */
- public String toString() {
- return "TetheringRequest [ type= " + mRequestParcel.tetheringType
- + ", localIPv4Address= " + mRequestParcel.localIPv4Address
- + ", staticClientAddress= " + mRequestParcel.staticClientAddress
- + ", exemptFromEntitlementCheck= " + mRequestParcel.exemptFromEntitlementCheck
- + ", showProvisioningUi= " + mRequestParcel.showProvisioningUi
- + ", softApConfig= " + mRequestParcel.softApConfig
- + ", uid= " + mRequestParcel.uid
- + ", packageName= " + mRequestParcel.packageName
- + " ]";
+ /**
+ * Get the type of the request.
+ * @hide
+ */
+ public @RequestType int getRequestType() {
+ return mRequestParcel.requestType;
}
+ /**
+ * String of TetheringRequest detail.
+ * @hide
+ */
+ @SystemApi
+ public String toString() {
+ StringJoiner sj = new StringJoiner(", ", "TetheringRequest[ ", " ]");
+ sj.add(typeToString(mRequestParcel.tetheringType));
+ if (mRequestParcel.requestType == REQUEST_TYPE_IMPLICIT) {
+ sj.add("IMPLICIT");
+ } else if (mRequestParcel.requestType == REQUEST_TYPE_LEGACY) {
+ sj.add("LEGACY");
+ } else if (mRequestParcel.requestType == REQUEST_TYPE_PLACEHOLDER) {
+ sj.add("PLACEHOLDER");
+ }
+ 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);
+ }
+ if (mRequestParcel.interfaceName != null) {
+ sj.add("interfaceName=" + mRequestParcel.interfaceName);
+ }
+ return sj.toString();
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof TetheringRequest otherRequest)) return false;
+ if (!equalsIgnoreUidPackage(otherRequest)) return false;
TetheringRequestParcel parcel = getParcel();
TetheringRequestParcel otherParcel = otherRequest.getParcel();
- return parcel.tetheringType == otherParcel.tetheringType
+ return parcel.uid == otherParcel.uid
+ && Objects.equals(parcel.packageName, otherParcel.packageName);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean equalsIgnoreUidPackage(TetheringRequest otherRequest) {
+ TetheringRequestParcel parcel = getParcel();
+ TetheringRequestParcel otherParcel = otherRequest.getParcel();
+ return parcel.requestType == otherParcel.requestType
+ && parcel.tetheringType == otherParcel.tetheringType
&& Objects.equals(parcel.localIPv4Address, otherParcel.localIPv4Address)
&& Objects.equals(parcel.staticClientAddress, otherParcel.staticClientAddress)
&& parcel.exemptFromEntitlementCheck == otherParcel.exemptFromEntitlementCheck
&& parcel.showProvisioningUi == otherParcel.showProvisioningUi
&& parcel.connectivityScope == otherParcel.connectivityScope
&& Objects.equals(parcel.softApConfig, otherParcel.softApConfig)
- && parcel.uid == otherParcel.uid
- && Objects.equals(parcel.packageName, otherParcel.packageName);
+ && Objects.equals(parcel.interfaceName, otherParcel.interfaceName);
}
+ /**
+ * @hide
+ */
+ @SystemApi
@Override
public int hashCode() {
TetheringRequestParcel parcel = getParcel();
return Objects.hash(parcel.tetheringType, parcel.localIPv4Address,
parcel.staticClientAddress, parcel.exemptFromEntitlementCheck,
parcel.showProvisioningUi, parcel.connectivityScope, parcel.softApConfig,
- parcel.uid, parcel.packageName);
+ parcel.uid, parcel.packageName, parcel.interfaceName);
}
}
/**
* Callback for use with {@link #startTethering} to find out whether tethering succeeded.
*/
+ @SuppressLint("UnflaggedApi")
public interface StartTetheringCallback {
/**
* Called when tethering has been successfully started.
*/
+ @SuppressLint("UnflaggedApi")
default void onTetheringStarted() {}
/**
@@ -1024,26 +1319,40 @@
*
* @param error The error that caused the failure.
*/
+ @SuppressLint("UnflaggedApi")
default void onTetheringFailed(@StartTetheringError final int error) {}
}
/**
+ * Callback for use with {@link #stopTethering} to find out whether stop tethering succeeded.
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ public interface StopTetheringCallback {
+ /**
+ * Called when tethering has been successfully stopped.
+ */
+ default void onStopTetheringSucceeded() {}
+
+ /**
+ * Called when starting tethering failed.
+ *
+ * @param error The error that caused the failure.
+ */
+ default void onStopTetheringFailed(@StopTetheringError final int error) {}
+ }
+
+ /**
* Starts tethering and runs tether provisioning for the given type if needed. If provisioning
* fails, stopTethering will be called automatically.
*
- * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
- * fail if a tethering entitlement check is required.
- *
* @param request a {@link TetheringRequest} which can specify the preferred configuration.
* @param executor {@link Executor} to specify the thread upon which the callback of
* TetheringRequest will be invoked.
* @param callback A callback that will be called to indicate the success status of the
* tethering start request.
*/
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @RequiresPermission(value = android.Manifest.permission.TETHER_PRIVILEGED, conditional = true)
+ @SuppressLint("UnflaggedApi")
public void startTethering(@NonNull final TetheringRequest request,
@NonNull final Executor executor, @NonNull final StartTetheringCallback callback) {
final String callerPkg = mContext.getOpPackageName();
@@ -1069,18 +1378,12 @@
* Starts tethering and runs tether provisioning for the given type if needed. If provisioning
* fails, stopTethering will be called automatically.
*
- * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
- * fail if a tethering entitlement check is required.
- *
* @param type The tethering type, on of the {@code TetheringManager#TETHERING_*} constants.
* @param executor {@link Executor} to specify the thread upon which the callback of
* TetheringRequest will be invoked.
* @hide
*/
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@SystemApi(client = MODULE_LIBRARIES)
public void startTethering(int type, @NonNull final Executor executor,
@NonNull final StartTetheringCallback callback) {
@@ -1091,13 +1394,10 @@
* Stops tethering for the given type. Also cancels any provisioning rechecks for that type if
* applicable.
*
- * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
- * fail if a tethering entitlement check is required.
+ * @hide
*/
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+ @SystemApi
public void stopTethering(@TetheringType final int type) {
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "stopTethering caller:" + callerPkg);
@@ -1115,9 +1415,40 @@
}
/**
+ * Stops tethering for the given request. Operation will fail with
+ * {@link #TETHER_ERROR_UNKNOWN_REQUEST} if there is no request that matches it.
+ */
+ @RequiresPermission(value = android.Manifest.permission.TETHER_PRIVILEGED, conditional = true)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ public void stopTethering(@NonNull TetheringRequest request,
+ @NonNull final Executor executor, @NonNull final StopTetheringCallback callback) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ final String callerPkg = mContext.getOpPackageName();
+ Log.i(TAG, "stopTethering: request=" + request + ", caller=" + callerPkg);
+ getConnector(c -> c.stopTetheringRequest(request, callerPkg, getAttributionTag(),
+ new IIntResultListener.Stub() {
+ @Override
+ public void onResult(final int resultCode) {
+ executor.execute(() -> {
+ if (resultCode == TETHER_ERROR_NO_ERROR) {
+ callback.onStopTetheringSucceeded();
+ } else {
+ callback.onStopTetheringFailed(resultCode);
+ }
+ });
+ }
+ }));
+ }
+
+ /**
* Callback for use with {@link #getLatestTetheringEntitlementResult} to find out whether
* entitlement succeeded.
+ * @hide
*/
+ @SystemApi
public interface OnTetheringEntitlementResultListener {
/**
* Called to notify entitlement result.
@@ -1138,9 +1469,6 @@
* {@link #TETHER_ERROR_ENTITLEMENT_UNKNOWN} will be returned. If {@code showEntitlementUi} is
* true, entitlement will be run.
*
- * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
- * fail if a tethering entitlement check is required.
- *
* @param type the downstream type of tethering. Must be one of {@code #TETHERING_*} constants.
* @param showEntitlementUi a boolean indicating whether to check result for the UI-based
* entitlement check or the silent entitlement check.
@@ -1148,11 +1476,10 @@
* @param listener an {@link OnTetheringEntitlementResultListener} which will be called to
* notify the caller of the result of entitlement check. The listener may be called zero
* or one time.
+ * @hide
*/
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void requestLatestTetheringEntitlementResult(@TetheringType int type,
boolean showEntitlementUi,
@NonNull Executor executor,
@@ -1196,6 +1523,7 @@
* Callback for use with {@link registerTetheringEventCallback} to find out tethering
* upstream status.
*/
+ @SuppressLint("UnflaggedApi")
public interface TetheringEventCallback {
/**
* Called when tethering supported status changed.
@@ -1207,7 +1535,9 @@
* policy restrictions.
*
* @param supported whether any tethering type is supported.
+ * @hide
*/
+ @SystemApi
default void onTetheringSupported(boolean supported) {}
/**
@@ -1232,7 +1562,9 @@
*
* @param network the {@link Network} of tethering upstream. Null means tethering doesn't
* have any upstream.
+ * @hide
*/
+ @SystemApi
default void onUpstreamChanged(@Nullable Network network) {}
/**
@@ -1259,7 +1591,9 @@
* <p>This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
* @param interfaces The list of tetherable interface names.
+ * @hide
*/
+ @SystemApi
default void onTetherableInterfacesChanged(@NonNull List<String> interfaces) {}
/**
@@ -1269,7 +1603,9 @@
* <p>This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
* @param interfaces The set of TetheringInterface of currently tetherable interface.
+ * @hide
*/
+ @SystemApi
default void onTetherableInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
// By default, the new callback calls the old callback, so apps
// implementing the old callback just work.
@@ -1282,7 +1618,9 @@
* <p>This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
* @param interfaces The lit of 0 or more String of currently tethered interface names.
+ * @hide
*/
+ @SystemApi
default void onTetheredInterfacesChanged(@NonNull List<String> interfaces) {}
/**
@@ -1293,6 +1631,7 @@
* @param interfaces The set of 0 or more TetheringInterface of currently tethered
* interface.
*/
+ @SuppressLint("UnflaggedApi")
default void onTetheredInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
// By default, the new callback calls the old callback, so apps
// implementing the old callback just work.
@@ -1305,7 +1644,9 @@
* <p>This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
* @param interfaces The list of 0 or more String of active local-only interface names.
+ * @hide
*/
+ @SystemApi
default void onLocalOnlyInterfacesChanged(@NonNull List<String> interfaces) {}
/**
@@ -1315,7 +1656,9 @@
* multiple times later upon changes.
* @param interfaces The set of 0 or more TetheringInterface of active local-only
* interface.
+ * @hide
*/
+ @SystemApi
default void onLocalOnlyInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
// By default, the new callback calls the old callback, so apps
// implementing the old callback just work.
@@ -1329,7 +1672,9 @@
* on the interface is an error, and may be called multiple times later upon changes.
* @param ifName Name of the interface.
* @param error One of {@code TetheringManager#TETHER_ERROR_*}.
+ * @hide
*/
+ @SystemApi
default void onError(@NonNull String ifName, @TetheringIfaceError int error) {}
/**
@@ -1339,7 +1684,9 @@
* on the interface is an error, and may be called multiple times later upon changes.
* @param iface The interface that experienced the error.
* @param error One of {@code TetheringManager#TETHER_ERROR_*}.
+ * @hide
*/
+ @SystemApi
default void onError(@NonNull TetheringInterface iface, @TetheringIfaceError int error) {
// By default, the new callback calls the old callback, so apps
// implementing the old callback just work.
@@ -1356,7 +1703,9 @@
* clients may still be reported by this callback after disconnection as the system cannot
* determine if they are still connected.
* @param clients The new set of tethered clients; the collection is not ordered.
+ * @hide
*/
+ @SystemApi
default void onClientsChanged(@NonNull Collection<TetheredClient> clients) {}
/**
@@ -1364,7 +1713,9 @@
*
* <p>This will be called immediately after the callback is registered.
* @param status The offload status.
+ * @hide
*/
+ @SystemApi
default void onOffloadStatusChanged(@TetherOffloadStatus int status) {}
}
@@ -1458,6 +1809,7 @@
* @param callback the callback to be called when tethering has change events.
*/
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
+ @SuppressLint("UnflaggedApi")
public void registerTetheringEventCallback(@NonNull Executor executor,
@NonNull TetheringEventCallback callback) {
Objects.requireNonNull(executor);
@@ -1616,6 +1968,7 @@
Manifest.permission.TETHER_PRIVILEGED,
Manifest.permission.ACCESS_NETWORK_STATE
})
+ @SuppressLint("UnflaggedApi")
public void unregisterTetheringEventCallback(@NonNull final TetheringEventCallback callback) {
Objects.requireNonNull(callback);
@@ -1806,14 +2159,10 @@
/**
* Stop all active tethering.
- *
- * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
- * fail if a tethering entitlement check is required.
+ * @hide
*/
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void stopAllTethering() {
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "stopAllTethering caller:" + callerPkg);
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index 789d5bb..9863385 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -24,6 +24,7 @@
* @hide
*/
parcelable TetheringRequestParcel {
+ int requestType;
int tetheringType;
LinkAddress localIPv4Address;
LinkAddress staticClientAddress;
@@ -33,4 +34,5 @@
SoftApConfiguration softApConfig;
int uid;
String packageName;
+ String interfaceName;
}
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 47e2848..6d857b1 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -1,3 +1,6 @@
+# Keep JNI registered methods
+-keepclasseswithmembers,includedescriptorclasses class * { native <methods>; }
+
# Keep class's integer static field for MessageUtils to parsing their name.
-keepclassmembers class com.android.server.**,android.net.**,com.android.networkstack.** {
static final % POLICY_*;
@@ -7,18 +10,6 @@
static final % EVENT_*;
}
--keep class com.android.networkstack.tethering.util.BpfMap {
- native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TcUtils {
- native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TetheringUtils {
- native <methods>;
-}
-
# Ensure runtime-visible field annotations are kept when using R8 full mode.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-keep interface com.android.networkstack.tethering.util.Struct$Field {
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index a0604f2..609d759 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -16,9 +16,16 @@
package android.net.ip;
+import static android.net.INetd.LOCAL_NET_ID;
import static android.net.RouteInfo.RTN_UNICAST;
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_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHERING_WIGIG;
import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
@@ -45,7 +52,6 @@
import android.net.MacAddress;
import android.net.RouteInfo;
import android.net.TetheredClient;
-import android.net.TetheringManager;
import android.net.TetheringManager.TetheringRequest;
import android.net.dhcp.DhcpLeaseParcelable;
import android.net.dhcp.DhcpServerCallbacks;
@@ -69,8 +75,8 @@
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.InterfaceParams;
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;
import com.android.net.module.util.SharedLog;
@@ -293,6 +299,9 @@
private LinkAddress mIpv4Address;
+ @Nullable
+ private TetheringRequest mTetheringRequest;
+
private final TetheringMetrics mTetheringMetrics;
private final Handler mHandler;
@@ -406,6 +415,12 @@
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.
@@ -414,9 +429,11 @@
return Collections.unmodifiableList(mDhcpLeases);
}
- /** Enable this IpServer. IpServer state machine will be tethered or localHotspot state. */
- public void enable(final int requestedState, final TetheringRequest request) {
- sendMessage(CMD_TETHER_REQUESTED, requestedState, 0, request);
+ /**
+ * Enable this IpServer. IpServer state machine will be tethered or localHotspot state based on
+ * the connectivity scope of the TetheringRequest. */
+ public void enable(@NonNull final TetheringRequest request) {
+ sendMessage(CMD_TETHER_REQUESTED, 0, 0, request);
}
/** Stop this IpServer. After this is called this IpServer should not be used any more. */
@@ -580,8 +597,8 @@
@NonNull final Inet4Address dnsServer, @NonNull LinkAddress serverAddr,
@Nullable Inet4Address clientAddr) {
final boolean changePrefixOnDecline =
- (mInterfaceType == TetheringManager.TETHERING_NCM && clientAddr == null);
- final int subnetPrefixLength = mInterfaceType == TetheringManager.TETHERING_WIFI_P2P
+ (mInterfaceType == TETHERING_NCM && clientAddr == null);
+ final int subnetPrefixLength = mInterfaceType == TETHERING_WIFI_P2P
? mP2pLeasesSubnetPrefixLength : 0 /* default value */;
return new DhcpServingParamsParcelExt()
@@ -681,10 +698,10 @@
final IpPrefix ipv4Prefix = asIpPrefix(mIpv4Address);
final Boolean setIfaceUp;
- if (mInterfaceType == TetheringManager.TETHERING_WIFI
- || mInterfaceType == TetheringManager.TETHERING_WIFI_P2P
- || mInterfaceType == TetheringManager.TETHERING_ETHERNET
- || mInterfaceType == TetheringManager.TETHERING_WIGIG) {
+ if (mInterfaceType == TETHERING_WIFI
+ || mInterfaceType == TETHERING_WIFI_P2P
+ || mInterfaceType == TETHERING_ETHERNET
+ || mInterfaceType == TETHERING_WIGIG) {
// The WiFi and Ethernet stack has ownership of the interface up/down state.
// It is unclear whether the Bluetooth or USB stacks will manage their own
// state.
@@ -710,12 +727,12 @@
private boolean shouldNotConfigureBluetoothInterface() {
// Before T, bluetooth tethering configures the interface elsewhere.
- return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
+ return (mInterfaceType == TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
}
private boolean shouldUseWifiP2pDedicatedIp() {
return mIsWifiP2pDedicatedIpEnabled
- && mInterfaceType == TetheringManager.TETHERING_WIFI_P2P;
+ && mInterfaceType == TETHERING_WIFI_P2P;
}
private LinkAddress requestIpv4Address(final int scope, final boolean useLastAddress) {
@@ -835,12 +852,13 @@
}
}
- private void removeRoutesFromLocalNetwork(@NonNull final List<RouteInfo> toBeRemoved) {
- final int removalFailures = NetdUtils.removeRoutesFromLocalNetwork(
- mNetd, toBeRemoved);
+ private void removeRoutesFromNetworkAndLinkProperties(int netId,
+ @NonNull final List<RouteInfo> toBeRemoved) {
+ final int removalFailures = NetdUtils.removeRoutesFromNetwork(
+ mNetd, netId, toBeRemoved);
if (removalFailures > 0) {
- mLog.e(String.format("Failed to remove %d IPv6 routes from local table.",
- removalFailures));
+ mLog.e("Failed to remove " + removalFailures
+ + " IPv6 routes from network " + netId + ".");
}
for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
@@ -870,14 +888,15 @@
}
}
- private void addRoutesToLocalNetwork(@NonNull final List<RouteInfo> toBeAdded) {
+ private void addRoutesToNetworkAndLinkProperties(int netId,
+ @NonNull final List<RouteInfo> toBeAdded) {
// It's safe to call addInterfaceToNetwork() even if
- // the interface is already in the local_network.
- addInterfaceToNetwork(INetd.LOCAL_NET_ID, mIfaceName);
+ // the interface is already in the network.
+ addInterfaceToNetwork(netId, mIfaceName);
try {
// Add routes from local network. Note that adding routes that
// already exist does not cause an error (EEXIST is silently ignored).
- NetdUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
+ NetdUtils.addRoutesToNetwork(mNetd, netId, mIfaceName, toBeAdded);
} catch (IllegalStateException e) {
mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
return;
@@ -890,7 +909,8 @@
ArraySet<IpPrefix> deprecatedPrefixes, ArraySet<IpPrefix> newPrefixes) {
// [1] Remove the routes that are deprecated.
if (!deprecatedPrefixes.isEmpty()) {
- removeRoutesFromLocalNetwork(getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
+ removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
+ getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
}
// [2] Add only the routes that have not previously been added.
@@ -901,7 +921,8 @@
}
if (!addedPrefixes.isEmpty()) {
- addRoutesToLocalNetwork(getLocalRoutesFor(mIfaceName, addedPrefixes));
+ addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
+ getLocalRoutesFor(mIfaceName, addedPrefixes));
}
}
}
@@ -1007,11 +1028,11 @@
mLinkProperties.setInterfaceName(mIfaceName);
}
- private void maybeConfigureStaticIp(final TetheringRequest request) {
+ private void maybeConfigureStaticIp(@NonNull final TetheringRequest request) {
// Ignore static address configuration if they are invalid or null. In theory, static
// addresses should not be invalid here because TetheringManager do not allow caller to
// specify invalid static address configuration.
- if (request == null || request.getLocalIpv4Address() == null
+ if (request.getLocalIpv4Address() == null
|| request.getClientStaticIpv4Address() == null || !checkStaticAddressConfiguration(
request.getLocalIpv4Address(), request.getClientStaticIpv4Address())) {
return;
@@ -1033,13 +1054,14 @@
switch (message.what) {
case CMD_TETHER_REQUESTED:
mLastError = TETHER_ERROR_NO_ERROR;
- switch (message.arg1) {
- case STATE_LOCAL_ONLY:
- maybeConfigureStaticIp((TetheringRequest) message.obj);
+ mTetheringRequest = (TetheringRequest) message.obj;
+ switch (mTetheringRequest.getConnectivityScope()) {
+ case CONNECTIVITY_SCOPE_LOCAL:
+ maybeConfigureStaticIp(mTetheringRequest);
transitionTo(mLocalHotspotState);
break;
- case STATE_TETHERED:
- maybeConfigureStaticIp((TetheringRequest) message.obj);
+ case CONNECTIVITY_SCOPE_GLOBAL:
+ maybeConfigureStaticIp(mTetheringRequest);
transitionTo(mTetheredState);
break;
default:
@@ -1104,7 +1126,8 @@
}
try {
- NetdUtils.tetherInterface(mNetd, mIfaceName, asIpPrefix(mIpv4Address));
+ NetdUtils.tetherInterface(mNetd, LOCAL_NET_ID, mIfaceName,
+ asIpPrefix(mIpv4Address));
} catch (RemoteException | ServiceSpecificException | IllegalStateException e) {
mLog.e("Error Tethering", e);
mLastError = TETHER_ERROR_TETHER_IFACE_ERROR;
@@ -1126,7 +1149,7 @@
stopIPv6();
try {
- NetdUtils.untetherInterface(mNetd, mIfaceName);
+ NetdUtils.untetherInterface(mNetd, LOCAL_NET_ID, mIfaceName);
} catch (RemoteException | ServiceSpecificException e) {
mLastError = TETHER_ERROR_UNTETHER_IFACE_ERROR;
mLog.e("Failed to untether interface: " + e);
@@ -1203,14 +1226,14 @@
return;
}
- // Remove deprecated routes from local network.
- removeRoutesFromLocalNetwork(
- Collections.singletonList(getDirectConnectedRoute(deprecatedLinkAddress)));
+ // Remove deprecated routes from downstream network.
+ removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
+ List.of(getDirectConnectedRoute(deprecatedLinkAddress)));
mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
- // Add new routes to local network.
- addRoutesToLocalNetwork(
- Collections.singletonList(getDirectConnectedRoute(mIpv4Address)));
+ // Add new routes to downstream network.
+ addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
+ List.of(getDirectConnectedRoute(mIpv4Address)));
mLinkProperties.addLinkAddress(mIpv4Address);
// Update local DNS caching server with new IPv4 address, otherwise, dnsmasq doesn't
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index cd57c8d..900b505 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -62,6 +62,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.FrameworkConnectivityStatsLog;
import com.android.net.module.util.SharedLog;
import java.io.PrintWriter;
@@ -159,7 +160,22 @@
// operates on the context user.
final int currentUserId = getCurrentUser();
final UserHandle currentUser = UserHandle.of(currentUserId);
- final Context userContext = mContext.createContextAsUser(currentUser, 0);
+ final Context userContext;
+ try {
+ // There is no safe way to invoke this method since tethering package
+ // might not be installed for a certain user on the OEM devices,
+ // refer to b/382628161.
+ userContext = mContext.createContextAsUser(currentUser, 0);
+ } catch (IllegalStateException e) {
+ FrameworkConnectivityStatsLog.write(
+ FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_ENTITLEMENT_CREATE_CONTEXT_AS_USER_THROWS
+ );
+ // Fallback to startActivity if createContextAsUser failed.
+ mLog.e("createContextAsUser failed, fallback to startActivity", e);
+ mContext.startActivity(intent);
+ return intent;
+ }
final UserManager userManager = userContext.getSystemService(UserManager.class);
if (userManager.isAdminUser()) {
@@ -167,6 +183,11 @@
} 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 {
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index df255f3..b50831d 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -28,6 +28,7 @@
import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
@@ -42,15 +43,18 @@
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHERING_WIGIG;
+import static android.net.TetheringManager.TETHER_ERROR_BLUETOOTH_SERVICE_PENDING;
import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_REQUEST;
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+import static android.net.TetheringManager.TetheringRequest.REQUEST_TYPE_PLACEHOLDER;
import static android.net.TetheringManager.toIfaces;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
@@ -67,6 +71,12 @@
import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
import static com.android.networkstack.tethering.UpstreamNetworkMonitor.isCellular;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P_SUCCESS;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_SUCCESS;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_TETHER_WITH_PLACEHOLDER_REQUEST;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_MAIN_SM;
import android.app.usage.NetworkStatsManager;
@@ -105,10 +115,12 @@
import android.net.wifi.p2p.WifiP2pInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.os.Binder;
+import android.os.Build;
import android.os.Bundle;
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;
@@ -121,7 +133,6 @@
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
-import android.util.Pair;
import android.util.SparseArray;
import androidx.annotation.NonNull;
@@ -139,11 +150,13 @@
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.TerribleErrorLog;
import com.android.networkstack.apishim.common.BluetoothPanShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.networkstack.tethering.metrics.TetheringMetrics;
+import com.android.networkstack.tethering.metrics.TetheringStatsLog;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
import com.android.networkstack.tethering.util.VersionedBroadcastListener;
@@ -216,10 +229,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;
}
}
@@ -229,7 +244,7 @@
// Currently active tethering requests per tethering type. Only one of each type can be
// requested at a time. After a tethering type is requested, the map keeps tethering parameters
// to be used after the interface comes up asynchronously.
- private final SparseArray<TetheringRequest> mActiveTetheringRequests =
+ private final SparseArray<TetheringRequest> mPendingTetheringRequests =
new SparseArray<>();
private final Context mContext;
@@ -263,7 +278,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;
@@ -278,7 +292,7 @@
private SettingsObserver mSettingsObserver;
private BluetoothPan mBluetoothPan;
private PanServiceListener mBluetoothPanListener;
- private ArrayList<Pair<Boolean, IIntResultListener>> mPendingPanRequests;
+ private final ArrayList<IIntResultListener> mPendingPanRequestListeners;
// AIDL doesn't support Set<Integer>. Maintain a int bitmap here. When the bitmap is passed to
// TetheringManager, TetheringManager would convert it to a set of Integer types.
// mSupportedTypeBitmap should always be updated inside tethering internal thread but it may be
@@ -298,7 +312,7 @@
// This is intended to ensrure that if something calls startTethering(bluetooth) just after
// bluetooth is enabled. Before onServiceConnected is called, store the calls into this
// list and handle them as soon as onServiceConnected is called.
- mPendingPanRequests = new ArrayList<>();
+ mPendingPanRequestListeners = new ArrayList<>();
mTetherStates = new ArrayMap<>();
mConnectedClientsTracker = new ConnectedClientsTracker();
@@ -449,6 +463,10 @@
return mSettingsObserver;
}
+ boolean isTetheringWithSoftApConfigEnabled() {
+ return mDeps.isTetheringWithSoftApConfigEnabled();
+ }
+
/**
* Start to register callbacks.
* Call this function when tethering is ready to handle callback events.
@@ -590,15 +608,42 @@
// This method needs to exist because TETHERING_BLUETOOTH before Android T and TETHERING_WIGIG
// can't use enableIpServing.
private void processInterfaceStateChange(final String iface, boolean enabled) {
+ final int type = ifaceNameToType(iface);
// Do not listen to USB interface state changes or USB interface add/removes. USB tethering
// is driven only by USB_ACTION broadcasts.
- final int type = ifaceNameToType(iface);
if (type == TETHERING_USB || type == TETHERING_NCM) return;
+ // On T+, BLUETOOTH uses enableIpServing.
if (type == TETHERING_BLUETOOTH && SdkLevel.isAtLeastT()) return;
+ // Cannot happen: on S+, tetherableWigigRegexps is always empty.
+ if (type == TETHERING_WIGIG && SdkLevel.isAtLeastS()) return;
+
+ // After V, disallow this legacy codepath from starting tethering of any type:
+ // everything must call ensureIpServerStarted directly.
+ //
+ // Don't touch the teardown path for now. It's more complicated because:
+ // - ensureIpServerStarted and ensureIpServerStopped act on different
+ // tethering types.
+ // - Depending on the type, ensureIpServerStopped is either called twice (once
+ // on interface down and once on interface removed) or just once (on
+ // interface removed).
+ //
+ // Note that this only affects WIFI and WIFI_P2P. The other types are either
+ // ignored above, or ignored by ensureIpServerStarted. Note that even for WIFI
+ // and WIFI_P2P, this code should not ever run in normal use, because the
+ // hotspot and p2p code do not call tether(). It's possible that this could
+ // happen in the field due to unforeseen OEM modifications. If it does happen,
+ // a terrible error is logged in tether().
+ // TODO: fix the teardown path to stop depending on interface state notifications.
+ // These are not necessary since most/all link layers have their own teardown
+ // notifications, and can race with those notifications.
+ if (enabled && Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ return;
+ }
+
if (enabled) {
- ensureIpServerStarted(iface);
+ ensureIpServerStartedForInterface(iface);
} else {
ensureIpServerStopped(iface);
}
@@ -659,14 +704,14 @@
final IIntResultListener listener) {
mHandler.post(() -> {
final int type = request.getTetheringType();
- final TetheringRequest unfinishedRequest = mActiveTetheringRequests.get(type);
+ final TetheringRequest unfinishedRequest = mPendingTetheringRequests.get(type);
// If tethering is already enabled with a different request,
// disable before re-enabling.
- if (unfinishedRequest != null && !unfinishedRequest.equals(request)) {
- enableTetheringInternal(type, false /* disabled */, null);
+ if (unfinishedRequest != null && !unfinishedRequest.equalsIgnoreUidPackage(request)) {
+ enableTetheringInternal(false /* disabled */, unfinishedRequest, null);
mEntitlementMgr.stopProvisioningIfNeeded(type);
}
- mActiveTetheringRequests.put(type, request);
+ mPendingTetheringRequests.put(type, request);
if (request.isExemptFromEntitlementCheck()) {
mEntitlementMgr.setExemptedDownstreamType(type);
@@ -674,7 +719,7 @@
mEntitlementMgr.startProvisioningIfNeeded(type,
request.getShouldShowEntitlementUi());
}
- enableTetheringInternal(type, true /* enabled */, listener);
+ enableTetheringInternal(true /* enabled */, request, listener);
mTetheringMetrics.createBuilder(type, callerPkg);
});
}
@@ -684,10 +729,47 @@
stopTetheringInternal(type);
});
}
- void stopTetheringInternal(int type) {
- mActiveTetheringRequests.remove(type);
- enableTetheringInternal(type, false /* disabled */, null);
+ private boolean isTetheringTypePendingOrServing(final int type) {
+ for (int i = 0; i < mPendingTetheringRequests.size(); i++) {
+ if (mPendingTetheringRequests.valueAt(i).getTetheringType() == type) return true;
+ }
+ for (TetherState state : mTetherStates.values()) {
+ // TODO: isCurrentlyServing only starts returning true once the IpServer has processed
+ // the CMD_TETHER_REQUESTED. Ensure that we consider the request to be serving even when
+ // that has not happened yet.
+ if (state.isCurrentlyServing() && state.ipServer.interfaceType() == type) return true;
+ }
+ return false;
+ }
+
+ void stopTetheringRequest(@NonNull final TetheringRequest request,
+ @NonNull final IIntResultListener listener) {
+ if (!isTetheringWithSoftApConfigEnabled()) return;
+ mHandler.post(() -> {
+ final int type = request.getTetheringType();
+ if (isTetheringTypePendingOrServing(type)) {
+ stopTetheringInternal(type);
+ try {
+ listener.onResult(TETHER_ERROR_NO_ERROR);
+ } catch (RemoteException ignored) { }
+ return;
+ }
+
+ // Request doesn't match any active requests, ignore.
+ try {
+ listener.onResult(TETHER_ERROR_UNKNOWN_REQUEST);
+ } catch (RemoteException ignored) { }
+ });
+ }
+
+ void stopTetheringInternal(int type) {
+ mPendingTetheringRequests.remove(type);
+
+ // Using a placeholder here is ok since none of the disable APIs use the request for
+ // anything. We simply need the tethering type to know which link layer to poke for removal.
+ // TODO: Remove the placeholder here and loop through each pending/serving request.
+ enableTetheringInternal(false /* disabled */, createPlaceholderRequest(type), null);
mEntitlementMgr.stopProvisioningIfNeeded(type);
}
@@ -695,9 +777,10 @@
* Enables or disables tethering for the given type. If provisioning is required, it will
* schedule provisioning rechecks for the specified interface.
*/
- private void enableTetheringInternal(int type, boolean enable,
+ private void enableTetheringInternal(boolean enable, @NonNull final TetheringRequest request,
final IIntResultListener listener) {
- int result = TETHER_ERROR_NO_ERROR;
+ final int type = request.getTetheringType();
+ final int result;
switch (type) {
case TETHERING_WIFI:
result = setWifiTethering(enable);
@@ -706,7 +789,7 @@
result = setUsbTethering(enable);
break;
case TETHERING_BLUETOOTH:
- setBluetoothTethering(enable, listener);
+ result = setBluetoothTethering(enable, listener);
break;
case TETHERING_NCM:
result = setNcmTethering(enable);
@@ -715,17 +798,17 @@
result = setEthernetTethering(enable);
break;
case TETHERING_VIRTUAL:
- result = setVirtualMachineTethering(enable);
+ result = setVirtualMachineTethering(enable, request);
break;
default:
Log.w(TAG, "Invalid tether type.");
result = TETHER_ERROR_UNKNOWN_TYPE;
}
- // The result of Bluetooth tethering will be sent by #setBluetoothTethering.
- if (type != TETHERING_BLUETOOTH) {
- sendTetherResult(listener, result, type);
- }
+ // The result of Bluetooth tethering will be sent after the pan service connects.
+ if (result == TETHER_ERROR_BLUETOOTH_SERVICE_PENDING) return;
+
+ sendTetherResult(listener, result, type);
}
private void sendTetherResult(final IIntResultListener listener, final int result,
@@ -739,7 +822,7 @@
// If changing tethering fail, remove corresponding request
// no matter who trigger the start/stop.
if (result != TETHER_ERROR_NO_ERROR) {
- mActiveTetheringRequests.remove(type);
+ mPendingTetheringRequests.remove(type);
mTetheringMetrics.updateErrorCode(type, result);
mTetheringMetrics.sendReport(type);
}
@@ -764,13 +847,12 @@
return TETHER_ERROR_INTERNAL_ERROR;
}
- private void setBluetoothTethering(final boolean enable, final IIntResultListener listener) {
+ private int setBluetoothTethering(final boolean enable, final IIntResultListener listener) {
final BluetoothAdapter adapter = mDeps.getBluetoothAdapter();
if (adapter == null || !adapter.isEnabled()) {
Log.w(TAG, "Tried to enable bluetooth tethering with null or disabled adapter. null: "
+ (adapter == null));
- sendTetherResult(listener, TETHER_ERROR_SERVICE_UNAVAIL, TETHERING_BLUETOOTH);
- return;
+ return TETHER_ERROR_SERVICE_UNAVAIL;
}
if (mBluetoothPanListener != null && mBluetoothPanListener.isConnected()) {
@@ -778,16 +860,21 @@
// When bluetooth tethering is enabled, any time a PAN client pairs with this
// host, bluetooth will bring up a bt-pan interface and notify tethering to
// enable IP serving.
- setBluetoothTetheringSettings(mBluetoothPan, enable, listener);
- return;
+ return setBluetoothTetheringSettings(mBluetoothPan, enable);
}
- // The reference of IIntResultListener should only exist when application want to start
- // tethering but tethering is not bound to pan service yet. Even if the calling process
- // dies, the referenice of IIntResultListener would still keep in mPendingPanRequests. Once
- // tethering bound to pan service (onServiceConnected) or bluetooth just crash
- // (onServiceDisconnected), all the references from mPendingPanRequests would be cleared.
- mPendingPanRequests.add(new Pair(enable, listener));
+ if (!enable) {
+ // The service is not connected. If disabling tethering, there's no point starting
+ // the service just to stop tethering since tethering is not started. Just remove
+ // any pending requests to enable tethering, and notify them that they have failed.
+ for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+ sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
+ TETHERING_BLUETOOTH);
+ }
+ mPendingPanRequestListeners.clear();
+ return TETHER_ERROR_NO_ERROR;
+ }
+ mPendingPanRequestListeners.add(listener);
// Bluetooth tethering is not a popular feature. To avoid bind to bluetooth pan service all
// the time but user never use bluetooth tethering. mBluetoothPanListener is created first
@@ -797,6 +884,7 @@
mBluetoothPanListener = new PanServiceListener();
adapter.getProfileProxy(mContext, mBluetoothPanListener, BluetoothProfile.PAN);
}
+ return TETHER_ERROR_BLUETOOTH_SERVICE_PENDING;
}
private class PanServiceListener implements ServiceListener {
@@ -813,10 +901,12 @@
mBluetoothPan = (BluetoothPan) proxy;
mIsConnected = true;
- for (Pair<Boolean, IIntResultListener> request : mPendingPanRequests) {
- setBluetoothTetheringSettings(mBluetoothPan, request.first, request.second);
+ for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+ final int result = setBluetoothTetheringSettings(mBluetoothPan,
+ true /* enable */);
+ sendTetherResult(pendingListener, result, TETHERING_BLUETOOTH);
}
- mPendingPanRequests.clear();
+ mPendingPanRequestListeners.clear();
});
}
@@ -827,11 +917,11 @@
// reachable before next onServiceConnected.
mIsConnected = false;
- for (Pair<Boolean, IIntResultListener> request : mPendingPanRequests) {
- sendTetherResult(request.second, TETHER_ERROR_SERVICE_UNAVAIL,
+ for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+ sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
TETHERING_BLUETOOTH);
}
- mPendingPanRequests.clear();
+ mPendingPanRequestListeners.clear();
mBluetoothIfaceRequest = null;
mBluetoothCallback = null;
maybeDisableBluetoothIpServing();
@@ -843,8 +933,8 @@
}
}
- private void setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
- final boolean enable, final IIntResultListener listener) {
+ private int setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
+ final boolean enable) {
if (SdkLevel.isAtLeastT()) {
changeBluetoothTetheringSettings(bluetoothPan, enable);
} else {
@@ -853,9 +943,8 @@
// Enabling bluetooth tethering settings can silently fail. Send internal error if the
// result is not expected.
- final int result = bluetoothPan.isTetheringOn() == enable
+ return bluetoothPan.isTetheringOn() == enable
? TETHER_ERROR_NO_ERROR : TETHER_ERROR_INTERNAL_ERROR;
- sendTetherResult(listener, result, TETHERING_BLUETOOTH);
}
private void changeBluetoothTetheringSettingsPreT(@NonNull final BluetoothPan bluetoothPan,
@@ -901,7 +990,9 @@
public void onAvailable(String iface) {
if (this != mBluetoothCallback) return;
- enableIpServing(TETHERING_BLUETOOTH, iface, getRequestedState(TETHERING_BLUETOOTH));
+ final TetheringRequest request =
+ getOrCreatePendingTetheringRequest(TETHERING_BLUETOOTH);
+ enableIpServing(request, iface);
mConfiguredBluetoothIface = iface;
}
@@ -956,7 +1047,9 @@
// Ethernet callback arrived after Ethernet tethering stopped. Ignore.
return;
}
- enableIpServing(TETHERING_ETHERNET, iface, getRequestedState(TETHERING_ETHERNET));
+
+ final TetheringRequest request = getOrCreatePendingTetheringRequest(TETHERING_ETHERNET);
+ enableIpServing(request, iface);
mConfiguredEthernetIface = iface;
}
@@ -970,14 +1063,16 @@
}
}
- private int setVirtualMachineTethering(final boolean enable) {
- // TODO(340377643): Use bridge ifname when it's introduced, not fixed TAP ifname.
+ private int setVirtualMachineTethering(final boolean enable,
+ @NonNull final TetheringRequest request) {
+ final String iface = request.getInterfaceName();
if (enable) {
- mConfiguredVirtualIface = "avf_tap_fixed";
- enableIpServing(
- TETHERING_VIRTUAL,
- mConfiguredVirtualIface,
- getRequestedState(TETHERING_VIRTUAL));
+ if (TextUtils.isEmpty(iface)) {
+ mConfiguredVirtualIface = "avf_tap_fixed";
+ } else {
+ mConfiguredVirtualIface = iface;
+ }
+ enableIpServing(request, mConfiguredVirtualIface);
} else if (mConfiguredVirtualIface != null) {
ensureIpServerStopped(mConfiguredVirtualIface);
mConfiguredVirtualIface = null;
@@ -985,15 +1080,132 @@
return TETHER_ERROR_NO_ERROR;
}
- void tether(String iface, int requestedState, final IIntResultListener listener) {
- mHandler.post(() -> {
- try {
- listener.onResult(tether(iface, requestedState));
- } catch (RemoteException e) { }
- });
+ /**
+ * Create a legacy tethering request for calls to the legacy tether() API, which doesn't take an
+ * explicit request. These are always CONNECTIVITY_SCOPE_GLOBAL, per historical behavior.
+ */
+ private TetheringRequest createLegacyGlobalScopeTetheringRequest(int type) {
+ final TetheringRequest request = new TetheringRequest.Builder(type).build();
+ request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_LEGACY;
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
+ return request;
}
- private int tether(String iface, int requestedState) {
+ /**
+ * Create a local-only implicit tethering request. This is used for Wifi local-only hotspot and
+ * Wifi P2P, which start tethering based on the WIFI_(AP/P2P)_STATE_CHANGED broadcasts.
+ */
+ @NonNull
+ private TetheringRequest createImplicitLocalOnlyTetheringRequest(int type) {
+ final TetheringRequest request = new TetheringRequest.Builder(type).build();
+ request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_IMPLICIT;
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
+ return request;
+ }
+
+ /**
+ * Create a placeholder request. This is used in case we try to find a pending request but there
+ * is none (e.g. stopTethering removed a pending request), or for cases where we only have the
+ * tethering type (e.g. stopTethering(int)).
+ */
+ @NonNull
+ private TetheringRequest createPlaceholderRequest(int type) {
+ final TetheringRequest request = new TetheringRequest.Builder(type).build();
+ request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_LEGACY;
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
+ return request;
+ }
+
+ /**
+ * Gets the TetheringRequest that #startTethering was called with but is waiting for the link
+ * layer event to indicate the interface is available to tether.
+ * Note: There are edge cases where the pending request is absent and we must temporarily
+ * synthesize a placeholder request, such as if stopTethering was called before link layer
+ * went up, or if the link layer goes up without us poking it (e.g. adb shell cmd wifi
+ * start-softap). These placeholder requests only specify the tethering type and the
+ * default connectivity scope.
+ */
+ @NonNull
+ private TetheringRequest getOrCreatePendingTetheringRequest(int type) {
+ TetheringRequest pending = mPendingTetheringRequests.get(type);
+ if (pending != null) return pending;
+
+ Log.w(TAG, "No pending TetheringRequest for type " + type + " found, creating a placeholder"
+ + " request");
+ return createPlaceholderRequest(type);
+ }
+
+ private void handleLegacyTether(String iface, final IIntResultListener listener) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ // After V, the TetheringManager and ConnectivityManager tether and untether methods
+ // throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
+ // that this code cannot run even if callers use raw binder calls or other
+ // unsupported methods.
+ return;
+ }
+
+ final int type = ifaceNameToType(iface);
+ if (type == TETHERING_INVALID) {
+ Log.e(TAG, "Ignoring call to legacy tether for unknown iface " + iface);
+ try {
+ listener.onResult(TETHER_ERROR_UNKNOWN_IFACE);
+ } catch (RemoteException e) { }
+ }
+
+ final TetheringRequest request = createLegacyGlobalScopeTetheringRequest(type);
+ int result = tetherInternal(request, iface);
+ switch (type) {
+ case TETHERING_WIFI:
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API called on Wifi iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI);
+ if (result == TETHER_ERROR_NO_ERROR) {
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API succeeded on Wifi iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_SUCCESS);
+ }
+ break;
+ case TETHERING_WIFI_P2P:
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API called on Wifi P2P iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P);
+ if (result == TETHER_ERROR_NO_ERROR) {
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Legacy tether API succeeded on Wifi P2P iface " + iface,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P_SUCCESS);
+ }
+ break;
+ default:
+ // Do nothing
+ break;
+ }
+ try {
+ listener.onResult(result);
+ } catch (RemoteException e) { }
+ }
+
+ /**
+ * Legacy tether API that starts tethering with CONNECTIVITY_SCOPE_GLOBAL on the given iface.
+ *
+ * This API relies on the IpServer having been started for the interface by
+ * processInterfaceStateChanged beforehand, which is only possible for
+ * - WIGIG Pre-S
+ * - BLUETOOTH Pre-T
+ * - WIFI
+ * - WIFI_P2P.
+ * Note that WIFI and WIFI_P2P already start tethering on their respective ifaces via
+ * WIFI_(AP/P2P_STATE_CHANGED broadcasts, which makes this API redundant for those types unless
+ * those broadcasts are disabled by OEM.
+ */
+ void legacyTether(String iface, final IIntResultListener listener) {
+ mHandler.post(() -> handleLegacyTether(iface, listener));
+ }
+
+ private int tetherInternal(@NonNull TetheringRequest request, String iface) {
if (DBG) Log.d(TAG, "Tethering " + iface);
TetherState tetherState = mTetherStates.get(iface);
if (tetherState == null) {
@@ -1006,29 +1218,38 @@
Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
return TETHER_ERROR_UNAVAIL_IFACE;
}
- // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's queue but not yet
+ // NOTE: If a CMD_TETHER_REQUESTED message is already in the IpServer's queue but not yet
// processed, this will be a no-op and it will not return an error.
//
// This code cannot race with untether() because they both run on the handler thread.
- final int type = tetherState.ipServer.interfaceType();
- final TetheringRequest request = mActiveTetheringRequests.get(type, null);
- if (request != null) {
- mActiveTetheringRequests.delete(type);
+ mPendingTetheringRequests.remove(request.getTetheringType());
+ tetherState.ipServer.enable(request);
+ if (request.getRequestType() == REQUEST_TYPE_PLACEHOLDER) {
+ TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
+ "Started tethering with placeholder request: " + request,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_TETHER_WITH_PLACEHOLDER_REQUEST);
}
- tetherState.ipServer.enable(requestedState, request);
return TETHER_ERROR_NO_ERROR;
}
- void untether(String iface, final IIntResultListener listener) {
+ void legacyUntether(String iface, final IIntResultListener listener) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ // After V, the TetheringManager and ConnectivityManager tether and untether methods
+ // throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
+ // that this code cannot run even if callers use raw binder calls or other
+ // unsupported methods.
+ return;
+ }
mHandler.post(() -> {
try {
- listener.onResult(untether(iface));
+ listener.onResult(legacyUntetherInternal(iface));
} catch (RemoteException e) {
}
});
}
- int untether(String iface) {
+ int legacyUntetherInternal(String iface) {
if (DBG) Log.d(TAG, "Untethering " + iface);
TetherState tetherState = mTetherStates.get(iface);
if (tetherState == null) {
@@ -1043,7 +1264,7 @@
return TETHER_ERROR_NO_ERROR;
}
- void untetherAll() {
+ void stopAllTethering() {
stopTethering(TETHERING_WIFI);
stopTethering(TETHERING_WIFI_P2P);
stopTethering(TETHERING_USB);
@@ -1067,22 +1288,6 @@
return mEntitlementMgr.isTetherProvisioningRequired(cfg);
}
- private int getRequestedState(int type) {
- final TetheringRequest request = mActiveTetheringRequests.get(type);
-
- // The request could have been deleted before we had a chance to complete it.
- // If so, assume that the scope is the default scope for this tethering type.
- // This likely doesn't matter - if the request has been deleted, then tethering is
- // likely going to be stopped soon anyway.
- final int connectivityScope = (request != null)
- ? request.getConnectivityScope()
- : TetheringRequest.getDefaultConnectivityScope(type);
-
- return connectivityScope == CONNECTIVITY_SCOPE_LOCAL
- ? IpServer.STATE_LOCAL_ONLY
- : IpServer.STATE_TETHERED;
- }
-
private int getServedUsbType(boolean forNcmFunction) {
// TETHERING_NCM is only used if the device does not use NCM for regular USB tethering.
if (forNcmFunction && !mConfig.isUsingNcm()) return TETHERING_NCM;
@@ -1091,21 +1296,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 +1357,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 +1375,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 {
@@ -1204,7 +1418,7 @@
mLog.log("OBSERVED data saver changed");
handleDataSaverChanged();
} else if (action.equals(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING)) {
- untetherAll();
+ stopAllTethering();
}
}
@@ -1368,14 +1582,14 @@
mDataSaverEnabled = isDataSaverEnabled;
if (mDataSaverEnabled) {
- untetherAll();
+ stopAllTethering();
}
}
}
@VisibleForTesting
- SparseArray<TetheringRequest> getActiveTetheringRequests() {
- return mActiveTetheringRequests;
+ SparseArray<TetheringRequest> getPendingTetheringRequests() {
+ return mPendingTetheringRequests;
}
@VisibleForTesting
@@ -1427,7 +1641,7 @@
mNotificationUpdater.notifyTetheringDisabledByRestriction();
// Untether from all downstreams since tethering is disallowed.
- mTethering.untetherAll();
+ mTethering.stopAllTethering();
}
return true;
@@ -1435,14 +1649,17 @@
}
}
- private void enableIpServing(int tetheringType, String ifname, int ipServingMode) {
- enableIpServing(tetheringType, ifname, ipServingMode, false /* isNcm */);
+ final TetheringRequest getPendingTetheringRequest(int type) {
+ return mPendingTetheringRequests.get(type, null);
}
- private void enableIpServing(int tetheringType, String ifname, int ipServingMode,
- boolean isNcm) {
- ensureIpServerStarted(ifname, tetheringType, isNcm);
- if (tether(ifname, ipServingMode) != TETHER_ERROR_NO_ERROR) {
+ private void enableIpServing(@NonNull TetheringRequest request, String ifname) {
+ enableIpServing(request, ifname, false /* isNcm */);
+ }
+
+ private void enableIpServing(@NonNull TetheringRequest request, String ifname, boolean isNcm) {
+ ensureIpServerStartedForType(ifname, request.getTetheringType(), isNcm);
+ if (tetherInternal(request, ifname) != TETHER_ERROR_NO_ERROR) {
Log.e(TAG, "unable start tethering on iface " + ifname);
}
}
@@ -1494,7 +1711,10 @@
mLog.e(ifname + " is not a tetherable iface, ignoring");
return;
}
- enableIpServing(type, ifname, IpServer.STATE_LOCAL_ONLY);
+ // No need to call getOrCreatePendingRequest. There can never be explicit requests for
+ // TETHERING_WIFI_P2P because enableTetheringInternal ignores that type.
+ final TetheringRequest request = createImplicitLocalOnlyTetheringRequest(type);
+ enableIpServing(request, ifname);
}
private void disableWifiP2pIpServingIfNeeded(String ifname) {
@@ -1504,17 +1724,34 @@
disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname);
}
+ // TODO: fold this in to enableWifiIpServing. We cannot do this at the moment because there
+ // are tests that send wifi AP broadcasts with a null interface. But if this can't happen on
+ // real devices, we should fix those tests to always pass in an interface.
+ private int maybeInferWifiTetheringType(String ifname) {
+ return SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
+ }
+
private void enableWifiIpServing(String ifname, int wifiIpMode) {
mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
// Map wifiIpMode values to IpServer.Callback serving states.
- final int ipServingMode;
+ TetheringRequest request;
+ final int type;
switch (wifiIpMode) {
case IFACE_IP_MODE_TETHERED:
- ipServingMode = IpServer.STATE_TETHERED;
+ type = maybeInferWifiTetheringType(ifname);
+ request = getOrCreatePendingTetheringRequest(type);
+ // Wifi requests will always have CONNECTIVITY_SCOPE_GLOBAL, because
+ // TetheringRequest.Builder will not allow callers to set CONNECTIVITY_SCOPE_LOCAL
+ // for TETHERING_WIFI. However, if maybeInferWifiTetheringType returns a non-Wifi
+ // type (which could happen on a pre-T implementation of Wi-Fi if the regexps are
+ // misconfigured), then force the connectivity scope to global in order to match the
+ // historical behavior.
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
break;
case IFACE_IP_MODE_LOCAL_ONLY:
- ipServingMode = IpServer.STATE_LOCAL_ONLY;
+ type = maybeInferWifiTetheringType(ifname);
+ request = createImplicitLocalOnlyTetheringRequest(type);
break;
default:
mLog.e("Cannot enable IP serving in unknown WiFi mode: " + wifiIpMode);
@@ -1523,14 +1760,13 @@
// After T, tethering always trust the iface pass by state change intent. This allow
// tethering to deprecate tetherable wifi regexs after T.
- final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
if (!checkTetherableType(type)) {
mLog.e(ifname + " is not a tetherable iface, ignoring");
return;
}
if (!TextUtils.isEmpty(ifname)) {
- enableIpServing(type, ifname, ipServingMode);
+ enableIpServing(request, ifname);
} else {
mLog.e("Cannot enable IP serving on missing interface name");
}
@@ -1551,7 +1787,6 @@
// for both TETHERING_USB and TETHERING_NCM, so the local-only NCM interface will be
// stopped immediately.
final int tetheringType = getServedUsbType(forNcmFunction);
- final int requestedState = getRequestedState(tetheringType);
String[] ifaces = null;
try {
ifaces = mNetd.interfaceGetList();
@@ -1560,10 +1795,11 @@
return;
}
+ final TetheringRequest request = getOrCreatePendingTetheringRequest(tetheringType);
if (ifaces != null) {
for (String iface : ifaces) {
if (ifaceNameToType(iface) == tetheringType) {
- enableIpServing(tetheringType, iface, requestedState, forNcmFunction);
+ enableIpServing(request, iface, forNcmFunction);
return;
}
}
@@ -2074,7 +2310,7 @@
}
mRoutingCoordinator.maybeRemoveDeprecatedUpstreams();
- mUpstreamNetworkMonitor.startObserveAllNetworks();
+ mUpstreamNetworkMonitor.startObserveUpstreamNetworks();
// TODO: De-duplicate with updateUpstreamWanted() below.
if (upstreamWanted()) {
@@ -2192,9 +2428,14 @@
break;
}
case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
- final int tetheringType = message.arg1;
+ final int type = message.arg1;
final Boolean enabled = (Boolean) message.obj;
- enableTetheringInternal(tetheringType, enabled, null);
+ // Using a placeholder here is ok since we just need to the type of
+ // tethering to poke the link layer. When the link layer comes up, we won't
+ // have a pending request to use, but this matches the historical behavior.
+ // TODO: Get the TetheringRequest from IpServer and make sure to put it in
+ // the pending list too.
+ enableTetheringInternal(enabled, createPlaceholderRequest(type), null);
break;
}
default:
@@ -2391,19 +2632,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;
@@ -2415,17 +2656,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;
}
@@ -2484,12 +2714,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.
}
@@ -2497,6 +2729,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) {
@@ -2506,7 +2750,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.
@@ -2741,7 +2985,7 @@
return;
}
mTetherMainSM.sendMessage(which, state, 0, who);
- sendTetherStateChangedBroadcast();
+ notifyTetherStatesChanged();
}
@Override
@@ -2792,7 +3036,7 @@
return type != TETHERING_INVALID;
}
- private void ensureIpServerStarted(final String iface) {
+ private void ensureIpServerStartedForInterface(final String iface) {
// If we don't care about this type of interface, ignore.
final int interfaceType = ifaceNameToType(iface);
if (!checkTetherableType(interfaceType)) {
@@ -2801,10 +3045,11 @@
return;
}
- ensureIpServerStarted(iface, interfaceType, false /* isNcm */);
+ ensureIpServerStartedForType(iface, interfaceType, false /* isNcm */);
}
- private void ensureIpServerStarted(final String iface, int interfaceType, boolean isNcm) {
+ private void ensureIpServerStartedForType(final String iface, int interfaceType,
+ boolean isNcm) {
// If we have already started a TISM for this interface, skip.
if (mTetherStates.containsKey(iface)) {
mLog.log("active iface (" + iface + ") reported as added, ignoring");
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index a4823ca..8e17085 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -25,6 +25,7 @@
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;
@@ -36,7 +37,7 @@
import androidx.annotation.RequiresApi;
import com.android.modules.utils.build.SdkLevel;
-import com.android.net.module.util.PrivateAddressCoordinator;
+import com.android.net.flags.Flags;
import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.net.module.util.RoutingCoordinatorService;
import com.android.net.module.util.SharedLog;
@@ -201,4 +202,19 @@
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();
+ }
+
+ /**
+ * Wrapper for tethering_with_soft_ap_config feature flag.
+ */
+ public boolean isTetheringWithSoftApConfigEnabled() {
+ return Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM
+ && Flags.tetheringWithSoftApConfig();
+ }
}
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 cea7e82..b553f46 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;
@@ -55,7 +56,6 @@
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.net.module.util.PermissionUtils;
import com.android.networkstack.apishim.SettingsShimImpl;
import com.android.networkstack.apishim.common.SettingsShim;
@@ -111,7 +111,7 @@
IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
- mTethering.tether(iface, IpServer.STATE_TETHERED, listener);
+ mTethering.legacyTether(iface, listener);
}
@Override
@@ -119,7 +119,7 @@
IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
- mTethering.untether(iface, listener);
+ mTethering.legacyUntether(iface, listener);
}
@Override
@@ -133,9 +133,11 @@
@Override
public void startTethering(TetheringRequestParcel request, String callerPkg,
String callingAttributionTag, IIntResultListener listener) {
+ boolean onlyAllowPrivileged = request.exemptFromEntitlementCheck
+ || request.interfaceName != null;
if (checkAndNotifyCommonError(callerPkg,
callingAttributionTag,
- request.exemptFromEntitlementCheck /* onlyAllowPrivileged */,
+ onlyAllowPrivileged,
listener)) {
return;
}
@@ -157,6 +159,18 @@
}
@Override
+ public void stopTetheringRequest(TetheringRequest request,
+ String callerPkg, String callingAttributionTag,
+ IIntResultListener listener) {
+ if (request == null) return;
+ if (listener == null) return;
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+ request.setUid(getBinderCallingUid());
+ request.setPackageName(callerPkg);
+ mTethering.stopTetheringRequest(request, listener);
+ }
+
+ @Override
public void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
boolean showEntitlementUi, String callerPkg, String callingAttributionTag) {
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, receiver)) return;
@@ -198,7 +212,7 @@
if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
try {
- mTethering.untetherAll();
+ mTethering.stopAllTethering();
listener.onResult(TETHER_ERROR_NO_ERROR);
} catch (RemoteException e) { }
}
@@ -284,6 +298,10 @@
return false;
}
+ private boolean hasNetworkSettingsPermission() {
+ return checkCallingOrSelfPermission(NETWORK_SETTINGS);
+ }
+
private boolean hasNetworkStackPermission() {
return checkCallingOrSelfPermission(NETWORK_STACK)
|| checkCallingOrSelfPermission(PERMISSION_MAINLINE_NETWORK_STACK);
@@ -299,10 +317,15 @@
private boolean hasTetherChangePermission(final String callerPkg,
final String callingAttributionTag, final boolean onlyAllowPrivileged) {
- if (onlyAllowPrivileged && !hasNetworkStackPermission()) return false;
+ if (onlyAllowPrivileged && !hasNetworkStackPermission()
+ && !hasNetworkSettingsPermission()) return false;
if (hasTetherPrivilegedPermission()) return true;
+ // After TetheringManager moves to public API, prevent third-party apps from being able
+ // to change tethering with only WRITE_SETTINGS permission.
+ if (mTethering.isTetheringWithSoftApConfigEnabled()) return false;
+
if (mTethering.isTetherProvisioningRequired()) return false;
int uid = getBinderCallingUid();
@@ -351,7 +374,11 @@
boolean checkPackageNameMatchesUid(@NonNull Context context, int uid,
@NonNull String callingPackage) {
try {
- PermissionUtils.enforcePackageNameMatchesUid(context, uid, callingPackage);
+ final AppOpsManager mAppOps = context.getSystemService(AppOpsManager.class);
+ if (mAppOps == null) {
+ return false;
+ }
+ mAppOps.checkPackage(uid, callingPackage);
} catch (SecurityException e) {
return false;
}
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index 7a05d74..9705d84 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -24,6 +24,7 @@
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -44,6 +45,7 @@
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
import com.android.networkstack.apishim.common.ConnectivityManagerShim;
@@ -62,9 +64,10 @@
* The owner of UNM gets it to register network callbacks by calling the
* following methods :
* Calling #startTrackDefaultNetwork() to track the system default network.
- * Calling #startObserveAllNetworks() to observe all networks. Listening all
- * networks is necessary while the expression of preferred upstreams remains
- * a list of legacy connectivity types. In future, this can be revisited.
+ * Calling #startObserveUpstreamNetworks() to observe upstream networks.
+ * Listening all upstream networks is necessary while the expression of
+ * preferred upstreams remains a list of legacy connectivity types.
+ * In future, this can be revisited.
* Calling #setTryCell() to request bringing up mobile DUN or HIPRI.
*
* The methods and data members of this class are only to be accessed and
@@ -94,7 +97,7 @@
@VisibleForTesting
public static final int TYPE_NONE = -1;
- private static final int CALLBACK_LISTEN_ALL = 1;
+ private static final int CALLBACK_LISTEN_UPSTREAM = 1;
private static final int CALLBACK_DEFAULT_INTERNET = 2;
private static final int CALLBACK_MOBILE_REQUEST = 3;
@@ -116,7 +119,7 @@
private HashSet<IpPrefix> mLocalPrefixes;
private ConnectivityManager mCM;
private EntitlementManager mEntitlementMgr;
- private NetworkCallback mListenAllCallback;
+ private NetworkCallback mListenUpstreamCallback;
private NetworkCallback mDefaultNetworkCallback;
private NetworkCallback mMobileNetworkCallback;
@@ -157,20 +160,29 @@
}
ConnectivityManagerShim mCmShim = ConnectivityManagerShimImpl.newInstance(mContext);
mDefaultNetworkCallback = new UpstreamNetworkCallback(CALLBACK_DEFAULT_INTERNET);
+ // TODO (b/382413665): By definition, a local network cannot be the system default,
+ // because it does not provide internet capability. Figure out whether this
+ // is enforced in ConnectivityService. Or what will happen for tethering if it happens.
mCmShim.registerSystemDefaultNetworkCallback(mDefaultNetworkCallback, mHandler);
if (mEntitlementMgr == null) {
mEntitlementMgr = entitle;
}
}
- /** Listen all networks. */
- public void startObserveAllNetworks() {
+ /** Listen upstream networks. */
+ public void startObserveUpstreamNetworks() {
stop();
- final NetworkRequest listenAllRequest = new NetworkRequest.Builder()
- .clearCapabilities().build();
- mListenAllCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_ALL);
- cm().registerNetworkCallback(listenAllRequest, mListenAllCallback, mHandler);
+ final NetworkRequest listenUpstreamRequest;
+ // Before V, only TV supports local agent on U, which doesn't support tethering.
+ if (SdkLevel.isAtLeastV()) {
+ listenUpstreamRequest = new NetworkRequest.Builder().clearCapabilities()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK).build();
+ } else {
+ listenUpstreamRequest = new NetworkRequest.Builder().clearCapabilities().build();
+ }
+ mListenUpstreamCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_UPSTREAM);
+ cm().registerNetworkCallback(listenUpstreamRequest, mListenUpstreamCallback, mHandler);
}
/**
@@ -183,8 +195,8 @@
public void stop() {
setTryCell(false);
- releaseCallback(mListenAllCallback);
- mListenAllCallback = null;
+ releaseCallback(mListenUpstreamCallback);
+ mListenUpstreamCallback = null;
mNetworkMap.clear();
}
@@ -535,10 +547,10 @@
return;
}
- // Any non-LISTEN_ALL callback will necessarily concern a network that will
- // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
- // So it's not useful to do this work for non-LISTEN_ALL callbacks.
- if (mCallbackType == CALLBACK_LISTEN_ALL) {
+ // Any non-LISTEN_UPSTREAM callback will necessarily concern a network that will
+ // also match the LISTEN_UPSTREAM callback by construction of the LISTEN_UPSTREAM
+ // callback. So it's not useful to do this work for non-LISTEN_UPSTREAM callbacks.
+ if (mCallbackType == CALLBACK_LISTEN_UPSTREAM) {
recomputeLocalPrefixes();
}
}
@@ -555,10 +567,11 @@
}
handleLost(network);
- // Any non-LISTEN_ALL callback will necessarily concern a network that will
- // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
- // So it's not useful to do this work for non-LISTEN_ALL callbacks.
- if (mCallbackType == CALLBACK_LISTEN_ALL) {
+ // Any non-LISTEN_UPSTREAM callback will necessarily concern a network that will
+ // also match the LISTEN_UPSTREAM callback by construction of the
+ // LISTEN_UPSTREAM callback. So it's not useful to do this work for
+ // non-LISTEN_UPSTREAM callbacks.
+ if (mCallbackType == CALLBACK_LISTEN_UPSTREAM) {
recomputeLocalPrefixes();
}
}
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 01f3af9..1323f28 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -332,15 +332,16 @@
// seconds. See b/289881008.
private static final int EXPANDED_TIMEOUT_MS = 30000;
- MyTetheringEventCallback(String iface) {
- mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+ MyTetheringEventCallback(int tetheringType, String iface) {
+ mIface = new TetheringInterface(tetheringType, iface);
mExpectedUpstream = null;
mAcceptAnyUpstream = true;
}
- MyTetheringEventCallback(String iface, @NonNull Network expectedUpstream) {
+ MyTetheringEventCallback(
+ int tetheringType, String iface, @NonNull Network expectedUpstream) {
Objects.requireNonNull(expectedUpstream);
- mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+ mIface = new TetheringInterface(tetheringType, iface);
mExpectedUpstream = expectedUpstream;
mAcceptAnyUpstream = false;
}
@@ -392,12 +393,12 @@
}
public void awaitInterfaceTethered() throws Exception {
- assertTrue("Ethernet not tethered after " + EXPANDED_TIMEOUT_MS + "ms",
+ assertTrue("Interface is not tethered after " + EXPANDED_TIMEOUT_MS + "ms",
mTetheringStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
public void awaitInterfaceLocalOnly() throws Exception {
- assertTrue("Ethernet not local-only after " + EXPANDED_TIMEOUT_MS + "ms",
+ assertTrue("Interface is not local-only after " + EXPANDED_TIMEOUT_MS + "ms",
mLocalOnlyStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
@@ -501,15 +502,17 @@
sCallbackErrors.add(error);
}
- protected static MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableTethering(String iface,
TetheringRequest request, Network expectedUpstream) throws Exception {
- // Enable ethernet tethering with null expectedUpstream means the test accept any upstream
- // after etherent tethering started.
+ // Enable tethering with null expectedUpstream means the test accept any upstream after
+ // tethering started.
final MyTetheringEventCallback callback;
if (expectedUpstream != null) {
- callback = new MyTetheringEventCallback(iface, expectedUpstream);
+ callback =
+ new MyTetheringEventCallback(
+ request.getTetheringType(), iface, expectedUpstream);
} else {
- callback = new MyTetheringEventCallback(iface);
+ callback = new MyTetheringEventCallback(request.getTetheringType(), iface);
}
runAsShell(NETWORK_SETTINGS, () -> {
sTm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
@@ -521,7 +524,7 @@
StartTetheringCallback startTetheringCallback = new StartTetheringCallback() {
@Override
public void onTetheringStarted() {
- Log.d(TAG, "Ethernet tethering started");
+ Log.d(TAG, "Tethering started");
tetheringStartedLatch.countDown();
}
@@ -530,8 +533,8 @@
addCallbackError("Unexpectedly got onTetheringFailed");
}
};
- Log.d(TAG, "Starting Ethernet tethering");
- runAsShell(TETHER_PRIVILEGED, () -> {
+ Log.d(TAG, "Starting tethering");
+ runAsShell(TETHER_PRIVILEGED, NETWORK_SETTINGS, () -> {
sTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
// Binder call is an async call. Need to hold the shell permission until tethering
// started. This helps to avoid the test become flaky.
@@ -557,7 +560,7 @@
protected static MyTetheringEventCallback enableEthernetTethering(String iface,
Network expectedUpstream) throws Exception {
- return enableEthernetTethering(iface,
+ return enableTethering(iface,
new TetheringRequest.Builder(TETHERING_ETHERNET)
.setShouldShowEntitlementUi(false).build(), expectedUpstream);
}
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 1bbea94..5c8d347 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -19,7 +19,9 @@
import static android.Manifest.permission.DUMP;
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
import static android.net.TetheringTester.TestDnsPacket;
import static android.net.TetheringTester.buildIcmpEchoPacketV4;
import static android.net.TetheringTester.buildUdpPacket;
@@ -236,7 +238,8 @@
downstreamReader = makePacketReader(fd, mtu);
tetheringEventCallback = enableEthernetTethering(downstreamIface.getInterfaceName(),
null /* any upstream */);
- checkTetheredClientCallbacks(downstreamReader, tetheringEventCallback);
+ checkTetheredClientCallbacks(
+ downstreamReader, TETHERING_ETHERNET, tetheringEventCallback);
} finally {
maybeStopTapPacketReader(downstreamReader);
maybeCloseTestInterface(downstreamIface);
@@ -267,7 +270,8 @@
downstreamReader = makePacketReader(fd, getMTU(downstreamIface));
tetheringEventCallback = enableEthernetTethering(downstreamIface.getInterfaceName(),
null /* any upstream */);
- checkTetheredClientCallbacks(downstreamReader, tetheringEventCallback);
+ checkTetheredClientCallbacks(
+ downstreamReader, TETHERING_ETHERNET, tetheringEventCallback);
} finally {
maybeStopTapPacketReader(downstreamReader);
maybeCloseTestInterface(downstreamIface);
@@ -302,7 +306,7 @@
final String localAddr = "192.0.2.3/28";
final String clientAddr = "192.0.2.2/28";
- tetheringEventCallback = enableEthernetTethering(iface,
+ tetheringEventCallback = enableTethering(iface,
requestWithStaticIpv4(localAddr, clientAddr), null /* any upstream */);
tetheringEventCallback.awaitInterfaceTethered();
@@ -368,8 +372,7 @@
final TetheringRequest request = new TetheringRequest.Builder(TETHERING_ETHERNET)
.setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build();
- tetheringEventCallback = enableEthernetTethering(iface, request,
- null /* any upstream */);
+ tetheringEventCallback = enableTethering(iface, request, null /* any upstream */);
tetheringEventCallback.awaitInterfaceLocalOnly();
// makePacketReader only works after tethering is started, because until then the
@@ -424,6 +427,7 @@
}
private void checkTetheredClientCallbacks(final PollPacketReader packetReader,
+ final int tetheringType,
final MyTetheringEventCallback tetheringEventCallback) throws Exception {
// Create a fake client.
byte[] clientMacAddr = new byte[6];
@@ -438,7 +442,7 @@
// Check the MAC address.
assertEquals(MacAddress.fromBytes(clientMacAddr), client.getMacAddress());
- assertEquals(TETHERING_ETHERNET, client.getTetheringType());
+ assertEquals(tetheringType, client.getTetheringType());
// Check the hostname.
assertEquals(1, client.getAddresses().size());
@@ -475,8 +479,7 @@
private void assertInvalidStaticIpv4Request(String iface, String local, String client)
throws Exception {
try {
- enableEthernetTethering(iface, requestWithStaticIpv4(local, client),
- null /* any upstream */);
+ enableTethering(iface, requestWithStaticIpv4(local, client), null /* any upstream */);
fail("Unexpectedly accepted invalid IPv4 configuration: " + local + ", " + client);
} catch (IllegalArgumentException | NullPointerException expected) { }
}
@@ -1180,4 +1183,32 @@
TX_UDP_PACKET_COUNT * (TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN),
newEgress4.bytes - oldEgress4.bytes);
}
+
+ @Test
+ public void testTetheringVirtual() throws Exception {
+ assumeFalse(isInterfaceForTetheringAvailable());
+ setIncludeTestInterfaces(true);
+
+ TestNetworkInterface downstreamIface = null;
+ MyTetheringEventCallback tetheringEventCallback = null;
+ PollPacketReader downstreamReader = null;
+ try {
+ downstreamIface = createTestInterface();
+ String iface = downstreamIface.getInterfaceName();
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_VIRTUAL)
+ .setConnectivityScope(CONNECTIVITY_SCOPE_GLOBAL)
+ .setInterfaceName(iface)
+ .build();
+ tetheringEventCallback = enableTethering(iface, request, null /* any upstream */);
+
+ FileDescriptor fd = downstreamIface.getFileDescriptor().getFileDescriptor();
+ downstreamReader = makePacketReader(fd, getMTU(downstreamIface));
+ checkTetheredClientCallbacks(
+ downstreamReader, TETHERING_VIRTUAL, tetheringEventCallback);
+ } finally {
+ maybeStopTapPacketReader(downstreamReader);
+ maybeCloseTestInterface(downstreamIface);
+ maybeUnregisterTetheringEventCallback(tetheringEventCallback);
+ }
+ }
}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 680e81d..84b301f 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -48,7 +48,6 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.clearInvocations;
@@ -74,6 +73,7 @@
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
+import android.net.TetheringManager.TetheringRequest;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
import android.net.dhcp.IDhcpEventCallbacks;
@@ -240,7 +240,8 @@
Set<LinkAddress> upstreamAddresses, boolean usingLegacyDhcp, boolean usingBpfOffload)
throws Exception {
initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
verify(mBpfCoordinator).addIpServer(mIpServer);
if (upstreamIface != null) {
InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
@@ -345,7 +346,8 @@
public void canBeTetheredAsBluetooth() throws Exception {
initStateMachine(TETHERING_BLUETOOTH);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
if (isAtLeastT()) {
inOrder.verify(mRoutingCoordinatorManager)
@@ -400,7 +402,8 @@
public void canBeTetheredAsUsb() throws Exception {
initStateMachine(TETHERING_USB);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
eq(CONNECTIVITY_SCOPE_GLOBAL), any());
@@ -423,7 +426,8 @@
public void canBeTetheredAsWifiP2p_NotUsingDedicatedIp() throws Exception {
initStateMachine(TETHERING_WIFI_P2P);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
InOrder inOrder = inOrder(mCallback, mNetd, mRoutingCoordinatorManager);
inOrder.verify(mRoutingCoordinatorManager).requestStickyDownstreamAddress(anyInt(),
eq(CONNECTIVITY_SCOPE_LOCAL), any());
@@ -447,7 +451,8 @@
initStateMachine(TETHERING_WIFI_P2P, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD,
true /* shouldEnableWifiP2pDedicatedIp */);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
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.
@@ -627,7 +632,8 @@
initStateMachine(TETHERING_USB);
doThrow(RemoteException.class).when(mNetd).tetherInterfaceAdd(IFACE_NAME);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
usbTeardownOrder.verify(mNetd).interfaceSetCfg(
argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
@@ -713,7 +719,8 @@
@Test
public void startsDhcpServerOnNcm() throws Exception {
initStateMachine(TETHERING_NCM);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
assertDhcpStarted(new IpPrefix("192.168.42.0/24"));
@@ -722,7 +729,8 @@
@Test
public void testOnNewPrefixRequest() throws Exception {
initStateMachine(TETHERING_NCM);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_LOCAL));
final IDhcpEventCallbacks eventCallbacks;
final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
@@ -911,7 +919,8 @@
doNothing().when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(),
cbCaptor.capture());
initStateMachine(TETHERING_WIFI);
- dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0,
+ createMockTetheringRequest(CONNECTIVITY_SCOPE_GLOBAL));
verify(mDhcpServer, never()).startWithCallbacks(any(), any());
// No stop dhcp server because dhcp server is not created yet.
@@ -957,14 +966,22 @@
assertDhcpServingParams(mDhcpParamsCaptor.getValue(), expectedPrefix);
}
+ private TetheringRequest createMockTetheringRequest(int connectivityScope) {
+ TetheringRequest request = mock(TetheringRequest.class);
+ when(request.getConnectivityScope()).thenReturn(connectivityScope);
+ return request;
+ }
+
/**
* Send a command to the state machine under test, and run the event loop to idle.
*
* @param command One of the IpServer.CMD_* constants.
- * @param arg1 An additional argument to pass.
+ * @param arg1 An additional argument to pass.
+ * @param arg2 An additional argument to pass.
+ * @param obj An additional object to pass.
*/
- private void dispatchCommand(int command, int arg1) {
- mIpServer.sendMessage(command, arg1);
+ private void dispatchCommand(int command, int arg1, int arg2, Object obj) {
+ mIpServer.sendMessage(command, arg1, arg2, obj);
mLooper.dispatchAll();
}
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 8626b18..58e1894 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -50,6 +50,7 @@
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -84,6 +85,7 @@
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;
@@ -159,10 +161,20 @@
return super.getSystemServiceName(serviceClass);
}
+ @NonNull
@Override
public Context createContextAsUser(UserHandle user, int flags) {
+ if (mCreateContextAsUserException != null) {
+ throw mCreateContextAsUserException;
+ }
return mMockContext; // Return self for easier test injection.
}
+
+ private RuntimeException mCreateContextAsUserException = null;
+
+ private void setCreateContextAsUserException(RuntimeException e) {
+ mCreateContextAsUserException = e;
+ }
}
class TestDependencies extends EntitlementManager.Dependencies {
@@ -187,8 +199,9 @@
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);
}
- receiver.send(fakeEntitlementResult, null);
return intent;
}
@@ -348,99 +361,43 @@
public void testRequestLastEntitlementCacheValue() throws Exception {
// 1. Entitlement check is not required.
mDeps.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();
+ 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();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_ENTITLEMENT_UNKNOWN, false);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
// 3. No cache value and ui entitlement check is needed.
mDeps.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();
+ 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.
mDeps.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();
+ 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.
mDeps.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();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_NO_ERROR, true);
assertEquals(1, mDeps.uiProvisionCount);
mDeps.reset();
// 6. Cache value is TETHER_ERROR_NO_ERROR.
mDeps.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();
+ 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();
+ assertLatestEntitlementResult(TETHERING_USB, TETHER_ERROR_ENTITLEMENT_UNKNOWN, false);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
// 8. Test get value for invalid downstream type.
mDeps.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();
+ assertLatestEntitlementResult(TETHERING_WIFI_P2P, TETHER_ERROR_ENTITLEMENT_UNKNOWN, true);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
}
@@ -648,6 +605,14 @@
@IgnoreUpTo(SC_V2)
@Test
+ public void testUiProvisioningMultiUser_aboveT_createContextAsUserThrows() {
+ mMockContext.setCreateContextAsUserException(new IllegalStateException());
+ doTestUiProvisioningMultiUser(true, 1);
+ doTestUiProvisioningMultiUser(false, 1);
+ }
+
+ @IgnoreUpTo(SC_V2)
+ @Test
public void testUiProvisioningMultiUser_aboveT() {
doTestUiProvisioningMultiUser(true, 1);
doTestUiProvisioningMultiUser(false, 0);
@@ -660,21 +625,59 @@
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();
+ clearInvocations(mTetherProvisioningFailedListener);
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() throws Exception {
+ public void testSetExemptedDownstreamType() {
setupForRequiredProvisioning();
// Cellular upstream is not permitted when no entitlement result.
assertFalse(mEnMgr.isCellularUpstreamPermitted());
@@ -737,14 +740,7 @@
setupCarrierConfig(false);
setupForRequiredProvisioning();
mDeps.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();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_PROVISIONING_FAILED, false);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
}
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 1608e1a..f9e3a6a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -186,7 +186,8 @@
// - Test bluetooth prefix is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
getSubAddress(mBluetoothAddress.getAddress().getAddress()));
- final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer);
+ final LinkAddress hotspotAddress = requestStickyDownstreamAddress(mHotspotIpServer,
+ CONNECTIVITY_SCOPE_GLOBAL);
final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
releaseDownstream(mHotspotIpServer);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
index b2cbf75..51ba140 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -18,6 +18,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static com.android.networkstack.apishim.common.ShimUtils.isAtLeastS;
@@ -41,6 +42,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.modules.utils.build.SdkLevel;
+
import java.util.Map;
import java.util.Objects;
@@ -119,12 +122,15 @@
&& mLegacyTypeMap.isEmpty();
}
- boolean isListeningForAll() {
- final NetworkCapabilities empty = new NetworkCapabilities();
- empty.clearAll();
+ boolean isListeningForUpstream() {
+ final NetworkCapabilities upstreamNc = new NetworkCapabilities();
+ upstreamNc.clearAll();
+ if (SdkLevel.isAtLeastV()) {
+ upstreamNc.addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ }
for (NetworkRequestInfo nri : mListening.values()) {
- if (nri.request.networkCapabilities.equalRequestableCapabilities(empty)) {
+ if (nri.request.networkCapabilities.equalRequestableCapabilities(upstreamNc)) {
return true;
}
}
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 1988311..b550ada 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -17,9 +17,11 @@
package com.android.networkstack.tethering;
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.Manifest.permission.TETHER_PRIVILEGED;
import static android.Manifest.permission.WRITE_SETTINGS;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
@@ -33,13 +35,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;
@@ -48,7 +53,6 @@
import android.net.TetheringManager;
import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringRequestParcel;
-import android.net.ip.IpServer;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Handler;
@@ -82,6 +86,7 @@
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;
@@ -89,6 +94,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.
@@ -132,6 +138,8 @@
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
@@ -145,27 +153,43 @@
}
private void runAsNoPermission(final TestTetheringCall test) throws Exception {
- runTetheringCall(test, true /* isTetheringAllowed */, new String[0]);
+ runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ new String[0]);
}
private void runAsTetherPrivileged(final TestTetheringCall test) throws Exception {
- runTetheringCall(test, true /* isTetheringAllowed */, TETHER_PRIVILEGED);
+ runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ TETHER_PRIVILEGED);
}
private void runAsAccessNetworkState(final TestTetheringCall test) throws Exception {
- runTetheringCall(test, true /* isTetheringAllowed */, ACCESS_NETWORK_STATE);
+ runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ ACCESS_NETWORK_STATE);
}
private void runAsWriteSettings(final TestTetheringCall test) throws Exception {
- runTetheringCall(test, true /* isTetheringAllowed */, WRITE_SETTINGS);
+ runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ WRITE_SETTINGS);
+ }
+
+ private void runAsWriteSettingsWhenWriteSettingsAllowed(
+ final TestTetheringCall test) throws Exception {
+ runTetheringCall(test, true /* isTetheringAllowed */, true /* isWriteSettingsAllowed */,
+ WRITE_SETTINGS);
}
private void runAsTetheringDisallowed(final TestTetheringCall test) throws Exception {
- runTetheringCall(test, false /* isTetheringAllowed */, TETHER_PRIVILEGED);
+ runTetheringCall(test, false /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ TETHER_PRIVILEGED);
+ }
+
+ private void runAsNetworkSettings(final TestTetheringCall test) throws Exception {
+ runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ NETWORK_SETTINGS, TETHER_PRIVILEGED);
}
private void runTetheringCall(final TestTetheringCall test, boolean isTetheringAllowed,
- String... permissions) throws Exception {
+ boolean isWriteSettingsAllowed, String... permissions) throws Exception {
// Allow the test to run even if ACCESS_NETWORK_STATE was granted at the APK level
if (!CollectionUtils.contains(permissions, ACCESS_NETWORK_STATE)) {
mMockConnector.setPermission(ACCESS_NETWORK_STATE, PERMISSION_DENIED);
@@ -175,6 +199,8 @@
try {
when(mTethering.isTetheringSupported()).thenReturn(true);
when(mTethering.isTetheringAllowed()).thenReturn(isTetheringAllowed);
+ when(mTethering.isTetheringWithSoftApConfigEnabled())
+ .thenReturn(!isWriteSettingsAllowed);
test.runTetheringCall(new TestTetheringResult());
} finally {
mUiAutomation.dropShellPermissionIdentity();
@@ -192,7 +218,7 @@
mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- verify(mTethering).tether(TEST_IFACE_NAME, IpServer.STATE_TETHERED, result);
+ verify(mTethering).legacyTether(TEST_IFACE_NAME, result);
}
@Test
@@ -200,7 +226,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -211,7 +237,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runTether(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -231,7 +266,7 @@
result);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- verify(mTethering).untether(eq(TEST_IFACE_NAME), eq(result));
+ verify(mTethering).legacyUntether(eq(TEST_IFACE_NAME), eq(result));
}
@Test
@@ -239,7 +274,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -250,7 +285,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runUnTether(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -284,7 +328,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG,
TEST_ATTRIBUTION_TAG, result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -295,7 +339,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG,
+ TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runSetUsbTethering(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -328,17 +381,16 @@
runAsNoPermission((result) -> {
mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
runAsTetherPrivileged((result) -> {
- String wrongPackage = "wrong.package";
- mTetheringConnector.startTethering(request, wrongPackage,
+ mTetheringConnector.startTethering(request, TEST_WRONG_PACKAGE,
TEST_ATTRIBUTION_TAG, result);
verify(mTethering, never()).startTethering(
- eq(new TetheringRequest(request)), eq(wrongPackage), eq(result));
+ eq(new TetheringRequest(request)), eq(TEST_WRONG_PACKAGE), eq(result));
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -349,7 +401,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runStartTethering(result, request);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -364,6 +425,32 @@
});
}
+ @Test
+ public void testStartTetheringWithInterfaceSucceeds() throws Exception {
+ final TetheringRequestParcel request = new TetheringRequestParcel();
+ request.tetheringType = TETHERING_VIRTUAL;
+ request.interfaceName = "avf_tap_fixed";
+
+ runAsNetworkSettings((result) -> {
+ runStartTethering(result, request);
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
+ @Test
+ public void testStartTetheringNoNetworkStackPermissionWithInterfaceFails() throws Exception {
+ final TetheringRequestParcel request = new TetheringRequestParcel();
+ request.tetheringType = TETHERING_VIRTUAL;
+ request.interfaceName = "avf_tap_fixed";
+
+ runAsTetherPrivileged((result) -> {
+ mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
private void runStartTetheringAndVerifyNoPermission(final TestTetheringResult result)
throws Exception {
final TetheringRequestParcel request = new TetheringRequestParcel();
@@ -408,7 +495,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.stopTethering(TETHERING_WIFI, TEST_CALLER_PKG,
TEST_ATTRIBUTION_TAG, result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -419,7 +506,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.stopTethering(TETHERING_WIFI, TEST_CALLER_PKG,
+ TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runStopTethering(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -448,11 +544,13 @@
public void testRequestLatestTetheringEntitlementResult() throws Exception {
// Run as no permission.
final MyResultReceiver result = new MyResultReceiver(null);
- mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
- true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
- verify(mTethering).isTetherProvisioningRequired();
- result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
- verifyNoMoreInteractions(mTethering);
+ runAsNoPermission((none) -> {
+ mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
+ true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
runAsTetherPrivileged((none) -> {
runRequestLatestTetheringEntitlementResult();
@@ -461,24 +559,32 @@
runAsTetherPrivileged((none) -> {
mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
- true /* showEntitlementUi */, "wrong.package", TEST_ATTRIBUTION_TAG);
+ true /* showEntitlementUi */, TEST_WRONG_PACKAGE, TEST_ATTRIBUTION_TAG);
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
- verifyNoMoreInteractions(mTethering);
+ verifyNoMoreInteractionsForTethering();
});
runAsWriteSettings((none) -> {
+ mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
+ true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((none) -> {
runRequestLatestTetheringEntitlementResult();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
runAsTetheringDisallowed((none) -> {
- final MyResultReceiver receiver = new MyResultReceiver(null);
- mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver,
+ mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- receiver.assertResult(TETHER_ERROR_UNSUPPORTED);
+ result.assertResult(TETHER_ERROR_UNSUPPORTED);
verifyNoMoreInteractionsForTethering();
});
}
@@ -554,7 +660,7 @@
mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- verify(mTethering).untetherAll();
+ verify(mTethering).stopAllTethering();
result.assertResult(TETHER_ERROR_NO_ERROR);
}
@@ -562,7 +668,7 @@
public void testStopAllTethering() throws Exception {
runAsNoPermission((result) -> {
mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -573,7 +679,15 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runStopAllTethering(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -599,7 +713,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.isTetheringSupported(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -610,7 +724,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.isTetheringSupported(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runIsTetheringSupported(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
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 d0c036f..e1c2db9 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;
@@ -43,6 +47,7 @@
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_VIRTUAL;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
@@ -88,7 +93,6 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.notNull;
@@ -164,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;
@@ -190,6 +195,7 @@
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;
@@ -223,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;
@@ -248,6 +255,7 @@
private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0";
private static final String TEST_NCM_IFNAME = "test_ncm0";
private static final String TEST_ETH_IFNAME = "test_eth0";
+ private static final String TEST_VIRT_IFNAME = "test_virt0";
private static final String TEST_BT_IFNAME = "test_pan0";
private static final String TETHERING_NAME = "Tethering";
private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
@@ -257,8 +265,10 @@
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 int TEST_CALLER_UID_2 = 2000;
private static final String TEST_CALLER_PKG = "com.test.tethering";
-
+ private static final String TEST_CALLER_PKG_2 = "com.test.tethering2";
private static final int CELLULAR_NETID = 100;
private static final int WIFI_NETID = 101;
private static final int DUN_NETID = 102;
@@ -328,6 +338,7 @@
private TestConnectivityManager mCm;
private boolean mForceEthernetServiceUnavailable = false;
+ private int mBinderCallingUid = TEST_CALLER_UID;
private class TestContext extends BroadcastInterceptingContext {
TestContext(Context base) {
@@ -408,11 +419,12 @@
|| ifName.equals(TEST_P2P_IFNAME)
|| ifName.equals(TEST_NCM_IFNAME)
|| ifName.equals(TEST_ETH_IFNAME)
- || ifName.equals(TEST_BT_IFNAME));
+ || ifName.equals(TEST_BT_IFNAME)
+ || ifName.equals(TEST_VIRT_IFNAME));
final String[] ifaces = new String[] {
TEST_RNDIS_IFNAME, TEST_WLAN_IFNAME, TEST_WLAN2_IFNAME, TEST_WIFI_IFNAME,
TEST_MOBILE_IFNAME, TEST_DUN_IFNAME, TEST_P2P_IFNAME, TEST_NCM_IFNAME,
- TEST_ETH_IFNAME};
+ TEST_ETH_IFNAME, TEST_VIRT_IFNAME};
return new InterfaceParams(ifName,
CollectionUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET,
MacAddress.ALL_ZEROS_ADDRESS);
@@ -555,6 +567,11 @@
}
return mBluetoothPanShim;
}
+
+ @Override
+ public int getBinderCallingUid() {
+ return mBinderCallingUid;
+ }
}
private static LinkProperties buildUpstreamLinkProperties(String interfaceName,
@@ -660,7 +677,8 @@
when(mNetd.interfaceGetList())
.thenReturn(new String[] {
TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_WLAN2_IFNAME, TEST_RNDIS_IFNAME,
- TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME});
+ TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME,
+ TEST_VIRT_IFNAME});
when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
mInterfaceConfiguration = new InterfaceConfigurationParcel();
mInterfaceConfiguration.flags = new String[0];
@@ -751,12 +769,12 @@
}
private TetheringRequest createTetheringRequest(final int type) {
- return createTetheringRequest(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
+ return createTetheringRequest(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL, null);
}
private TetheringRequest createTetheringRequest(final int type,
final LinkAddress localIPv4Address, final LinkAddress staticClientAddress,
- final boolean exempt, final int scope) {
+ final boolean exempt, final int scope, final String interfaceName) {
TetheringRequest.Builder builder = new TetheringRequest.Builder(type)
.setExemptFromEntitlementCheck(exempt)
.setConnectivityScope(scope)
@@ -764,7 +782,13 @@
if (localIPv4Address != null && staticClientAddress != null) {
builder.setStaticIpv4Addresses(localIPv4Address, staticClientAddress);
}
- return builder.build();
+ if (interfaceName != null) {
+ builder.setInterfaceName(interfaceName);
+ }
+ TetheringRequest request = builder.build();
+ request.setUid(TEST_CALLER_UID);
+ request.setPackageName(TEST_CALLER_PKG);
+ return request;
}
@NonNull
@@ -908,11 +932,18 @@
// it creates a IpServer and sends out a broadcast indicating that the
// interface is "available".
if (emulateInterfaceStatusChanged) {
- // There is 1 IpServer state change event: STATE_AVAILABLE
- verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
- verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
- verify(mWifiManager).updateInterfaceIpState(
- TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ if (!SdkLevel.isAtLeastB()) {
+ // There is 1 IpServer state change event: STATE_AVAILABLE
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ } else {
+ // Starting in B, ignore the interfaceStatusChanged
+ verify(mNotificationUpdater, never()).onDownstreamChanged(DOWNSTREAM_NONE);
+ verify(mWifiManager, never()).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ }
}
verifyNoMoreInteractions(mNetd);
verifyNoMoreInteractions(mWifiManager);
@@ -932,8 +963,8 @@
mTethering.startTethering(request, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
- assertEquals(1, mTethering.getActiveTetheringRequests().size());
- assertEquals(request, mTethering.getActiveTetheringRequests().get(TETHERING_USB));
+ assertEquals(1, mTethering.getPendingTetheringRequests().size());
+ assertEquals(request, mTethering.getPendingTetheringRequests().get(TETHERING_USB));
if (mTethering.getTetheringConfiguration().isUsingNcm()) {
verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NCM);
@@ -1011,7 +1042,7 @@
verify(mWifiManager).updateInterfaceIpState(TEST_WLAN_IFNAME, expectedState);
verifyNoMoreInteractions(mWifiManager);
- verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
if (isLocalOnly) {
// There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY.
verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
@@ -1239,7 +1270,7 @@
// Start USB tethering with no current upstream.
prepareUsbTethering();
sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
- inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ inOrder.verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
// Pretend cellular connected and expect the upstream to be set.
@@ -1838,7 +1869,7 @@
// Start USB tethering with no current upstream.
prepareUsbTethering();
sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
- inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ inOrder.verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
inOrder.verify(mCm).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(),
@@ -1909,7 +1940,6 @@
workingLocalOnlyHotspotEnrichedApBroadcast(false);
}
- // TODO: Test with and without interfaceStatusChanged().
@Test
public void failingWifiTetheringLegacyApBroadcast() throws Exception {
initTetheringOnTestThread();
@@ -1928,12 +1958,20 @@
// tethering mode is to be started.
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
+ mLooper.dispatchAll();
- // There is 1 IpServer state change event: STATE_AVAILABLE
- verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
- verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
- verify(mWifiManager).updateInterfaceIpState(
- TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ if (!SdkLevel.isAtLeastB()) {
+ // There is 1 IpServer state change event: STATE_AVAILABLE from interfaceStatusChanged
+ verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ } else {
+ // Starting in B, ignore the interfaceStatusChanged
+ verify(mNotificationUpdater, never()).onDownstreamChanged(DOWNSTREAM_NONE);
+ verify(mWifiManager, never()).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ }
verifyNoMoreInteractions(mNetd);
verifyNoMoreInteractions(mWifiManager);
}
@@ -2073,7 +2111,7 @@
verify(mNotificationUpdater, times(expectedInteractionsWithShowNotification))
.notifyTetheringDisabledByRestriction();
- verify(mockTethering, times(expectedInteractionsWithShowNotification)).untetherAll();
+ verify(mockTethering, times(expectedInteractionsWithShowNotification)).stopAllTethering();
}
@Test
@@ -2145,7 +2183,7 @@
runUsbTethering(upstreamState);
assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_RNDIS_IFNAME);
assertTrue(mTethering.isTetheringActive());
- assertEquals(0, mTethering.getActiveTetheringRequests().size());
+ assertEquals(0, mTethering.getPendingTetheringRequests().size());
final Tethering.UserRestrictionActionListener ural = makeUserRestrictionActionListener(
mTethering, false /* currentDisallow */, true /* nextDisallow */);
@@ -2335,15 +2373,20 @@
// 2. Enable wifi tethering.
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
initTetheringUpstream(upstreamState);
- when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
- mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
- mLooper.dispatchAll();
- tetherState = callback.pollTetherStatesChanged();
- assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
+ when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
null);
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ mLooper.dispatchAll();
+ if (SdkLevel.isAtLeastB()) {
+ // Starting in B, ignore the interfaceStatusChanged
+ callback.assertNoStateChangeCallback();
+ }
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ mLooper.dispatchAll();
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
tetherState = callback.pollTetherStatesChanged();
assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
callback.expectUpstreamChanged(upstreamState.network);
@@ -2380,6 +2423,111 @@
}
@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);
+
+ // Enable wifi tethering
+ mBinderCallingUid = TEST_CALLER_UID;
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ mLooper.dispatchAll();
+ if (SdkLevel.isAtLeastB()) {
+ // Starting in B, ignore the interfaceStatusChanged
+ callback.assertNoStateChangeCallback();
+ }
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ mLooper.dispatchAll();
+ // Verify we see Available -> Tethered states
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+ callback.pollTetherStatesChanged().availableList);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+ differentCallback.pollTetherStatesChanged().availableList);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+ settingsCallback.pollTetherStatesChanged().availableList);
+ 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();
@@ -2467,7 +2615,7 @@
verify(mNetd, times(1)).tetherStartWithConfiguration(any());
verifyNoMoreInteractions(mNetd);
verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
- verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+ verify(mUpstreamNetworkMonitor, times(1)).startObserveUpstreamNetworks();
// There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY
verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
@@ -2638,6 +2786,10 @@
public void assertHasResult() {
if (!mHasResult) fail("No callback result");
}
+
+ public void assertDoesNotHaveResult() {
+ if (mHasResult) fail("Has callback result");
+ }
}
@Test
@@ -2666,10 +2818,21 @@
verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
reset(mUsbManager);
+ // Enable USB tethering again with the same request but different uid/package and expect no
+ // change to USB.
+ TetheringRequest differentUidPackage = createTetheringRequest(TETHERING_USB);
+ differentUidPackage.setUid(TEST_CALLER_UID_2);
+ differentUidPackage.setPackageName(TEST_CALLER_PKG_2);
+ mTethering.startTethering(differentUidPackage, TEST_CALLER_PKG_2, secondResult);
+ mLooper.dispatchAll();
+ secondResult.assertHasResult();
+ verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ reset(mUsbManager);
+
// Enable USB tethering with a different request and expect that USB is stopped and
// started.
mTethering.startTethering(createTetheringRequest(TETHERING_USB,
- serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+ serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL, null),
TEST_CALLER_PKG, thirdResult);
mLooper.dispatchAll();
thirdResult.assertHasResult();
@@ -2700,7 +2863,7 @@
final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
mTethering.startTethering(createTetheringRequest(TETHERING_USB,
- serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+ serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL, null),
TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
@@ -2829,7 +2992,7 @@
setupForRequiredProvisioning();
final TetheringRequest wifiNotExemptRequest =
createTetheringRequest(TETHERING_WIFI, null, null, false,
- CONNECTIVITY_SCOPE_GLOBAL);
+ CONNECTIVITY_SCOPE_GLOBAL, null);
mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
@@ -2843,7 +3006,7 @@
setupForRequiredProvisioning();
final TetheringRequest wifiExemptRequest =
createTetheringRequest(TETHERING_WIFI, null, null, true,
- CONNECTIVITY_SCOPE_GLOBAL);
+ CONNECTIVITY_SCOPE_GLOBAL, null);
mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
@@ -3235,10 +3398,9 @@
}
@Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testBluetoothTethering() throws Exception {
initTetheringOnTestThread();
- // Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP.
- assumeTrue(isAtLeastT());
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
@@ -3272,10 +3434,9 @@
}
@Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
public void testBluetoothTetheringBeforeT() throws Exception {
initTetheringOnTestThread();
- // Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP.
- assumeFalse(isAtLeastT());
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
@@ -3291,7 +3452,7 @@
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+ mTethering.legacyTether(TEST_BT_IFNAME, tetherResult);
mLooper.dispatchAll();
tetherResult.assertHasResult();
@@ -3311,7 +3472,7 @@
mTethering.stopTethering(TETHERING_BLUETOOTH);
mLooper.dispatchAll();
final ResultListener untetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.untether(TEST_BT_IFNAME, untetherResult);
+ mTethering.legacyUntether(TEST_BT_IFNAME, untetherResult);
mLooper.dispatchAll();
untetherResult.assertHasResult();
verifySetBluetoothTethering(false /* enable */, false /* bindToPanService */);
@@ -3341,7 +3502,7 @@
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+ mTethering.legacyTether(TEST_BT_IFNAME, tetherResult);
mLooper.dispatchAll();
tetherResult.assertHasResult();
}
@@ -3355,6 +3516,23 @@
verifyNetdCommandForBtTearDown();
}
+ @Test
+ public void testPendingPanEnableRequestFailedUponDisableRequest() throws Exception {
+ initTetheringOnTestThread();
+
+ mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
+ final ResultListener failedEnable = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
+ mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
+ TEST_CALLER_PKG, failedEnable);
+ mLooper.dispatchAll();
+ failedEnable.assertDoesNotHaveResult();
+
+ // Stop tethering before the pan service connects. This should fail the enable request.
+ mTethering.stopTethering(TETHERING_BLUETOOTH);
+ mLooper.dispatchAll();
+ failedEnable.assertHasResult();
+ }
+
private void mockBluetoothSettings(boolean bluetoothOn, boolean tetheringOn) {
when(mBluetoothAdapter.isEnabled()).thenReturn(bluetoothOn);
when(mBluetoothPan.isTetheringOn()).thenReturn(tetheringOn);
@@ -3637,7 +3815,7 @@
verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
- verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
// Verify never enable upstream if only P2P active.
verify(mUpstreamNetworkMonitor, never()).setTryCell(true);
assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
@@ -3666,4 +3844,18 @@
// TODO: Test that a request for hotspot mode doesn't interfere with an
// already operating tethering mode interface.
+
+ @Test
+ public void testVirtualTetheringWithInterfaceName() throws Exception {
+ initTetheringOnTestThread();
+ final TetheringRequest virtualTetheringRequest =
+ createTetheringRequest(TETHERING_VIRTUAL, null, null, false,
+ CONNECTIVITY_SCOPE_GLOBAL, TEST_VIRT_IFNAME);
+ assertEquals(TEST_VIRT_IFNAME, virtualTetheringRequest.getInterfaceName());
+ mTethering.startTethering(virtualTetheringRequest, TEST_CALLER_PKG, null);
+ mLooper.dispatchAll();
+ assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_VIRT_IFNAME);
+ mTethering.stopTethering(TETHERING_VIRTUAL);
+ mLooper.dispatchAll();
+ }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index 90fd709..f192492 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -36,7 +36,6 @@
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -141,7 +140,7 @@
assertTrue(mCM.hasNoCallbacks());
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
assertEquals(1, mCM.mTrackingDefault.size());
mUNM.stop();
@@ -149,13 +148,13 @@
}
@Test
- public void testListensForAllNetworks() throws Exception {
+ public void testListensForUpstreamNetworks() throws Exception {
assertTrue(mCM.mListening.isEmpty());
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
assertFalse(mCM.mListening.isEmpty());
- assertTrue(mCM.isListeningForAll());
+ assertTrue(mCM.isListeningForUpstream());
mUNM.stop();
assertTrue(mCM.onlyHasDefaultCallbacks());
@@ -179,7 +178,7 @@
assertTrue(TestConnectivityManager.looksLikeDefaultRequest(requestCaptor.getValue()));
}
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
verify(mCM, times(1)).registerNetworkCallback(
any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
@@ -192,7 +191,7 @@
assertFalse(mUNM.mobileNetworkRequested());
assertEquals(0, mCM.mRequested.size());
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
assertFalse(mUNM.mobileNetworkRequested());
assertEquals(0, mCM.mRequested.size());
@@ -215,7 +214,7 @@
assertFalse(mUNM.mobileNetworkRequested());
assertEquals(0, mCM.mRequested.size());
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
verify(mCM, times(1)).registerNetworkCallback(
any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
assertFalse(mUNM.mobileNetworkRequested());
@@ -251,7 +250,7 @@
assertFalse(mUNM.mobileNetworkRequested());
assertEquals(0, mCM.mRequested.size());
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
assertFalse(mUNM.mobileNetworkRequested());
assertEquals(0, mCM.mRequested.size());
@@ -271,7 +270,7 @@
@Test
public void testUpdateMobileRequiresDun() throws Exception {
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
// Test going from no-DUN to DUN correctly re-registers callbacks.
mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
@@ -301,7 +300,7 @@
preferredTypes.add(TYPE_WIFI);
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
// There are no networks, so there is nothing to select.
assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
@@ -374,7 +373,7 @@
@Test
public void testGetCurrentPreferredUpstream() throws Exception {
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
mUNM.setTryCell(true);
@@ -446,7 +445,7 @@
@Test
public void testLocalPrefixes() throws Exception {
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
// [0] Test minimum set of local prefixes.
Set<IpPrefix> local = mUNM.getLocalPrefixes();
@@ -558,7 +557,7 @@
preferredTypes.add(TYPE_MOBILE_HIPRI);
preferredTypes.add(TYPE_WIFI);
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
// Setup wifi and make wifi as default network.
final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
wifiAgent.fakeConnect();
@@ -579,7 +578,7 @@
final String ipv6Addr1 = "2001:db8:4:fd00:827a:bfff:fe6f:374d/64";
final String ipv6Addr2 = "2003:aa8:3::123/64";
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
mUNM.setTryCell(true);
diff --git a/bpf/headers/BpfMapTest.cpp b/bpf/headers/BpfMapTest.cpp
index 862114d..33b88fa 100644
--- a/bpf/headers/BpfMapTest.cpp
+++ b/bpf/headers/BpfMapTest.cpp
@@ -250,5 +250,10 @@
expectMapEmpty(testMap);
}
+TEST_F(BpfMapTest, testGTSbitmapTestOpen) {
+ BpfMap<int, uint64_t> bitmap;
+ ASSERT_RESULT_OK(bitmap.init("/sys/fs/bpf/tethering/map_test_bitmap"));
+}
+
} // namespace bpf
} // namespace android
diff --git a/bpf/headers/include/bpf/BpfClassic.h b/bpf/headers/include/bpf/BpfClassic.h
index 81be37d..26d8ad5 100644
--- a/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,9 +164,15 @@
#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)
+// IPv6 MLD start with u8 type
+#define BPF_LOAD_NETX_RELATIVE_MLD_TYPE BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+
// IPv6 fragment header is always exactly 8 bytes long
#define BPF_LOAD_CONSTANT_V6FRAGHDR_LEN \
BPF_STMT(BPF_LD | BPF_IMM, 8)
diff --git a/bpf/headers/include/bpf/BpfMap.h b/bpf/headers/include/bpf/BpfMap.h
index 1037beb..576cca6 100644
--- a/bpf/headers/include/bpf/BpfMap.h
+++ b/bpf/headers/include/bpf/BpfMap.h
@@ -26,6 +26,7 @@
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
+#include <cstdio>
#include <functional>
namespace android {
@@ -35,6 +36,30 @@
using base::unique_fd;
using std::function;
+#ifdef BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+#undef BPFMAP_VERBOSE_ABORT
+#define BPFMAP_VERBOSE_ABORT
+#endif
+
+[[noreturn]] __attribute__((__format__(__printf__, 2, 3))) static inline
+void Abort(int __unused error, const char* __unused fmt, ...) {
+#ifdef BPFMAP_VERBOSE_ABORT
+ va_list va;
+ va_start(va, fmt);
+
+ fflush(stdout);
+ vfprintf(stderr, fmt, va);
+ if (error) fprintf(stderr, "; errno=%d [%s]", error, strerror(error));
+ putc('\n', stderr);
+ fflush(stderr);
+
+ va_end(va);
+#endif
+
+ abort();
+}
+
+
// This is a class wrapper for eBPF maps. The eBPF map is a special in-kernel
// data structure that stores data in <Key, Value> pairs. It can be read/write
// from userspace by passing syscalls with the map file descriptor. This class
@@ -60,14 +85,21 @@
protected:
void abortOnMismatch(bool writable) const {
- if (!mMapFd.ok()) abort();
+ if (!mMapFd.ok()) Abort(errno, "mMapFd %d is not valid", mMapFd.get());
if (isAtLeastKernelVersion(4, 14, 0)) {
int flags = bpfGetFdMapFlags(mMapFd);
- if (flags < 0) abort();
- if (flags & BPF_F_WRONLY) abort();
- if (writable && (flags & BPF_F_RDONLY)) abort();
- if (bpfGetFdKeySize(mMapFd) != sizeof(Key)) abort();
- if (bpfGetFdValueSize(mMapFd) != sizeof(Value)) abort();
+ if (flags < 0) Abort(errno, "bpfGetFdMapFlags fail: flags=%d", flags);
+ if (flags & BPF_F_WRONLY) Abort(0, "map is write-only (flags=0x%X)", flags);
+ if (writable && (flags & BPF_F_RDONLY))
+ Abort(0, "writable map is actually read-only (flags=0x%X)", flags);
+ int keySize = bpfGetFdKeySize(mMapFd);
+ if (keySize != sizeof(Key))
+ Abort(errno, "map key size mismatch (expected=%zu, actual=%d)",
+ sizeof(Key), keySize);
+ int valueSize = bpfGetFdValueSize(mMapFd);
+ if (valueSize != sizeof(Value))
+ Abort(errno, "map value size mismatch (expected=%zu, actual=%d)",
+ sizeof(Value), valueSize);
}
}
@@ -278,8 +310,8 @@
[[clang::reinitializes]] Result<void> resetMap(bpf_map_type map_type,
uint32_t max_entries,
uint32_t map_flags = 0) {
- if (map_flags & BPF_F_WRONLY) abort();
- if (map_flags & BPF_F_RDONLY) abort();
+ if (map_flags & BPF_F_WRONLY) Abort(0, "map_flags is write-only");
+ if (map_flags & BPF_F_RDONLY) Abort(0, "map_flags is read-only");
mMapFd.reset(createMap(map_type, sizeof(Key), sizeof(Value), max_entries,
map_flags));
if (!mMapFd.ok()) return ErrnoErrorf("BpfMap::resetMap() failed");
diff --git a/bpf/headers/include/bpf/BpfRingbuf.h b/bpf/headers/include/bpf/BpfRingbuf.h
index 4bcd259..5fe4ef7 100644
--- a/bpf/headers/include/bpf/BpfRingbuf.h
+++ b/bpf/headers/include/bpf/BpfRingbuf.h
@@ -99,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
@@ -247,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/bpf/headers/include/bpf/BpfUtils.h b/bpf/headers/include/bpf/BpfUtils.h
index 9dd5822..ed08e1a 100644
--- a/bpf/headers/include/bpf/BpfUtils.h
+++ b/bpf/headers/include/bpf/BpfUtils.h
@@ -26,6 +26,7 @@
#include <sys/socket.h>
#include <sys/utsname.h>
+#include <android-base/properties.h>
#include <log/log.h>
#include "KernelUtils.h"
@@ -33,6 +34,16 @@
namespace android {
namespace bpf {
+const bool unreleased = (base::GetProperty("ro.build.version.codename", "REL") != "REL");
+const int api_level = unreleased ? 10000 : android_get_device_api_level();
+const bool isAtLeastR = (api_level >= 30);
+const bool isAtLeastS = (api_level >= 31);
+// Sv2 is 32
+const bool isAtLeastT = (api_level >= 33);
+const bool isAtLeastU = (api_level >= 34);
+const bool isAtLeastV = (api_level >= 35);
+const bool isAtLeast25Q2 = (api_level >= 36);
+
// See kernel's net/core/sock_diag.c __sock_gen_cookie()
// the implementation of which guarantees 0 will never be returned,
// primarily because 0 is used to mean not yet initialized,
@@ -63,9 +74,9 @@
// 4.9 kernels. The kernel code of socket release on pf_key socket will
// explicitly call synchronize_rcu() which is exactly what we need.
//
- // Linux 4.14/4.19/5.4/5.10/5.15/6.1 (and 6.3-rc5) still have this same behaviour.
+ // Linux 4.14/4.19/5.4/5.10/5.15/6.1/6.6/6.12 (& 6.13) have this behaviour.
// see net/key/af_key.c: pfkey_release() -> synchronize_rcu()
- // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.3-rc5#n185
+ // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.13#n185
const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
if (pfSocket < 0) {
diff --git a/bpf/headers/include/bpf/KernelUtils.h b/bpf/headers/include/bpf/KernelUtils.h
index 68bc607..a36085a 100644
--- a/bpf/headers/include/bpf/KernelUtils.h
+++ b/bpf/headers/include/bpf/KernelUtils.h
@@ -55,12 +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, min for W
+ isKernelVersion(5, 4) || // first supported in Android R, min for 25Q2
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, 12); // first supported in Android W
+ isKernelVersion(6, 12); // first supported in Android 25Q2
}
// Figure out the bitness of userspace.
diff --git a/bpf/headers/include/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
index ac5ffda..6a0e5a8 100644
--- a/bpf/headers/include/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -62,8 +62,8 @@
// 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)
+// Android Mainline BpfLoader when running on Android 25Q2 (sdk=36)
+#define BPFLOADER_MAINLINE_25Q2_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
@@ -122,28 +122,46 @@
*/
#define CRITICAL(REASON) char _critical[] SECTION("critical") = (REASON)
-/*
- * Helper functions called from eBPF programs written in C. These are
- * implemented in the kernel sources.
- */
+// Helpers for writing kernel version specific bpf programs
struct kver_uint { unsigned int kver; };
#define KVER_(v) ((struct kver_uint){ .kver = (v) })
#define KVER(a, b, c) KVER_(((a) << 24) + ((b) << 16) + (c))
#define KVER_NONE KVER_(0)
+#define KVER_4_9 KVER(4, 9, 0)
#define KVER_4_14 KVER(4, 14, 0)
#define KVER_4_19 KVER(4, 19, 0)
#define KVER_5_4 KVER(5, 4, 0)
-#define KVER_5_8 KVER(5, 8, 0)
-#define KVER_5_9 KVER(5, 9, 0)
#define KVER_5_10 KVER(5, 10, 0)
#define KVER_5_15 KVER(5, 15, 0)
#define KVER_6_1 KVER(6, 1, 0)
#define KVER_6_6 KVER(6, 6, 0)
+#define KVER_6_12 KVER(6, 12, 0)
#define KVER_INF KVER_(0xFFFFFFFFu)
#define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
+// Helpers for writing sdk level specific bpf programs
+//
+// Note: we choose to follow sdk api level values, but there is no real need for this:
+// These just need to be monotonically increasing. We could also use values ten or even
+// a hundred times larger to leave room for quarters or months. We may also just use
+// dates or something (2502 or 202506 for 25Q2) or even the mainline bpfloader version...
+// For now this easily suffices for our use case.
+
+struct sdk_level_uint { unsigned int sdk_level; };
+#define SDK_LEVEL_(v) ((struct sdk_level_uint){ .sdk_level = (v) })
+#define SDK_LEVEL_NONE SDK_LEVEL_(0)
+#define SDK_LEVEL_S SDK_LEVEL_(31) // Android 12
+#define SDK_LEVEL_Sv2 SDK_LEVEL_(32) // Android 12L
+#define SDK_LEVEL_T SDK_LEVEL_(33) // Android 13
+#define SDK_LEVEL_U SDK_LEVEL_(34) // Android 14
+#define SDK_LEVEL_V SDK_LEVEL_(35) // Android 15
+#define SDK_LEVEL_24Q3 SDK_LEVEL_V
+#define SDK_LEVEL_25Q2 SDK_LEVEL_(36) // Android 16
+
+#define SDK_LEVEL_IS_AT_LEAST(lvl, v) ((lvl).sdk_level >= (SDK_LEVEL_##v).sdk_level)
+
/*
* BPFFS (ie. /sys/fs/bpf) labelling is as follows:
* subdirectory selinux context mainline usecase / usable by
@@ -168,6 +186,11 @@
* See cs/p:aosp-master%20-file:prebuilts/%20file:genfs_contexts%20"genfscon%20bpf"
*/
+/*
+ * Helper functions called from eBPF programs written in C. These are
+ * implemented in the kernel sources.
+ */
+
/* generic functions */
/*
@@ -231,16 +254,24 @@
(ignore_userdebug).ignore_on_userdebug), \
"bpfloader min version must be >= 0.33 in order to use ignored_on");
+#define ABSOLUTE(x) ((x) < 0 ? -(x) : (x))
+
+#define DEFAULT_BPF_MAP_FLAGS(type, num_entries, mapflags) \
+ ( (mapflags) | \
+ ((num_entries) < 0 ? BPF_F_NO_PREALLOC : 0) | \
+ (type == BPF_MAP_TYPE_LPM_TRIE ? BPF_F_NO_PREALLOC : 0) \
+ )
+
#define DEFINE_BPF_MAP_BASE(the_map, TYPE, keysize, valuesize, num_entries, \
usr, grp, md, selinux, pindir, share, minkver, \
maxkver, minloader, maxloader, ignore_eng, \
- ignore_user, ignore_userdebug) \
+ ignore_user, ignore_userdebug, mapflags) \
const struct bpf_map_def SECTION("maps") the_map = { \
.type = BPF_MAP_TYPE_##TYPE, \
.key_size = (keysize), \
.value_size = (valuesize), \
- .max_entries = (num_entries), \
- .map_flags = 0, \
+ .max_entries = ABSOLUTE(num_entries), \
+ .map_flags = DEFAULT_BPF_MAP_FLAGS(BPF_MAP_TYPE_##TYPE, num_entries, mapflags), \
.uid = (usr), \
.gid = (grp), \
.mode = (md), \
@@ -260,16 +291,17 @@
// Type safe macro to declare a ring buffer and related output functions.
// Compatibility:
// * BPF ring buffers are only available kernels 5.8 and above. Any program
-// accessing the ring buffer should set a program level min_kver >= 5.8.
-// * The definition below sets a map min_kver of 5.8 which requires targeting
+// accessing the ring buffer should set a program level min_kver >= 5.10,
+// since 5.10 is the next LTS version.
+// * The definition below sets a map min_kver of 5.10 which requires targeting
// a BPFLOADER_MIN_VER >= BPFLOADER_S_VERSION.
#define DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md, \
selinux, pindir, share, min_loader, max_loader, \
ignore_eng, ignore_user, ignore_userdebug) \
DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md, \
- selinux, pindir, share, KVER_5_8, KVER_INF, \
+ selinux, pindir, share, KVER_5_10, KVER_INF, \
min_loader, max_loader, ignore_eng, ignore_user, \
- ignore_userdebug); \
+ ignore_userdebug, 0); \
\
_Static_assert((size_bytes) >= 4096, "min 4 kiB ringbuffer size"); \
_Static_assert((size_bytes) <= 0x10000000, "max 256 MiB ringbuffer size"); \
@@ -317,11 +349,11 @@
/* type safe macro to declare a map and related accessor functions */
#define DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
selinux, pindir, share, min_loader, max_loader, ignore_eng, \
- ignore_user, ignore_userdebug) \
+ ignore_user, ignore_userdebug, mapFlags) \
DEFINE_BPF_MAP_BASE(the_map, TYPE, sizeof(KeyType), sizeof(ValueType), \
num_entries, usr, grp, md, selinux, pindir, share, \
KVER_NONE, KVER_INF, min_loader, max_loader, \
- ignore_eng, ignore_user, ignore_userdebug); \
+ ignore_eng, ignore_user, ignore_userdebug, mapFlags); \
BPF_MAP_ASSERT_OK(BPF_MAP_TYPE_##TYPE, (num_entries), (md)); \
_Static_assert(sizeof(KeyType) < 1024, "aosp/2370288 requires < 1024 byte keys"); \
_Static_assert(sizeof(ValueType) < 65536, "aosp/2370288 requires < 65536 byte values"); \
@@ -359,13 +391,13 @@
#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)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
#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)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -387,6 +419,22 @@
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
DEFAULT_BPF_MAP_UID, gid, 0660)
+// idea from Linux include/linux/compiler_types.h (eBPF is always a 64-bit arch)
+#define NATIVE_WORD(t) ((sizeof(t) == 1) || (sizeof(t) == 2) || (sizeof(t) == 4) || (sizeof(t) == 8))
+
+// simplified from Linux include/asm-generic/rwonce.h
+#define READ_ONCE(x) \
+ ({ \
+ _Static_assert(NATIVE_WORD(x), "READ_ONCE requires a native word size"); \
+ (*(const volatile typeof(x) *)&(x)) \
+ })
+
+#define WRITE_ONCE(x, value) \
+ do { \
+ _Static_assert(NATIVE_WORD(x), "WRITE_ONCE requires a native word size"); \
+ *(volatile typeof(x) *)&(x) = (value); \
+ } while (0)
+
// LLVM eBPF builtins: they directly generate BPF_LD_ABS/BPF_LD_IND (skb may be ignored?)
unsigned long long load_byte(void* skb, unsigned long long off) asm("llvm.bpf.load.byte");
unsigned long long load_half(void* skb, unsigned long long off) asm("llvm.bpf.load.half");
@@ -403,6 +451,8 @@
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;
diff --git a/bpf/headers/include/bpf_map_def.h b/bpf/headers/include/bpf_map_def.h
index 2d6736c..e95ca5f 100644
--- a/bpf/headers/include/bpf_map_def.h
+++ b/bpf/headers/include/bpf_map_def.h
@@ -94,6 +94,10 @@
_Static_assert(_Alignof(enum bpf_map_type) == 4, "_Alignof enum bpf_map_type != 4");
// Linux kernel requires sizeof(int) == 4, sizeof(void*) == sizeof(long), sizeof(long long) == 8
+_Static_assert(sizeof(int) == 4, "sizeof int != 4");
+_Static_assert(__alignof__(int) == 4, "__alignof__ int != 4");
+_Static_assert(_Alignof(int) == 4, "_Alignof int != 4");
+
_Static_assert(sizeof(unsigned int) == 4, "sizeof unsigned int != 4");
_Static_assert(__alignof__(unsigned int) == 4, "__alignof__ unsigned int != 4");
_Static_assert(_Alignof(unsigned int) == 4, "_Alignof unsigned int != 4");
@@ -102,8 +106,12 @@
// Here sizeof & __alignof__ are consistent, but _Alignof is not: compile for 'aosp_cf_x86_phone'
_Static_assert(sizeof(unsigned long long) == 8, "sizeof unsigned long long != 8");
_Static_assert(__alignof__(unsigned long long) == 8, "__alignof__ unsigned long long != 8");
-// BPF wants 8, but 32-bit x86 wants 4
-//_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+// BPF & everyone else wants 8, but 32-bit x86 wants 4
+#if defined(__i386__)
+_Static_assert(_Alignof(unsigned long long) == 4, "x86-32 _Alignof unsigned long long != 4");
+#else
+_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+#endif
// for maps:
@@ -155,7 +163,7 @@
enum bpf_map_type type;
unsigned int key_size;
unsigned int value_size;
- unsigned int max_entries;
+ int max_entries; // negative means BPF_F_NO_PREALLOC, but *might* not work with S
unsigned int map_flags;
// The following are not supported by the Android bpfloader:
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index 9a049c7..9486e75 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -61,6 +61,7 @@
// The following matches bpf_helpers.h, which is only for inclusion in bpf code
#define BPFLOADER_MAINLINE_VERSION 42u
+#define BPFLOADER_MAINLINE_25Q2_VERSION 47u
using android::base::EndsWith;
using android::base::GetIntProperty;
@@ -616,9 +617,6 @@
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.
@@ -732,6 +730,12 @@
}
enum bpf_map_type type = md[i].type;
+ if (type == BPF_MAP_TYPE_LPM_TRIE && !isAtLeastKernelVersion(4, 14, 0)) {
+ // On Linux Kernels older than 4.14 this map type doesn't exist - autoskip.
+ ALOGD("skipping LPM_TRIE map %s - requires kver 4.14+", mapNames[i].c_str());
+ mapFds.push_back(unique_fd());
+ continue;
+ }
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
@@ -794,7 +798,7 @@
.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),
+ .map_flags = md[i].map_flags,
};
if (isAtLeastKernelVersion(4, 15, 0))
strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
@@ -820,14 +824,14 @@
"tmp_map_" + objName + "_" + mapNames[i];
ret = bpfFdPin(fd, createLoc.c_str());
if (ret) {
- int err = errno;
+ const 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;
+ const int err = errno;
ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
err, strerror(err));
return -err;
@@ -835,32 +839,34 @@
} else {
ret = bpfFdPin(fd, mapPinLoc.c_str());
if (ret) {
- int err = errno;
+ const 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;
+ const 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;
+ const 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 {
+ if (isAtLeastKernelVersion(4, 14, 0)) {
+ int mapId = bpfGetFdMapId(fd);
+ if (mapId == -1) {
+ const int err = errno;
+ ALOGE("bpfGetFdMapId failed, errno: %d", err);
+ return -err;
+ }
ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
}
@@ -1003,7 +1009,7 @@
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"));
+ !fd.ok() ? std::strerror(errno) : "ok");
reuse = true;
} else {
static char log_buf[1 << 20]; // 1 MiB logging buffer
@@ -1034,7 +1040,7 @@
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"));
+ fd.get(), !fd.ok() ? std::strerror(errno) : "ok");
if (!fd.ok()) {
// kernel NULL terminates log_buf, so this checks for non-empty string
@@ -1063,14 +1069,14 @@
"tmp_prog_" + objName + '_' + string(name);
ret = bpfFdPin(fd, createLoc.c_str());
if (ret) {
- int err = errno;
+ const 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;
+ const int err = errno;
ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
err, strerror(err));
return -err;
@@ -1078,30 +1084,52 @@
} else {
ret = bpfFdPin(fd, progPinLoc.c_str());
if (ret) {
- int err = errno;
+ const 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;
+ const 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;
+ const 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);
+ if (isAtLeastKernelVersion(4, 14, 0)) {
+ int progId = bpfGetFdProgId(fd);
+ if (progId == -1) {
+ const int err = errno;
+ ALOGE("bpfGetFdProgId failed, errno: %d", err);
+ return -err;
+ }
+
+ int jitLen = bpfGetFdJitProgLen(fd);
+ if (jitLen == -1) {
+ const int err = errno;
+ ALOGE("bpfGetFdJitProgLen failed, ret: %d", err);
+ return -err;
+ }
+
+ int xlatLen = bpfGetFdXlatProgLen(fd);
+ if (xlatLen == -1) {
+ const int err = errno;
+ ALOGE("bpfGetFdXlatProgLen failed, ret: %d", err);
+ return -err;
+ }
+ ALOGI("prog %s id %d len jit:%d xlat:%d", progPinLoc.c_str(), progId, jitLen, xlatLen);
+
+ if (!jitLen && bpfloader_ver >= BPFLOADER_MAINLINE_25Q2_VERSION) {
+ ALOGE("Kernel eBPF JIT failure for %s", progPinLoc.c_str());
+ return -ENOTSUP;
+ }
}
}
@@ -1288,6 +1316,8 @@
#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;
@@ -1384,40 +1414,7 @@
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);
+ const int first_api_level = GetIntProperty("ro.board.first_api_level", api_level);
// last in U QPR2 beta1
const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
@@ -1430,10 +1427,10 @@
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
+ if (isAtLeast25Q2) ++bpfloader_ver; // [47] BPFLOADER_MAINLINE_25Q2_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,
+ bpfloader_ver, argv[0], android_get_device_api_level(), api_level,
kernelVersion(), describeArch(), getuid(),
has_platform_bpfloader_rc, has_platform_netbpfload_rc);
@@ -1473,6 +1470,13 @@
return 1;
}
+ // 25Q2 bumps the kernel requirement up to 5.4
+ // see also: //system/netd/tests/kernel_test.cpp TestKernel54
+ if (isAtLeast25Q2 && !isAtLeastKernelVersion(5, 4, 0)) {
+ ALOGE("Android 25Q2 requires kernel 5.4.");
+ 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)) {
@@ -1480,6 +1484,11 @@
if (!isTV()) return 1;
}
+ if (isKernel32Bit() && isAtLeast25Q2) {
+ ALOGE("Android 25Q2 requires 64 bit kernel.");
+ 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");
@@ -1496,13 +1505,13 @@
bool bad = false;
if (!isLtsKernel()) {
- ALOGW("Android V only supports LTS kernels.");
+ 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); \
+ ALOGW("Android V+ requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
bad = true; \
}
@@ -1512,6 +1521,7 @@
REQUIRE(5, 15, 136)
REQUIRE(6, 1, 57)
REQUIRE(6, 6, 0)
+ REQUIRE(6, 12, 0)
#undef REQUIRE
@@ -1542,16 +1552,15 @@
*
* Additionally the 32-bit kernel jit support is poor,
* and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
+ * Note, however, that TV and Wear devices will continue to support 32-bit userspace
+ * on ARM64.
*/
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()) {
+ if (isArm() && (isTV() || isWear())) {
+ // exempt Arm TV or Wear devices (arm32 ABI is far less problematic than x86-32)
+ ALOGW("[Arm TV/Wear] 32-bit userspace unsupported on 6.2+ kernels.");
+ } else if (first_api_level <= 33 /*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.");
@@ -1564,8 +1573,8 @@
}
}
- // Note: 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
- if (isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
+ // On handheld, 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
+ if (!isArm() && isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
ALOGE("64-bit userspace required on 6.7+ kernels.");
return 1;
}
@@ -1657,7 +1666,16 @@
}
// unreachable before U QPR3
- ALOGI("done, transferring control to platform bpfloader.");
+ if (exists(uprobestatsBpfLoader)) {
+ 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, };
diff --git a/bpf/netd/Android.bp b/bpf/netd/Android.bp
index fe4d999..473c8c9 100644
--- a/bpf/netd/Android.bp
+++ b/bpf/netd/Android.bp
@@ -82,7 +82,6 @@
"libcutils",
"liblog",
"libnetdutils",
- "libprocessgroup",
],
compile_multilib: "both",
multilib: {
diff --git a/bpf/netd/BpfBaseTest.cpp b/bpf/netd/BpfBaseTest.cpp
index 34dfbb4..4b8a04e 100644
--- a/bpf/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_HIERARCHY_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/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 8e4c2c6..e3e508b 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -22,7 +22,6 @@
#include <inttypes.h>
#include <android-base/unique_fd.h>
-#include <android-modules-utils/sdk_level.h>
#include <bpf/WaitForProgsLoaded.h>
#include <log/log.h>
#include <netdutils/UidConstants.h>
@@ -36,6 +35,12 @@
using base::unique_fd;
using base::WaitForProperty;
using bpf::getSocketCookie;
+using bpf::isAtLeastKernelVersion;
+using bpf::isAtLeastT;
+using bpf::isAtLeastU;
+using bpf::isAtLeastV;
+using bpf::isAtLeast25Q2;
+using bpf::queryProgram;
using bpf::retrieveProgram;
using netdutils::Status;
using netdutils::statusFromErrno;
@@ -56,7 +61,7 @@
if (!cgroupProg.ok()) {
return statusFromErrno(errno, fmt::format("Failed to get program from {}", programPath));
}
- if (android::bpf::attachProgram(type, cgroupProg, cgroupFd)) {
+ if (bpf::attachProgram(type, cgroupProg, cgroupFd)) {
return statusFromErrno(errno, fmt::format("Program {} attach failed", programPath));
}
return netdutils::status::ok;
@@ -74,29 +79,36 @@
if (!cg2_path) return Status("cg2_path is NULL");
// This code was mainlined in T, so this should be trivially satisfied.
- if (!modules::sdklevel::IsAtLeastT()) return Status("S- platform is unsupported");
+ if (!isAtLeastT) return Status("S- platform is unsupported");
// S requires eBPF support which was only added in 4.9, so this should be satisfied.
- if (!bpf::isAtLeastKernelVersion(4, 9, 0)) {
+ if (!isAtLeastKernelVersion(4, 9, 0)) {
return Status("kernel version < 4.9.0 is unsupported");
}
// U bumps the kernel requirement up to 4.14
- if (modules::sdklevel::IsAtLeastU() && !bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
return Status("U+ platform with kernel version < 4.14.0 is unsupported");
}
// U mandates this mount point (though it should also be the case on T)
- if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) {
+ if (isAtLeastU && !!strcmp(cg2_path, "/sys/fs/cgroup")) {
return Status("U+ platform with cg2_path != /sys/fs/cgroup is unsupported");
}
- unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (!cg_fd.ok()) {
- const int err = errno;
- ALOGE("Failed to open the cgroup directory: %s", strerror(err));
- return statusFromErrno(err, "Open the cgroup directory failed");
+ // V bumps the kernel requirement up to 4.19
+ if (isAtLeastV && !isAtLeastKernelVersion(4, 19, 0)) {
+ return Status("V+ platform with kernel version < 4.19.0 is unsupported");
}
+
+ // 25Q2 bumps the kernel requirement up to 5.4
+ if (isAtLeast25Q2 && !isAtLeastKernelVersion(5, 4, 0)) {
+ return Status("25Q2+ platform with kernel version < 5.4.0 is unsupported");
+ }
+
+ unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
+ if (!cg_fd.ok()) return statusFromErrno(errno, "Opening cgroup dir 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));
@@ -109,31 +121,35 @@
// cgroup if the program is pinned properly.
// TODO: delete the if statement once all devices should support cgroup
// socket filter (ie. the minimum kernel version required is 4.14).
- if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ if (isAtLeastKernelVersion(4, 14, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_CREATE_PROG_PATH,
cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
}
- if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+ if (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));
+ if (isAtLeastV) {
+ // 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 (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)) {
+ if (isAtLeastKernelVersion(5, 4, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_GETSOCKOPT_PROG_PATH,
cg_fd, BPF_CGROUP_GETSOCKOPT));
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SETSOCKOPT_PROG_PATH,
@@ -141,7 +157,7 @@
}
}
- if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ if (isAtLeastKernelVersion(4, 19, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND4_PROG_PATH,
cg_fd, BPF_CGROUP_INET4_BIND));
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND6_PROG_PATH,
@@ -149,28 +165,32 @@
// This should trivially pass, since we just attached up above,
// but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
+ if (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 (isAtLeastKernelVersion(5, 10, 0)) {
+ if (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();
+ if (isAtLeastV) {
+ // 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 (isAtLeastKernelVersion(4, 19, 0)) {
+ if (queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
+ if (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 (isAtLeastKernelVersion(5, 4, 0)) {
+ if (queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
+ if (queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
}
}
@@ -210,7 +230,7 @@
// but there could be platform provided (xt_)bpf programs that oem/vendor
// modified netd (which calls us during init) depends on...
ALOGI("Waiting for platform BPF programs");
- android::bpf::waitForProgsLoaded();
+ bpf::waitForProgsLoaded();
}
if (!mainlineNetBpfLoadDone()) {
@@ -242,12 +262,21 @@
// ...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();
+ if (!isAtLeast25Q2) waitForBpf();
RETURN_IF_NOT_OK(initPrograms(cg2_path));
RETURN_IF_NOT_OK(initMaps());
+ if (isAtLeast25Q2) {
+ // Make sure netd can create & write maps. sepolicy is V+, but enough to enforce on 25Q2+
+ int key = 1;
+ int value = 123;
+ unique_fd map(bpf::createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
+ if (!map.ok()) return statusFromErrno(errno, fmt::format("map create failed"));
+ int rv = bpf::writeToMapEntry(map, &key, &value, BPF_ANY);
+ if (rv) return statusFromErrno(errno, fmt::format("map write failed (rv={})", rv));
+ }
+
return netdutils::status::ok;
}
@@ -274,7 +303,7 @@
Status BpfHandler::initMaps() {
// bpfLock() requires bpfGetFdMapId which is only available on 4.14+ kernels.
- if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ if (isAtLeastKernelVersion(4, 14, 0)) {
mapLockTest();
}
@@ -284,7 +313,6 @@
RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
// initialized last so mCookieTagMap.isValid() implies everything else is valid too
RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
- ALOGI("%s successfully", __func__);
return netdutils::status::ok;
}
@@ -328,7 +356,7 @@
return -errno;
}
if (socketFamily != AF_INET && socketFamily != AF_INET6) {
- ALOGE("Unsupported family: %d", socketFamily);
+ ALOGV("Unsupported family: %d", socketFamily);
return -EAFNOSUPPORT;
}
@@ -339,7 +367,7 @@
return -errno;
}
if (socketProto != IPPROTO_UDP && socketProto != IPPROTO_TCP) {
- ALOGE("Unsupported protocol: %d", socketProto);
+ ALOGV("Unsupported protocol: %d", socketProto);
return -EPROTONOSUPPORT;
}
@@ -401,8 +429,8 @@
ALOGE("Failed to tag the socket: %s", strerror(res.error().code()));
return -res.error().code();
}
- ALOGD("Socket with cookie %" PRIu64 " tagged successfully with tag %" PRIu32 " uid %u "
- "and real uid %u", sock_cookie, tag, chargeUid, realUid);
+ ALOGV("Socket with cookie %" PRIu64 " tagged successfully with tag %" PRIu32 " uid %u "
+ "and real uid %u", sock_cookie, tag, chargeUid, realUid);
return 0;
}
@@ -413,10 +441,11 @@
if (!mCookieTagMap.isValid()) return -EPERM;
base::Result<void> res = mCookieTagMap.deleteValue(sock_cookie);
if (!res.ok()) {
- ALOGE("Failed to untag socket: %s", strerror(res.error().code()));
- return -res.error().code();
+ const int err = res.error().code();
+ if (err != ENOENT) ALOGE("Failed to untag socket: %s", strerror(err));
+ return -err;
}
- ALOGD("Socket with cookie %" PRIu64 " untagged successfully.", sock_cookie);
+ ALOGV("Socket with cookie %" PRIu64 " untagged successfully.", sock_cookie);
return 0;
}
diff --git a/bpf/progs/bpf_net_helpers.h b/bpf/progs/bpf_net_helpers.h
index a5664ba..4085ed4 100644
--- a/bpf/progs/bpf_net_helpers.h
+++ b/bpf/progs/bpf_net_helpers.h
@@ -84,6 +84,8 @@
#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
+static uint64_t (*bpf_get_netns_cookie)(void* ctx) = (void*)BPF_FUNC_get_netns_cookie;
+
// 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;
static uint64_t (*bpf_get_sk_cookie)(struct bpf_sock* sk) = (void*)BPF_FUNC_get_socket_cookie;
diff --git a/bpf/progs/netd.c b/bpf/progs/netd.c
index cbe856d..08635b3 100644
--- a/bpf/progs/netd.c
+++ b/bpf/progs/netd.c
@@ -25,10 +25,6 @@
static const int PASS = 1;
static const int DROP_UNLESS_DNS = 2; // internal to our program
-// Used for 'bool enable_tracing'
-static const bool TRACE_ON = true;
-static const bool TRACE_OFF = false;
-
// offsetof(struct iphdr, ihl) -- but that's a bitfield
#define IPPROTO_IHL_OFF 0
@@ -46,14 +42,14 @@
DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", \
PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
- LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
// For maps netd only needs read only access to
#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", \
PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
- LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
// For maps netd needs to be able to read and write
#define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
@@ -92,7 +88,7 @@
DEFINE_BPF_MAP_EXT(packet_trace_enabled_map, ARRAY, uint32_t, bool, 1,
AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
- LOAD_ON_USER, LOAD_ON_USERDEBUG)
+ LOAD_ON_USER, LOAD_ON_USERDEBUG, 0)
// A ring buffer on which packet information is pushed.
DEFINE_BPF_RINGBUF_EXT(packet_trace_ringbuf, PacketTrace, PACKET_TRACE_BUF_SIZE,
@@ -103,6 +99,17 @@
DEFINE_BPF_MAP_RO_NETD(data_saver_enabled_map, ARRAY, uint32_t, bool,
DATA_SAVER_ENABLED_MAP_SIZE)
+DEFINE_BPF_MAP_EXT(local_net_access_map, LPM_TRIE, LocalNetAccessKey, bool, 1000,
+ AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", PRIVATE,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG, LOAD_ON_USER,
+ LOAD_ON_USERDEBUG, 0)
+
+// not preallocated
+DEFINE_BPF_MAP_EXT(local_net_blocked_uid_map, HASH, uint32_t, bool, -1000,
+ AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", PRIVATE,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG, LOAD_ON_USER,
+ LOAD_ON_USERDEBUG, 0)
+
// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
// selinux contexts, because even non-xt_bpf iptables mutations are implemented as
// a full table dump, followed by an update in userspace, and then a reload into the kernel,
@@ -110,31 +117,34 @@
// program (see XT_BPF_MODE_PATH_PINNED) and then the iptables binary (or rather
// the kernel acting on behalf of it) must be able to retrieve the pinned program
// for the reload to succeed
-#define DEFINE_XTBPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog)
+#define DEFINE_XTBPF_PROG(SECTION_NAME, the_prog) \
+ DEFINE_BPF_PROG(SECTION_NAME, AID_ROOT, AID_NET_ADMIN, the_prog)
// programs that need to be usable by netd, but not by netutils_wrappers
// (this is because these are currently attached by the mainline provided libnetd_updatable .so
// which is loaded into netd and thus runs as netd uid/gid/selinux context)
-#define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV, maxKV) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
- minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+#define DEFINE_NETD_BPF_PROG_RANGES(SECTION_NAME, the_prog, minKV, maxKV, min_loader, max_loader) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, \
+ minKV, maxKV, min_loader, max_loader, MANDATORY, \
"fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
-#define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
- DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF)
+#define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, the_prog, minKV, maxKV) \
+ DEFINE_NETD_BPF_PROG_RANGES(SECTION_NAME, the_prog, minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER)
-#define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE)
+#define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, the_prog, min_kv) \
+ DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, the_prog, min_kv, KVER_INF)
-#define DEFINE_NETD_V_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV, \
+#define DEFINE_NETD_BPF_PROG(SECTION_NAME, the_prog) \
+ DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, the_prog, KVER_NONE)
+
+#define DEFINE_NETD_V_BPF_PROG_KVER(SECTION_NAME, the_prog, minKV) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, minKV, \
KVER_INF, BPFLOADER_MAINLINE_V_VERSION, BPFLOADER_MAX_VER, MANDATORY, \
"fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
// programs that only need to be usable by the system server
-#define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+#define DEFINE_SYS_BPF_PROG(SECTION_NAME, the_prog) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_NET_ADMIN, the_prog, KVER_NONE, KVER_INF, \
BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
"fs_bpf_net_shared", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
@@ -231,11 +241,70 @@
: bpf_skb_load_bytes(skb, L3_off, to, len);
}
+// False iff arguments are found with longest prefix match lookup and disallowed.
+static inline __always_inline bool is_local_net_access_allowed(const uint32_t if_index,
+ const struct in6_addr* remote_ip6, const uint16_t protocol, const __be16 remote_port) {
+ LocalNetAccessKey query_key = {
+ .lpm_bitlen = 8 * (sizeof(if_index) + sizeof(*remote_ip6) + sizeof(protocol)
+ + sizeof(remote_port)),
+ .if_index = if_index,
+ .remote_ip6 = *remote_ip6,
+ .protocol = protocol,
+ .remote_port = remote_port
+ };
+ bool* v = bpf_local_net_access_map_lookup_elem(&query_key);
+ return v ? *v : true;
+}
+
+static __always_inline inline bool should_block_local_network_packets(struct __sk_buff *skb,
+ const uint32_t uid, const struct egress_bool egress,
+ const struct kver_uint kver) {
+ if (is_system_uid(uid)) return false;
+
+ bool* block_local_net = bpf_local_net_blocked_uid_map_lookup_elem(&uid);
+ if (!block_local_net) return false; // uid not found in map
+ if (!*block_local_net) return false; // lookup returned 'bool false'
+
+ struct in6_addr remote_ip6;
+ uint8_t ip_proto;
+ uint8_t L4_off;
+ if (skb->protocol == htons(ETH_P_IP)) {
+ int remote_ip_ofs = egress.egress ? IP4_OFFSET(daddr) : IP4_OFFSET(saddr);
+ remote_ip6.s6_addr32[0] = 0;
+ remote_ip6.s6_addr32[1] = 0;
+ remote_ip6.s6_addr32[2] = htonl(0xFFFF);
+ (void)bpf_skb_load_bytes_net(skb, remote_ip_ofs, &remote_ip6.s6_addr32[3], 4, kver);
+ (void)bpf_skb_load_bytes_net(skb, IP4_OFFSET(protocol), &ip_proto, sizeof(ip_proto), kver);
+ uint8_t ihl;
+ (void)bpf_skb_load_bytes_net(skb, IPPROTO_IHL_OFF, &ihl, sizeof(ihl), kver);
+ L4_off = (ihl & 0x0F) * 4; // IHL calculation.
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ int remote_ip_ofs = egress.egress ? IP6_OFFSET(daddr) : IP6_OFFSET(saddr);
+ (void)bpf_skb_load_bytes_net(skb, remote_ip_ofs, &remote_ip6, sizeof(remote_ip6), kver);
+ (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(nexthdr), &ip_proto, sizeof(ip_proto), kver);
+ L4_off = sizeof(struct ipv6hdr);
+ } else {
+ return false;
+ }
+
+ __be16 remote_port = 0;
+ switch (ip_proto) {
+ case IPPROTO_TCP:
+ case IPPROTO_DCCP:
+ case IPPROTO_UDP:
+ case IPPROTO_UDPLITE:
+ case IPPROTO_SCTP:
+ (void)bpf_skb_load_bytes_net(skb, L4_off + (egress.egress ? 2 : 0), &remote_port, sizeof(remote_port), kver);
+ break;
+ }
+
+ return !is_local_net_access_allowed(skb->ifindex, &remote_ip6, ip_proto, remote_port);
+}
+
static __always_inline inline void do_packet_tracing(
const struct __sk_buff* const skb, const struct egress_bool egress, const uint32_t uid,
- const uint32_t tag, const bool enable_tracing, const struct kver_uint kver) {
- if (!enable_tracing) return;
- if (!KVER_IS_AT_LEAST(kver, 5, 8, 0)) return;
+ const uint32_t tag, const struct kver_uint kver) {
+ if (!KVER_IS_AT_LEAST(kver, 5, 10, 0)) return;
uint32_t mapKey = 0;
bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
@@ -393,7 +462,8 @@
static __always_inline inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid,
const struct egress_bool egress,
- const struct kver_uint kver) {
+ const struct kver_uint kver,
+ const struct sdk_level_uint lvl) {
if (is_system_uid(uid)) return PASS;
if (skip_owner_match(skb, egress, kver)) return PASS;
@@ -423,6 +493,11 @@
return DROP_UNLESS_DNS;
}
}
+
+ if (SDK_LEVEL_IS_AT_LEAST(lvl, 25Q2) && skb->ifindex == 1) {
+ // TODO: sdksandbox localhost restrictions
+ }
+
return PASS;
}
@@ -440,8 +515,8 @@
static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb,
const struct egress_bool egress,
- const bool enable_tracing,
- const struct kver_uint kver) {
+ const struct kver_uint kver,
+ const struct sdk_level_uint lvl) {
// sock_uid will be 'overflowuid' if !sk_fullsock(sk_to_full_sk(skb->sk))
uint32_t sock_uid = bpf_get_socket_uid(skb);
@@ -470,7 +545,7 @@
// CLAT daemon receives via an untagged AF_PACKET socket.
if (egress.egress && uid == AID_CLAT) return PASS;
- int match = bpf_owner_match(skb, sock_uid, egress, kver);
+ int match = bpf_owner_match(skb, sock_uid, egress, kver, lvl);
// Workaround for secureVPN with VpnIsolation enabled, refer to b/159994981 for details.
// Keep TAG_SYSTEM_DNS in sync with DnsResolver/include/netd_resolv/resolv.h
@@ -483,6 +558,10 @@
if (match == DROP_UNLESS_DNS) match = DROP;
}
+ if (SDK_LEVEL_IS_AT_LEAST(lvl, 25Q2) && (match != DROP)) {
+ if (should_block_local_network_packets(skb, uid, egress, kver)) match = DROP;
+ }
+
// If an outbound packet is going to be dropped, we do not count that traffic.
if (egress.egress && (match == DROP)) return DROP;
@@ -496,7 +575,7 @@
if (!selectedMap) return PASS; // cannot happen, needed to keep bpf verifier happy
- do_packet_tracing(skb, egress, uid, tag, enable_tracing, kver);
+ do_packet_tracing(skb, egress, uid, tag, kver);
update_stats_with_config(*selectedMap, skb, &key, egress, kver);
update_app_uid_stats_map(skb, &uid, egress, kver);
@@ -509,52 +588,104 @@
return match;
}
-// 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, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// -----
+
+// Supported kernel + platform/os version combinations:
+//
+// | 4.9 | 4.14 | 4.19 | 5.4 | 5.10 | 5.15 | 6.1 | 6.6 | 6.12 |
+// 25Q2 | | | | x | x | x | x | x | x |
+// V | | | x | x | x | x | x | x | | (netbpfload)
+// U | | x | x | x | x | x | x | | |
+// T | x | x | x | x | x | x | | | | (magic netbpfload)
+// S | x | x | x | x | x | | | | | (platform loads offload)
+// R | x | x | x | x | | | | | | (no mainline ebpf)
+//
+// Not relevant for eBPF, but R can also run on 4.4
+
+// ----- cgroupskb/ingress/stats -----
+
+// Android 25Q2+ 5.10+ (localnet protection + tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_10_25q2",
+ bpf_cgroup_ingress_5_10_25q2, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
+ return bpf_traffic_account(skb, INGRESS, KVER_5_10, SDK_LEVEL_25Q2);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
+// Android 25Q2+ 5.4 (localnet protection)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_4_25q2",
+ bpf_cgroup_ingress_5_4_25q2, KVER_5_4, KVER_5_10,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, INGRESS, KVER_5_4, SDK_LEVEL_25Q2);
+}
+
+// Android U/V 5.10+ (tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/ingress/stats$5_10_u",
+ bpf_cgroup_ingress_5_10_u, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAINLINE_25Q2_VERSION)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, INGRESS, KVER_5_10, SDK_LEVEL_U);
+}
+
+// Android T/U/V 4.19 & T/U/V/25Q2 5.4 & T 5.10/5.15
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19",
bpf_cgroup_ingress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_4_19);
+ return bpf_traffic_account(skb, INGRESS, KVER_4_19, SDK_LEVEL_T);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_4_14, KVER_NONE, KVER_4_19)
+// Android T 4.9 & T/U 4.14
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_9",
+ bpf_cgroup_ingress_4_9, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
+ return bpf_traffic_account(skb, INGRESS, KVER_NONE, SDK_LEVEL_T);
}
-// 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, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// ----- cgroupskb/egress/stats -----
+
+// Android 25Q2+ 5.10+ (localnet protection + tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_10_25q2",
+ bpf_cgroup_egress_5_10_25q2, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
+ return bpf_traffic_account(skb, EGRESS, KVER_5_10, SDK_LEVEL_25Q2);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
+// Android 25Q2+ 5.4 (localnet protection)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_4_25q2",
+ bpf_cgroup_egress_5_4_25q2, KVER_5_4, KVER_5_10,
+ BPFLOADER_MAINLINE_25Q2_VERSION, BPFLOADER_MAX_VER)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, EGRESS, KVER_5_4, SDK_LEVEL_25Q2);
+}
+
+// Android U/V 5.10+ (tracing)
+DEFINE_NETD_BPF_PROG_RANGES("cgroupskb/egress/stats$5_10_u",
+ bpf_cgroup_egress_5_10_u, KVER_5_10, KVER_INF,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAINLINE_25Q2_VERSION)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, EGRESS, KVER_5_10, SDK_LEVEL_U);
+}
+
+// Android T/U/V 4.19 & T/U/V/25Q2 5.4 & T 5.10/5.15
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19",
bpf_cgroup_egress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_4_19);
+ return bpf_traffic_account(skb, EGRESS, KVER_4_19, SDK_LEVEL_T);
}
-DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_4_14, KVER_NONE, KVER_4_19)
+// Android T 4.9 & T/U 4.14
+DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_9",
+ bpf_cgroup_egress_4_9, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
+ return bpf_traffic_account(skb, EGRESS, KVER_NONE, SDK_LEVEL_T);
}
+// -----
+
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", xt_bpf_egress_prog)
(struct __sk_buff* skb) {
// Clat daemon does not generate new traffic, all its traffic is accounted for already
// on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
@@ -573,7 +704,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", xt_bpf_ingress_prog)
(struct __sk_buff* skb) {
// Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
// (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
@@ -585,7 +716,7 @@
return XTBPF_MATCH;
}
-DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
+DEFINE_SYS_BPF_PROG("schedact/ingress/account",
tc_bpf_ingress_account_prog)
(struct __sk_buff* skb) {
if (is_received_skb(skb)) {
@@ -597,7 +728,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", xt_bpf_allowlist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
if (is_system_uid(sock_uid)) return XTBPF_MATCH;
@@ -616,7 +747,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", xt_bpf_denylist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
@@ -639,14 +770,12 @@
return permissions ? *permissions : BPF_PERMISSION_INTERNET;
}
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", AID_ROOT, AID_ROOT, inet_socket_create,
- KVER_4_14)
+DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", inet_socket_create, KVER_4_14)
(__unused struct bpf_sock* sk) {
return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? BPF_ALLOW : BPF_DISALLOW;
}
-DEFINE_NETD_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
- inet_socket_release, KVER_5_10)
+DEFINE_NETD_BPF_PROG_KVER("cgroupsockrelease/inet_release", 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);
@@ -699,47 +828,47 @@
return BPF_ALLOW;
}
-DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", AID_ROOT, AID_ROOT, inet4_bind, KVER_4_19)
+DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", 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)
+DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", 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_14)
+DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", 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", 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", 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", 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", 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", udp6_sendmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("getsockopt/prog", AID_ROOT, AID_ROOT, getsockopt_prog, KVER_5_4)
+DEFINE_NETD_V_BPF_PROG_KVER("getsockopt/prog", getsockopt_prog, KVER_5_4)
(struct bpf_sockopt *ctx) {
// 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)
@@ -747,7 +876,7 @@
return BPF_ALLOW;
}
-DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", AID_ROOT, AID_ROOT, setsockopt_prog, KVER_5_4)
+DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", setsockopt_prog, KVER_5_4)
(struct bpf_sockopt *ctx) {
// 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).
diff --git a/bpf/progs/netd.h b/bpf/progs/netd.h
index be7c311..8400679 100644
--- a/bpf/progs/netd.h
+++ b/bpf/progs/netd.h
@@ -185,6 +185,8 @@
#define PACKET_TRACE_RINGBUF_PATH BPF_NETD_PATH "map_netd_packet_trace_ringbuf"
#define PACKET_TRACE_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_packet_trace_enabled_map"
#define DATA_SAVER_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_data_saver_enabled_map"
+#define LOCAL_NET_ACCESS_MAP_PATH BPF_NETD_PATH "map_netd_local_net_access_map"
+#define LOCAL_NET_BLOCKED_UID_MAP_PATH BPF_NETD_PATH "map_netd_local_net_blocked_uid_map"
#endif // __cplusplus
@@ -245,6 +247,18 @@
} IngressDiscardValue;
STRUCT_SIZE(IngressDiscardValue, 2 * 4); // 8
+typedef struct {
+ // Longest prefix match length in bits (value from 0 to 192).
+ uint32_t lpm_bitlen;
+ uint32_t if_index;
+ // IPv4 uses IPv4-mapped IPv6 address format.
+ struct in6_addr remote_ip6;
+ // u16 instead of u8 to avoid padding due to alignment requirement.
+ uint16_t protocol;
+ __be16 remote_port;
+} LocalNetAccessKey;
+STRUCT_SIZE(LocalNetAccessKey, 4 + 4 + 16 + 2 + 2); // 28
+
// Entry in the configuration map that stores which UID rules are enabled.
#define UID_RULES_CONFIGURATION_KEY 0
// Entry in the configuration map that stores which stats map is currently in use.
diff --git a/bpf/progs/offload.c b/bpf/progs/offload.c
index 631908a..0f23844 100644
--- a/bpf/progs/offload.c
+++ b/bpf/progs/offload.c
@@ -609,27 +609,27 @@
// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
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)
+ sched_cls_tether_downstream4_rawip_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_10);
}
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)
+ sched_cls_tether_upstream4_rawip_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_10);
}
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)
+ sched_cls_tether_downstream4_ether_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_10);
}
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)
+ sched_cls_tether_upstream4_ether_5_8, KVER_5_10)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
+ return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_10);
}
// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
@@ -638,7 +638,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_rawip_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
@@ -646,7 +646,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_rawip_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_4_14);
}
@@ -654,7 +654,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_downstream4_ether_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
@@ -662,7 +662,7 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
AID_ROOT, AID_NETWORK_STACK,
sched_cls_tether_upstream4_ether_opt,
- KVER_4_14, KVER_5_8)
+ KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_4_14);
}
@@ -682,13 +682,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", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
+ sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_10)
(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", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
+ sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
}
@@ -715,13 +715,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", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
+ sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_10)
(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", AID_ROOT, AID_NETWORK_STACK,
- sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
+ sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_10)
(struct __sk_buff* skb) {
return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
}
@@ -805,7 +805,7 @@
}
#define DEFINE_XDP_PROG(str, func) \
- DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_9)(struct xdp_md *ctx)
+ DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_10)(struct xdp_md *ctx)
DEFINE_XDP_PROG("xdp/tether_downstream_ether",
xdp_tether_downstream_ether) {
diff --git a/bpf/syscall_wrappers/include/BpfSyscallWrappers.h b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
index 73cef89..1d72b77 100644
--- a/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), \
}}; \
@@ -281,21 +269,20 @@
return info.FIELD; \
}
-// All 7 of these fields are already present in Linux v4.14 (even ACK 4.14-P)
+// All 9 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)
+DEFINE_BPF_GET_FD(prog, JitProgLen, jited_prog_len) // int bpfGetFdJitProgLen(...)
+DEFINE_BPF_GET_FD(prog, XlatProgLen, xlated_prog_len) // int bpfGetFdXlatProgLen(...)
#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/bpf/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
index 0b5b7be..75fb8e9 100644
--- a/bpf/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -20,7 +20,8 @@
#include <set>
#include <string>
-#include <android-modules-utils/sdk_level.h>
+#include <android-base/properties.h>
+#include <android/api-level.h>
#include <bpf/BpfUtils.h>
#include <gtest/gtest.h>
@@ -30,11 +31,12 @@
using std::string;
using android::bpf::isAtLeastKernelVersion;
-using android::modules::sdklevel::IsAtLeastR;
-using android::modules::sdklevel::IsAtLeastS;
-using android::modules::sdklevel::IsAtLeastT;
-using android::modules::sdklevel::IsAtLeastU;
-using android::modules::sdklevel::IsAtLeastV;
+using android::bpf::isAtLeastR;
+using android::bpf::isAtLeastS;
+using android::bpf::isAtLeastT;
+using android::bpf::isAtLeastU;
+using android::bpf::isAtLeastV;
+using android::bpf::isAtLeast25Q2;
#define PLATFORM "/sys/fs/bpf/"
#define TETHERING "/sys/fs/bpf/tethering/"
@@ -159,6 +161,12 @@
NETD "prog_netd_setsockopt_prog",
};
+// Provided by *current* mainline module for 25Q2+ devices
+static const set<string> MAINLINE_FOR_25Q2_PLUS = {
+ NETD "map_netd_local_net_access_map",
+ NETD "map_netd_local_net_blocked_uid_map",
+};
+
static void addAll(set<string>& a, const set<string>& b) {
a.insert(b.begin(), b.end());
}
@@ -181,33 +189,36 @@
// and for the presence of mainline stuff.
// Note: Q is no longer supported by mainline
- ASSERT_TRUE(IsAtLeastR());
+ ASSERT_TRUE(isAtLeastR);
// R can potentially run on pre-4.9 kernel non-eBPF capable devices.
- DO_EXPECT(IsAtLeastR() && !IsAtLeastS() && isAtLeastKernelVersion(4, 9, 0), PLATFORM_ONLY_IN_R);
+ DO_EXPECT(isAtLeastR && !isAtLeastS && isAtLeastKernelVersion(4, 9, 0), PLATFORM_ONLY_IN_R);
// 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);
+ if (isAtLeastS) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
+ DO_EXPECT(isAtLeastS, MAINLINE_FOR_S_PLUS);
// Nothing added or removed in SCv2.
// T still only requires Linux Kernel 4.9+.
- 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);
+ 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.
- if (IsAtLeastU()) ASSERT_TRUE(isAtLeastKernelVersion(4, 14, 0));
- DO_EXPECT(IsAtLeastU(), MAINLINE_FOR_U_PLUS);
- DO_EXPECT(IsAtLeastU() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_U_5_10_PLUS);
+ if (isAtLeastU) ASSERT_TRUE(isAtLeastKernelVersion(4, 14, 0));
+ DO_EXPECT(isAtLeastU, MAINLINE_FOR_U_PLUS);
+ DO_EXPECT(isAtLeastU && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_U_5_10_PLUS);
// V requires Linux Kernel 4.19+, but nothing (as yet) added or removed in V.
- 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);
+ 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);
+
+ if (isAtLeast25Q2) ASSERT_TRUE(isAtLeastKernelVersion(5, 4, 0));
+ DO_EXPECT(isAtLeast25Q2, MAINLINE_FOR_25Q2_PLUS);
for (const auto& file : mustExist) {
EXPECT_EQ(0, access(file.c_str(), R_OK)) << file << " does not exist";
diff --git a/clatd/main.c b/clatd/main.c
index f888041..7aa1671 100644
--- a/clatd/main.c
+++ b/clatd/main.c
@@ -37,7 +37,7 @@
/* function: stop_loop
* signal handler: stop the event loop
*/
-static void stop_loop() { running = 0; };
+static void stop_loop(__attribute__((unused)) int unused) { running = 0; };
/* function: print_help
* in case the user is running this on the command line
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index 39ff2d4..f89ff9d 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -17,6 +17,7 @@
aconfig_declarations {
name: "com.android.net.flags-aconfig",
package: "com.android.net.flags",
+ exportable: true,
container: "com.android.tethering",
srcs: ["flags.aconfig"],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
@@ -32,6 +33,17 @@
],
}
+java_aconfig_library {
+ name: "com.android.net.flags-aconfig-java-export",
+ aconfig_declarations: "com.android.net.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.wifi",
+ ],
+ mode: "exported",
+}
+
aconfig_declarations {
name: "com.android.net.thread.flags-aconfig",
package: "com.android.net.thread.flags",
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 4c6d8ba..51b4fc0 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -41,7 +41,7 @@
}
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"
@@ -156,3 +156,21 @@
bug: "354619988"
is_fixed_read_only: true
}
+
+flag {
+ name: "ipv6_over_ble"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "API flag for IPv6 over BLE"
+ bug: "372936361"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "restrict_local_network"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for controlling access to the local network behind a new runtime permission. Requires ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK to enable feature."
+ bug: "388774939"
+ is_fixed_read_only: true
+}
diff --git a/common/networksecurity_flags.aconfig b/common/networksecurity_flags.aconfig
index 6438ba4..4a83af4 100644
--- a/common/networksecurity_flags.aconfig
+++ b/common/networksecurity_flags.aconfig
@@ -8,3 +8,12 @@
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/src/com/android/net/module/util/bpf/LocalNetAccessKey.java b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
new file mode 100644
index 0000000..95265b9
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
@@ -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.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.InetAddressUtils;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+public class LocalNetAccessKey extends Struct {
+
+ @Field(order = 0, type = Type.U32)
+ public final long lpmBitlen;
+ @Field(order = 1, type = Type.U32)
+ public final long ifIndex;
+ @Field(order = 2, type = Type.Ipv6Address)
+ public final Inet6Address remoteAddress;
+ @Field(order = 3, type = Type.U16)
+ public final int protocol;
+ @Field(order = 4, type = Type.UBE16)
+ public final int remotePort;
+
+ public LocalNetAccessKey(long lpmBitlen, long ifIndex, InetAddress remoteAddress, int protocol,
+ int remotePort) {
+ this.lpmBitlen = lpmBitlen;
+ this.ifIndex = ifIndex;
+ this.protocol = protocol;
+ this.remotePort = remotePort;
+
+ if (remoteAddress instanceof Inet4Address) {
+ this.remoteAddress = InetAddressUtils.v4MappedV6Address((Inet4Address) remoteAddress);
+ } else {
+ this.remoteAddress = (Inet6Address) remoteAddress;
+ }
+ }
+
+ public LocalNetAccessKey(long lpmBitlen, long ifIndex, Inet6Address remoteAddress, int protocol,
+ int remotePort) {
+ this.lpmBitlen = lpmBitlen;
+ this.ifIndex = ifIndex;
+ this.remoteAddress = remoteAddress;
+ this.protocol = protocol;
+ this.remotePort = remotePort;
+ }
+
+ @Override
+ public String toString() {
+ return "LocalNetAccessKey{"
+ + "lpmBitlen=" + lpmBitlen
+ + ", ifIndex=" + ifIndex
+ + ", remoteAddress=" + remoteAddress
+ + ", protocol=" + protocol
+ + ", remotePort=" + remotePort
+ + "}";
+ }
+}
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
index 60120bc..8cc2bb4 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -44,3 +44,12 @@
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 7551b92..9d6d356 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -102,6 +102,7 @@
java_library {
name: "framework-connectivity-t-pre-jarjar",
defaults: ["framework-connectivity-t-defaults"],
+ installable: false,
libs: [
"framework-bluetooth.stubs.module_lib",
"framework-wifi.stubs.module_lib",
@@ -172,6 +173,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/current.txt b/framework-t/api/current.txt
index 9ae0cf7..d66482c 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -210,6 +210,23 @@
package android.net.nsd {
+ @FlaggedApi("com.android.net.flags.ipv6_over_ble") public final class AdvertisingRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method public long getFlags();
+ method public int getProtocolType();
+ method @NonNull public android.net.nsd.NsdServiceInfo getServiceInfo();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.AdvertisingRequest> CREATOR;
+ field public static final long FLAG_SKIP_PROBING = 2L; // 0x2L
+ }
+
+ @FlaggedApi("com.android.net.flags.ipv6_over_ble") public static final class AdvertisingRequest.Builder {
+ ctor public AdvertisingRequest.Builder(@NonNull android.net.nsd.NsdServiceInfo);
+ method @NonNull public android.net.nsd.AdvertisingRequest build();
+ method @NonNull public android.net.nsd.AdvertisingRequest.Builder setFlags(long);
+ method @NonNull public android.net.nsd.AdvertisingRequest.Builder setProtocolType(int);
+ }
+
@FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public final class DiscoveryRequest implements android.os.Parcelable {
method public int describeContents();
method @Nullable public android.net.Network getNetwork();
@@ -288,6 +305,7 @@
method public java.util.Map<java.lang.String,byte[]> getAttributes();
method @Deprecated public java.net.InetAddress getHost();
method @NonNull public java.util.List<java.net.InetAddress> getHostAddresses();
+ method @FlaggedApi("com.android.net.flags.ipv6_over_ble") @Nullable public String getHostname();
method @Nullable public android.net.Network getNetwork();
method public int getPort();
method public String getServiceName();
diff --git a/framework-t/src/android/net/INetworkStatsService.aidl b/framework-t/src/android/net/INetworkStatsService.aidl
index 01ac106..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,16 +79,13 @@
void unregisterUsageRequest(in DataUsageRequest request);
/** Get the uid stats information since boot */
- NetworkStats getTypelessUidStats(int uid);
+ StatsResult getUidStats(int uid);
/** Get the iface stats information since boot */
- NetworkStats getTypelessIfaceStats(String iface);
+ StatsResult getIfaceStats(String iface);
/** Get the total network stats information since boot */
- NetworkStats getTypelessTotalStats();
-
- /** Get the uid stats information (with specified type) since boot */
- long getUidStats(int uid, int type);
+ StatsResult getTotalStats();
/** Registers a network stats provider */
INetworkStatsProviderCallback registerNetworkStatsProvider(String tag,
@@ -107,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/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 7c9b3ec..449588a 100644
--- a/framework-t/src/android/net/NetworkStatsAccess.java
+++ b/framework-t/src/android/net/NetworkStatsAccess.java
@@ -111,6 +111,12 @@
/** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
public static @NetworkStatsAccess.Level int checkAccessLevel(
Context context, int callingPid, int callingUid, @Nullable String callingPackage) {
+ final int appId = UserHandle.getAppId(callingUid);
+ if (appId == Process.SYSTEM_UID) {
+ // the system can access data usage for all apps on the device.
+ // check system uid first, to avoid possible dead lock from other APIs
+ return NetworkStatsAccess.Level.DEVICE;
+ }
final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class);
final TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -126,16 +132,13 @@
Binder.restoreCallingIdentity(token);
}
- final int appId = UserHandle.getAppId(callingUid);
-
final boolean isNetworkStack = PermissionUtils.hasAnyPermissionOf(
context, callingPid, callingUid, android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
- if (hasCarrierPrivileges || isDeviceOwner
- || appId == Process.SYSTEM_UID || isNetworkStack) {
- // Carrier-privileged apps and device owners, and the system (including the
- // network stack) can access data usage for all apps on the device.
+ if (hasCarrierPrivileges || isDeviceOwner || isNetworkStack) {
+ // Carrier-privileged apps and device owners, and the network stack
+ // can access data usage for all apps on the device.
return NetworkStatsAccess.Level.DEVICE;
}
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index 3b6a69b..868033a 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -17,8 +17,12 @@
package android.net;
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkStats.UID_ALL;
+
+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,20 +33,26 @@
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.media.MediaPlayer;
+import android.net.netstats.StatsResult;
+import android.net.netstats.TrafficStatsRateLimitCacheConfig;
import android.os.Binder;
import android.os.Build;
import android.os.RemoteException;
import android.os.StrictMode;
+import android.os.SystemClock;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.BinderUtils;
+import com.android.net.module.util.LruCacheWithExpiry;
+
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.Socket;
import java.net.SocketException;
-import java.util.Iterator;
-import java.util.Objects;
-
+import java.util.function.LongSupplier;
/**
* Class that provides network traffic statistics. These statistics include
@@ -177,10 +187,51 @@
/** @hide */
public static final int TAG_SYSTEM_PROBE = 0xFFFFFF42;
+ private static final StatsResult EMPTY_STATS = new StatsResult(0L, 0L, 0L, 0L);
+
+ private static final Object sRateLimitCacheLock = new Object();
+
+ @GuardedBy("TrafficStats.class")
+ @Nullable
private static INetworkStatsService sStatsService;
+ // The variable will only be accessed in the test, which is effectively
+ // single-threaded.
+ @Nullable
+ private static INetworkStatsService sStatsServiceForTest = null;
+
+ // This holds the configuration for the TrafficStats rate limit caches.
+ // It will be filled with the result of a query to the service the first time
+ // the caller invokes get*Stats APIs.
+ // This variable can be accessed from any thread with the lock held.
+ @GuardedBy("sRateLimitCacheLock")
+ @Nullable
+ private static TrafficStatsRateLimitCacheConfig sRateLimitCacheConfig;
+
+ // Cache for getIfaceStats and getTotalStats binder interfaces.
+ // This variable can be accessed from any thread with the lock held,
+ // while the cache itself is thread-safe and can be accessed outside
+ // the lock.
+ @GuardedBy("sRateLimitCacheLock")
+ @Nullable
+ private static LruCacheWithExpiry<String, StatsResult> sRateLimitIfaceCache;
+
+ // Cache for getUidStats binder interface.
+ // This variable can be accessed from any thread with the lock held,
+ // while the cache itself is thread-safe and can be accessed outside
+ // the lock.
+ @GuardedBy("sRateLimitCacheLock")
+ @Nullable
+ private static LruCacheWithExpiry<Integer, StatsResult> sRateLimitUidCache;
+
+ // The variable will only be accessed in the test, which is effectively
+ // single-threaded.
+ @Nullable
+ private static LongSupplier sTimeSupplierForTest = 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());
@@ -188,6 +239,43 @@
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;
+ }
+
+ /**
+ * Set time supplier for test, or null to reset.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public static void setTimeSupplierForTest(LongSupplier timeSupplier) {
+ sTimeSupplierForTest = timeSupplier;
+ }
+
+ /**
+ * Trigger query rate-limit cache config and initializing the caches.
+ *
+ * This is for test purpose.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public static void reinitRateLimitCacheForTest() {
+ maybeGetConfigAndInitRateLimitCache(true /* forceReinit */);
+ }
+
/**
* Snapshot of {@link NetworkStats} when the currently active profiling
* session started, or {@code null} if no session active.
@@ -228,6 +316,92 @@
sStatsService = statsManager.getBinder();
}
+ @Nullable
+ private static LruCacheWithExpiry<String, StatsResult> maybeGetRateLimitIfaceCache() {
+ if (!maybeGetConfigAndInitRateLimitCache(false /* forceReinit */)) return null;
+ synchronized (sRateLimitCacheLock) {
+ return sRateLimitIfaceCache;
+ }
+ }
+
+ @Nullable
+ private static LruCacheWithExpiry<Integer, StatsResult> maybeGetRateLimitUidCache() {
+ if (!maybeGetConfigAndInitRateLimitCache(false /* forceReinit */)) return null;
+ synchronized (sRateLimitCacheLock) {
+ return sRateLimitUidCache;
+ }
+ }
+
+ /**
+ * Gets the rate limit cache configuration and init caches if null.
+ *
+ * Gets the configuration from the service as the configuration
+ * is not expected to change dynamically. And use it to initialize
+ * rate-limit cache if not yet initialized.
+ *
+ * @return whether the rate-limit cache is enabled.
+ *
+ * @hide
+ */
+ private static boolean maybeGetConfigAndInitRateLimitCache(boolean forceReinit) {
+ // Access the service outside the lock to avoid potential deadlocks. This is
+ // especially important when the caller is a system component (e.g.,
+ // NetworkPolicyManagerService) that might hold other locks that the service
+ // also needs.
+ // Although this introduces a race condition where multiple threads might
+ // query the service concurrently, it's acceptable in this case because the
+ // configuration doesn't change dynamically. The configuration only needs to
+ // be fetched once before initializing the cache.
+ synchronized (sRateLimitCacheLock) {
+ if (sRateLimitCacheConfig != null && !forceReinit) {
+ return sRateLimitCacheConfig.isCacheEnabled;
+ }
+ }
+
+ final TrafficStatsRateLimitCacheConfig config;
+ try {
+ config = getStatsService().getRateLimitCacheConfig();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ synchronized (sRateLimitCacheLock) {
+ if (sRateLimitCacheConfig == null || forceReinit) {
+ sRateLimitCacheConfig = config;
+ initRateLimitCacheLocked();
+ }
+ }
+ return config.isCacheEnabled;
+ }
+
+ @GuardedBy("sRateLimitCacheLock")
+ private static void initRateLimitCacheLocked() {
+ // Set up rate limiting caches.
+ // Use uid cache with UID_ALL to cache total stats.
+ if (sRateLimitCacheConfig.isCacheEnabled) {
+ // A time supplier which is monotonic until device reboots, and counts
+ // time spent in sleep. This is needed to ensure the get*Stats caller
+ // won't get stale value after system time adjustment or waking up from sleep.
+ final LongSupplier realtimeSupplier = (sTimeSupplierForTest != null
+ ? sTimeSupplierForTest : () -> SystemClock.elapsedRealtime());
+ sRateLimitIfaceCache = new LruCacheWithExpiry<String, StatsResult>(
+ realtimeSupplier,
+ sRateLimitCacheConfig.expiryDurationMs,
+ sRateLimitCacheConfig.maxEntries,
+ (statsResult) -> !isEmpty(statsResult)
+ );
+ sRateLimitUidCache = new LruCacheWithExpiry<Integer, StatsResult>(
+ realtimeSupplier,
+ sRateLimitCacheConfig.expiryDurationMs,
+ sRateLimitCacheConfig.maxEntries,
+ (statsResult) -> !isEmpty(statsResult)
+ );
+ } else {
+ sRateLimitIfaceCache = null;
+ sRateLimitUidCache = null;
+ }
+ }
+
/**
* Attach the socket tagger implementation to the current process, to
* get notified when a socket's {@link FileDescriptor} is assigned to
@@ -242,8 +416,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() {
}
@@ -450,7 +624,7 @@
*/
@Deprecated
public static void setThreadStatsUidSelf() {
- setThreadStatsUid(android.os.Process.myUid());
+ setThreadStatsUid(getMyUid());
}
/**
@@ -591,7 +765,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) {
@@ -710,6 +884,14 @@
android.Manifest.permission.NETWORK_STACK,
android.Manifest.permission.NETWORK_SETTINGS})
public static void clearRateLimitCaches() {
+ final LruCacheWithExpiry<String, StatsResult> ifaceCache = maybeGetRateLimitIfaceCache();
+ if (ifaceCache != null) {
+ ifaceCache.clear();
+ }
+ final LruCacheWithExpiry<Integer, StatsResult> uidCache = maybeGetRateLimitUidCache();
+ if (uidCache != null) {
+ uidCache.clear();
+ }
try {
getStatsService().clearTrafficStatsRateLimitCaches();
} catch (RemoteException e) {
@@ -959,45 +1141,76 @@
/** @hide */
public static long getUidStats(int uid, int type) {
- if (!isEntryValueTypeValid(type)
- || android.os.Process.myUid() != uid) {
- return UNSUPPORTED;
- }
- final NetworkStats stats;
+ return fetchStats(maybeGetRateLimitUidCache(), uid,
+ () -> getStatsService().getUidStats(uid), type);
+ }
+
+ // Note: This method calls to the service, do not invoke this method with lock held.
+ private static <K> long fetchStats(
+ @Nullable LruCacheWithExpiry<K, StatsResult> cache, K key,
+ BinderUtils.ThrowingSupplier<StatsResult, RemoteException> statsFetcher, int type) {
try {
- stats = getStatsService().getTypelessUidStats(uid);
+ final StatsResult stats;
+ if (cache != null) {
+ stats = fetchStatsWithCache(cache, key, statsFetcher);
+ } else {
+ // Cache is not enabled, fetch directly from service.
+ stats = statsFetcher.get();
+ }
+ return getEntryValueForType(stats, type);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
- return getValueForTypeFromFirstEntry(stats, type);
+ }
+
+ // Note: This method calls to the service, do not invoke this method with lock held.
+ @Nullable
+ private static <K> StatsResult fetchStatsWithCache(LruCacheWithExpiry<K, StatsResult> cache,
+ K key, BinderUtils.ThrowingSupplier<StatsResult, RemoteException> statsFetcher)
+ throws RemoteException {
+ // Attempt to retrieve from the cache first.
+ StatsResult stats = cache.get(key);
+
+ // Although the cache instance is thread-safe, this can still introduce a
+ // race condition between threads of the same process, potentially
+ // returning non-monotonic results. This is because there is no lock
+ // between get, fetch, and put operations. This is considered acceptable
+ // because varying thread execution speeds can also cause non-monotonic
+ // results, even with locking.
+ if (stats == null) {
+ // Cache miss, fetch from the service.
+ stats = statsFetcher.get();
+
+ // Update the cache with the fetched result if valid.
+ if (stats != null && !isEmpty(stats)) {
+ final StatsResult cachedValue = cache.putIfAbsent(key, stats);
+ if (cachedValue != null) {
+ // Some other thread cached a value after this thread
+ // originally got a cache miss. Return the cached value
+ // to ensure all returned values after caching are consistent.
+ return cachedValue;
+ }
+ }
+ }
+ return stats;
+ }
+
+ private static boolean isEmpty(StatsResult stats) {
+ return stats.equals(EMPTY_STATS);
}
/** @hide */
public static long getTotalStats(int type) {
- if (!isEntryValueTypeValid(type)) {
- return UNSUPPORTED;
- }
- final NetworkStats stats;
- try {
- stats = getStatsService().getTypelessTotalStats();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- return getValueForTypeFromFirstEntry(stats, type);
+ // In practice, Bpf doesn't use UID_ALL for storing per-UID stats.
+ // Use uid cache with UID_ALL to cache total stats.
+ return fetchStats(maybeGetRateLimitUidCache(), UID_ALL,
+ () -> getStatsService().getTotalStats(), type);
}
/** @hide */
public static long getIfaceStats(String iface, int type) {
- if (!isEntryValueTypeValid(type)) {
- return UNSUPPORTED;
- }
- final NetworkStats stats;
- try {
- stats = getStatsService().getTypelessIfaceStats(iface);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- return getValueForTypeFromFirstEntry(stats, type);
+ return fetchStats(maybeGetRateLimitIfaceCache(), iface,
+ () -> getStatsService().getIfaceStats(iface), type);
}
/**
@@ -1094,7 +1307,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) {
@@ -1127,18 +1340,18 @@
public static final int TYPE_TX_PACKETS = 3;
/** @hide */
- private static long getEntryValueForType(@NonNull NetworkStats.Entry entry, int type) {
- Objects.requireNonNull(entry);
+ 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 entry.getRxBytes();
+ return stats.rxBytes;
case TYPE_RX_PACKETS:
- return entry.getRxPackets();
+ return stats.rxPackets;
case TYPE_TX_BYTES:
- return entry.getTxBytes();
+ return stats.txBytes;
case TYPE_TX_PACKETS:
- return entry.getTxPackets();
+ return stats.txPackets;
default:
throw new IllegalStateException("Bug: Invalid type: "
+ type + " should not reach here.");
@@ -1157,13 +1370,5 @@
return false;
}
}
-
- /** @hide */
- public static long getValueForTypeFromFirstEntry(@NonNull NetworkStats stats, int type) {
- Objects.requireNonNull(stats);
- Iterator<NetworkStats.Entry> iter = stats.iterator();
- if (!iter.hasNext()) return UNSUPPORTED;
- return getEntryValueForType(iter.next(), type);
- }
}
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/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
index 6afb2d5..a62df65 100644
--- a/framework-t/src/android/net/nsd/AdvertisingRequest.java
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -15,12 +15,16 @@
*/
package android.net.nsd;
+import android.annotation.FlaggedApi;
import android.annotation.LongDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.net.nsd.NsdManager.ProtocolType;
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.net.flags.Flags;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Duration;
@@ -28,16 +32,32 @@
/**
* Encapsulates parameters for {@link NsdManager#registerService}.
- * @hide
*/
-//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+@FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
public final class AdvertisingRequest implements Parcelable {
/**
* Only update the registration without sending exit and re-announcement.
+ * @hide
*/
public static final long NSD_ADVERTISING_UPDATE_ONLY = 1;
+ // TODO: if apps are allowed to set hostnames, the below doc should be updated to mention that
+ // passed in hostnames must also be known unique to use this flag.
+ /**
+ * Skip the probing step when advertising.
+ *
+ * <p>This must only be used when the service name ({@link NsdServiceInfo#getServiceName()} is
+ * known to be unique and cannot possibly be used by any other device on the network.
+ */
+ public static final long FLAG_SKIP_PROBING = 1 << 1;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(flag = true, prefix = {"FLAG_"}, value = {
+ FLAG_SKIP_PROBING,
+ })
+ public @interface AdvertisingFlags {}
@NonNull
public static final Creator<AdvertisingRequest> CREATOR =
@@ -79,7 +99,7 @@
/**
* The constructor for the advertiseRequest
*/
- private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+ private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, @ProtocolType int protocolType,
long advertisingConfig, @NonNull Duration ttl) {
mServiceInfo = serviceInfo;
mProtocolType = protocolType;
@@ -88,7 +108,7 @@
}
/**
- * Returns the {@link NsdServiceInfo}
+ * @return the {@link NsdServiceInfo} describing the service to advertise.
*/
@NonNull
public NsdServiceInfo getServiceInfo() {
@@ -96,16 +116,18 @@
}
/**
- * Returns the service advertise protocol
+ * @return the service advertisement protocol.
*/
+ @ProtocolType
public int getProtocolType() {
return mProtocolType;
}
/**
- * Returns the advertising config.
+ * @return the flags affecting advertising behavior.
*/
- public long getAdvertisingConfig() {
+ @AdvertisingFlags
+ public long getFlags() {
return mAdvertisingConfig;
}
@@ -165,34 +187,45 @@
dest.writeLong(mTtl == null ? -1L : mTtl.getSeconds());
}
-// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
/**
- * The builder for creating new {@link AdvertisingRequest} objects.
- * @hide
+ * A builder for creating new {@link AdvertisingRequest} objects.
*/
+ @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
public static final class Builder {
@NonNull
private final NsdServiceInfo mServiceInfo;
- private final int mProtocolType;
+ private int mProtocolType;
private long mAdvertisingConfig;
@Nullable
private Duration mTtl;
+
/**
* Creates a new {@link Builder} object.
+ * @param serviceInfo the {@link NsdServiceInfo} describing the service to advertise.
+ * @param protocolType the advertising protocol to use.
+ * @hide
*/
- public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) {
+ public Builder(@NonNull NsdServiceInfo serviceInfo, @ProtocolType int protocolType) {
mServiceInfo = serviceInfo;
mProtocolType = protocolType;
}
/**
+ * Creates a new {@link Builder} object.
+ * @param serviceInfo the {@link NsdServiceInfo} describing the service to advertise.
+ */
+ public Builder(@NonNull NsdServiceInfo serviceInfo) {
+ this(serviceInfo, NsdManager.PROTOCOL_DNS_SD);
+ }
+
+ /**
* Sets advertising configuration flags.
*
- * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags.
+ * @param flags flags to use for advertising.
*/
@NonNull
- public Builder setAdvertisingConfig(long advertisingConfigFlags) {
- mAdvertisingConfig = advertisingConfigFlags;
+ public Builder setFlags(@AdvertisingFlags long flags) {
+ mAdvertisingConfig = flags;
return this;
}
@@ -232,6 +265,16 @@
return this;
}
+ /**
+ * Sets the protocol to use for advertising.
+ * @param protocolType the advertising protocol to use.
+ */
+ @NonNull
+ public Builder setProtocolType(@ProtocolType int protocolType) {
+ mProtocolType = protocolType;
+ return this;
+ }
+
/** Creates a new {@link AdvertisingRequest} object. */
@NonNull
public AdvertisingRequest build() {
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 116bea6..426a92d 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -314,6 +314,13 @@
/** Dns based service discovery protocol */
public static final int PROTOCOL_DNS_SD = 0x0001;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"PROTOCOL_"}, value = {
+ PROTOCOL_DNS_SD,
+ })
+ public @interface ProtocolType {}
+
/**
* The minimum TTL seconds which is allowed for a service registration.
*
@@ -1272,7 +1279,7 @@
// documented in the NsdServiceInfo.setSubtypes API instead, but this provides a limited
// option for users of the older undocumented behavior, only for subtype changes.
if (isSubtypeUpdateRequest(serviceInfo, listener)) {
- builder.setAdvertisingConfig(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
+ builder.setFlags(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
}
registerService(builder.build(), executor, listener);
}
@@ -1358,7 +1365,7 @@
checkProtocol(protocolType);
final int key;
// For update only request, the old listener has to be reused
- if ((advertisingRequest.getAdvertisingConfig()
+ if ((advertisingRequest.getFlags()
& AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) {
key = updateRegisteredListener(listener, executor, serviceInfo);
} else {
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 18c59d9..6a5ab4d 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -200,19 +200,19 @@
/**
* Get the hostname.
*
- * <p>When a service is resolved, it returns the hostname of the resolved service . The top
- * level domain ".local." is omitted.
- *
- * <p>For example, it returns "MyHost" when the service's hostname is "MyHost.local.".
- *
- * @hide
+ * <p>When a service is resolved through {@link NsdManager#resolveService} or
+ * {@link NsdManager#registerServiceInfoCallback}, this returns the hostname of the resolved
+ * service. In all other cases, this will be null. The top level domain ".local." is omitted.
+ * For example, this returns "MyHost" when the service's hostname is "MyHost.local.".
*/
-// @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
+ @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
@Nullable
public String getHostname() {
return mHostname;
}
+ // TODO: if setHostname is made public, AdvertisingRequest#FLAG_SKIP_PROBING javadoc must be
+ // updated to mention that hostnames must also be known unique to use that flag.
/**
* Set a custom hostname for this service instance for registration.
*
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index e4b2f43..fd824f3 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -282,7 +282,7 @@
}
/**
- * Get the service type. (e.g. "_http._tcp.local" )
+ * Get the service type. (e.g. "_http._tcp" )
*/
@NonNull
public String getServiceType() {
diff --git a/framework/Android.bp b/framework/Android.bp
index 0334e11..f66bc60 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -64,6 +64,7 @@
":net-utils-framework-common-srcs",
":framework-connectivity-api-shared-srcs",
":framework-networksecurity-sources",
+ ":statslog-framework-connectivity-java-gen",
],
aidl: {
generate_get_transaction_name: true,
@@ -74,6 +75,7 @@
// the module builds against API (the parcelable declarations exist in framework.aidl)
"frameworks/base/core/java", // For framework parcelables
"frameworks/native/aidl/binder", // For PersistableBundle.aidl
+ "packages/modules/Connectivity/Tethering/common/TetheringLib/src",
],
},
stub_only_libs: [
@@ -104,6 +106,7 @@
"androidx.annotation_annotation",
"app-compat-annotations",
"framework-connectivity-t.stubs.module_lib",
+ "framework-statsd.stubs.module_lib",
"unsupportedappusage",
],
apex_available: [
@@ -141,6 +144,7 @@
java_library {
name: "framework-connectivity-pre-jarjar",
defaults: ["framework-module-defaults"],
+ installable: false,
min_sdk_version: "30",
static_libs: [
"framework-connectivity-pre-jarjar-without-cronet",
@@ -156,6 +160,9 @@
java_defaults {
name: "CronetJavaDefaults",
srcs: [":httpclient_api_sources"],
+ static_libs: [
+ "com.android.net.http.flags-aconfig-java",
+ ],
libs: [
"androidx.annotation_annotation",
],
@@ -187,6 +194,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",
@@ -212,6 +223,7 @@
},
aconfig_declarations: [
"com.android.net.flags-aconfig",
+ "com.android.net.http.flags-aconfig",
"com.android.networksecurity.flags-aconfig",
],
}
@@ -326,6 +338,7 @@
aidl: {
include_dirs: [
"packages/modules/Connectivity/framework/aidl-export",
+ "packages/modules/Connectivity/Tethering/common/TetheringLib/src",
"frameworks/native/aidl/binder", // For PersistableBundle.aidl
],
},
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 7bc0cf3..323c533 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -103,6 +103,7 @@
method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, int);
method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler, int);
method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
+ method @FlaggedApi("com.android.net.flags.ipv6_over_ble") public void reserveNetwork(@NonNull android.net.NetworkRequest, @NonNull android.os.Handler, @NonNull android.net.ConnectivityManager.NetworkCallback);
method @Deprecated public void setNetworkPreference(int);
method @Deprecated public static boolean setProcessDefaultNetwork(@Nullable android.net.Network);
method public void unregisterNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
@@ -151,6 +152,7 @@
method public void onLinkPropertiesChanged(@NonNull android.net.Network, @NonNull android.net.LinkProperties);
method public void onLosing(@NonNull android.net.Network, int);
method public void onLost(@NonNull android.net.Network);
+ method @FlaggedApi("com.android.net.flags.ipv6_over_ble") public void onReserved(@NonNull android.net.NetworkCapabilities);
method public void onUnavailable();
field public static final int FLAG_INCLUDE_LOCATION_INFO = 1; // 0x1
}
@@ -231,6 +233,32 @@
field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR;
}
+ @FlaggedApi("com.android.net.flags.ipv6_over_ble") public final class L2capNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getHeaderCompression();
+ method public int getPsm();
+ method @Nullable public android.net.MacAddress getRemoteAddress();
+ method public int getRole();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.L2capNetworkSpecifier> CREATOR;
+ field public static final int HEADER_COMPRESSION_6LOWPAN = 2; // 0x2
+ field public static final int HEADER_COMPRESSION_ANY = 0; // 0x0
+ field public static final int HEADER_COMPRESSION_NONE = 1; // 0x1
+ field public static final int PSM_ANY = 0; // 0x0
+ field public static final int ROLE_ANY = 0; // 0x0
+ field public static final int ROLE_CLIENT = 1; // 0x1
+ field public static final int ROLE_SERVER = 2; // 0x2
+ }
+
+ public static final class L2capNetworkSpecifier.Builder {
+ ctor public L2capNetworkSpecifier.Builder();
+ method @NonNull public android.net.L2capNetworkSpecifier build();
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setHeaderCompression(int);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setPsm(@IntRange(from=0, to=255) int);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setRemoteAddress(@Nullable android.net.MacAddress);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setRole(int);
+ }
+
public class LinkAddress implements android.os.Parcelable {
method public int describeContents();
method public java.net.InetAddress getAddress();
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/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index f3773de..f1a6f00 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -60,6 +60,11 @@
"/sys/fs/bpf/netd_shared/map_netd_data_saver_enabled_map";
public static final String INGRESS_DISCARD_MAP_PATH =
"/sys/fs/bpf/netd_shared/map_netd_ingress_discard_map";
+ public static final String LOCAL_NET_ACCESS_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_local_net_access_map";
+ public static final String LOCAL_NET_BLOCKED_UID_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_local_net_blocked_uid_map";
+
public static final Struct.S32 UID_RULES_CONFIGURATION_KEY = new Struct.S32(0);
public static final Struct.S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new Struct.S32(1);
public static final Struct.S32 DATA_SAVER_ENABLED_KEY = new Struct.S32(0);
diff --git a/framework/src/android/net/CaptivePortal.java b/framework/src/android/net/CaptivePortal.java
index 4a7b601..4c534f3 100644
--- a/framework/src/android/net/CaptivePortal.java
+++ b/framework/src/android/net/CaptivePortal.java
@@ -18,10 +18,19 @@
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
+import android.annotation.TargetApi;
+import android.os.Binder;
+import android.os.Build;
import android.os.IBinder;
+import android.os.OutcomeReceiver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.OsConstants;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
/**
* A class allowing apps handling the {@link ConnectivityManager#ACTION_CAPTIVE_PORTAL_SIGN_IN}
@@ -69,6 +78,15 @@
@SystemApi
public static final int APP_REQUEST_REEVALUATION_REQUIRED = APP_REQUEST_BASE + 0;
+ /**
+ * Binder object used for tracking the lifetime of the process, so CS can perform cleanup if
+ * the CaptivePortal app dies. This binder is not parcelled as part of this object. It is
+ * created in the client process and sent to the server by setDelegateUid so that the server
+ * can use it to register a death recipient.
+ *
+ */
+ private final Binder mLifetimeBinder = new Binder();
+
private final IBinder mBinder;
/** @hide */
@@ -167,4 +185,56 @@
@SystemApi
public void logEvent(int eventId, @NonNull String packageName) {
}
+
+ /**
+ * Sets the UID of the app that is allowed to perform network traffic for captive
+ * portal login.
+ *
+ * This app will be allowed to communicate directly on the captive
+ * portal by binding to the {@link android.net.Network} extra passed in the
+ * ACTION_CAPTIVE_PORTAL_SIGN_IN broadcast that contained this object.
+ *
+ * Communication will bypass network access restrictions such as VPNs and
+ * Private DNS settings, so the delegated UID must be trusted to ensure that only
+ * traffic intended for captive portal login binds to that network.
+ *
+ * By default, no UID is delegated. The delegation can be cleared by calling
+ * this method again with {@link android.os.Process.INVALID_UID}. Only one UID can
+ * be delegated at any given time.
+ *
+ * The operation is asynchronous. The uid is only guaranteed to have access when
+ * the provided OutcomeReceiver is called.
+ *
+ * @hide
+ */
+ @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+ // OutcomeReceiver is not available on R, but the mainline version of this
+ // class is only available on S+.
+ @TargetApi(Build.VERSION_CODES.S)
+ public void setDelegateUid(int uid, @NonNull Executor executor,
+ @NonNull final OutcomeReceiver<Void, ServiceSpecificException> receiver) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+ try {
+ ICaptivePortal.Stub.asInterface(mBinder).setDelegateUid(
+ uid,
+ mLifetimeBinder,
+ new IIntResultListener.Stub() {
+ @Override
+ public void onResult(int resultCode) {
+ if (resultCode != 0) {
+ final String msg = "Fail to set the delegate UID " + uid
+ + ", error: " + OsConstants.errnoName(resultCode);
+ executor.execute(() -> {
+ receiver.onError(new ServiceSpecificException(resultCode, msg));
+ });
+ } else {
+ executor.execute(() -> receiver.onResult(null));
+ }
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 1ebc4a3..5d99b74 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -21,6 +21,7 @@
import static android.net.NetworkRequest.Type.LISTEN;
import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
import static android.net.NetworkRequest.Type.REQUEST;
+import static android.net.NetworkRequest.Type.RESERVATION;
import static android.net.NetworkRequest.Type.TRACK_DEFAULT;
import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
import static android.net.QosCallback.QosCallbackRegistrationException;
@@ -1873,7 +1874,7 @@
public NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser(int userId) {
try {
return mService.getDefaultNetworkCapabilitiesForUser(
- userId, mContext.getOpPackageName(), getAttributionTag());
+ userId, mContext.getOpPackageName(), mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1967,7 +1968,7 @@
@NonNull String packageName) {
try {
return mService.getRedactedLinkPropertiesForPackage(
- lp, uid, packageName, getAttributionTag());
+ lp, uid, packageName, mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1993,7 +1994,7 @@
public NetworkCapabilities getNetworkCapabilities(@Nullable Network network) {
try {
return mService.getNetworkCapabilities(
- network, mContext.getOpPackageName(), getAttributionTag());
+ network, mContext.getOpPackageName(), mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2027,7 +2028,7 @@
int uid, @NonNull String packageName) {
try {
return mService.getRedactedNetworkCapabilitiesForPackage(nc, uid, packageName,
- getAttributionTag());
+ mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2752,21 +2753,13 @@
checkLegacyRoutingApiAccess();
try {
return mService.requestRouteToHostAddress(networkType, hostAddress.getAddress(),
- mContext.getOpPackageName(), getAttributionTag());
+ mContext.getOpPackageName(), mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
- * @return the context's attribution tag
- */
- // TODO: Remove method and replace with direct call once R code is pushed to AOSP
- private @Nullable String getAttributionTag() {
- return mContext.getAttributionTag();
- }
-
- /**
* Returns the value of the setting for background data usage. If false,
* applications should not use the network if the application is not in the
* foreground. Developers should respect this setting, and check the value
@@ -3072,7 +3065,8 @@
* <p>WARNING: New clients should not use this function. The only usages should be in PanService
* and WifiStateMachine which need direct access. All other clients should use
* {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
- * logic.</p>
+ * logic. On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will throw
+ * an UnsupportedOperationException.</p>
*
* @param iface the interface name to tether.
* @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -3097,7 +3091,8 @@
* <p>WARNING: New clients should not use this function. The only usages should be in PanService
* and WifiStateMachine which need direct access. All other clients should use
* {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
- * logic.</p>
+ * logic. On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will throw
+ * an UnsupportedOperationException.</p>
*
* @param iface the interface name to untether.
* @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -4279,12 +4274,18 @@
private static final int METHOD_ONLOST = 6;
/**
- * Called if no network is found within the timeout time specified in
- * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the
- * requested network request cannot be fulfilled (whether or not a timeout was
- * specified). When this callback is invoked the associated
- * {@link NetworkRequest} will have already been removed and released, as if
- * {@link #unregisterNetworkCallback(NetworkCallback)} had been called.
+ * If the callback was registered with one of the {@code requestNetwork} methods, this will
+ * be called if no network is found within the timeout specified in {@link
+ * #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the requested network
+ * request cannot be fulfilled (whether or not a timeout was specified).
+ *
+ * If the callback was registered when reserving a network, this method indicates that the
+ * reservation is removed. It can be called when the reservation is requested, because the
+ * system could not satisfy the reservation, or after the reserved network connects.
+ *
+ * When this callback is invoked the associated {@link NetworkRequest} will have already
+ * been removed and released, as if {@link #unregisterNetworkCallback(NetworkCallback)} had
+ * been called.
*/
@FilteredCallback(methodId = METHOD_ONUNAVAILABLE, calledByCallbackId = CALLBACK_UNAVAIL)
public void onUnavailable() {}
@@ -4425,6 +4426,28 @@
}
private static final int METHOD_ONBLOCKEDSTATUSCHANGED_INT = 14;
+ /**
+ * Called when a network is reserved.
+ *
+ * The reservation includes the {@link NetworkCapabilities} that uniquely describe the
+ * network that was reserved. the caller communicates this information to hardware or
+ * software components on or off-device to instruct them to create a network matching this
+ * reservation.
+ *
+ * {@link #onReserved(NetworkCapabilities)} is called at most once and is guaranteed to be
+ * called before any other callback unless the reservation is unavailable.
+ *
+ * Once a reservation is made, the reserved {@link NetworkCapabilities} will not be updated,
+ * and the reservation remains in place until the reserved network connects or {@link
+ * #onUnavailable} is called.
+ *
+ * @param networkCapabilities The {@link NetworkCapabilities} of the reservation.
+ */
+ @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+ @FilteredCallback(methodId = METHOD_ONRESERVED, calledByCallbackId = CALLBACK_RESERVED)
+ public void onReserved(@NonNull NetworkCapabilities networkCapabilities) {}
+ private static final int METHOD_ONRESERVED = 15;
+
private NetworkRequest networkRequest;
private final int mFlags;
}
@@ -4476,6 +4499,8 @@
public static final int CALLBACK_BLK_CHANGED = 11;
/** @hide */
public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
+ /** @hide */
+ public static final int CALLBACK_RESERVED = 13;
// When adding new IDs, note CallbackQueue assumes callback IDs are at most 16 bits.
@@ -4495,6 +4520,7 @@
case CALLBACK_RESUMED: return "CALLBACK_RESUMED";
case CALLBACK_BLK_CHANGED: return "CALLBACK_BLK_CHANGED";
case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: return "CALLBACK_LOCAL_NETWORK_INFO_CHANGED";
+ case CALLBACK_RESERVED: return "CALLBACK_RESERVED";
default:
return Integer.toString(whichCallback);
}
@@ -4525,6 +4551,7 @@
public static class NetworkCallbackMethodsHolder {
public static final NetworkCallbackMethod[] NETWORK_CB_METHODS =
new NetworkCallbackMethod[] {
+ method("onReserved", 1 << CALLBACK_RESERVED, NetworkCapabilities.class),
method("onPreCheck", 1 << CALLBACK_PRECHECK, Network.class),
// Note the final overload of onAvailable is not included, since it cannot
// match any overridden method.
@@ -4604,6 +4631,11 @@
}
switch (message.what) {
+ case CALLBACK_RESERVED: {
+ final NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
+ callback.onReserved(cap);
+ break;
+ }
case CALLBACK_PRECHECK: {
callback.onPreCheck(network);
break;
@@ -4705,12 +4737,12 @@
if (reqType == LISTEN) {
request = mService.listenForNetwork(
need, messenger, binder, callbackFlags, callingPackageName,
- getAttributionTag(), declaredMethodsFlag);
+ mContext.getAttributionTag(), declaredMethodsFlag);
} else {
request = mService.requestNetwork(
asUid, need, reqType.ordinal(), messenger, timeoutMs, binder,
- legacyType, callbackFlags, callingPackageName, getAttributionTag(),
- declaredMethodsFlag);
+ legacyType, callbackFlags, callingPackageName,
+ mContext.getAttributionTag(), declaredMethodsFlag);
}
if (request != null) {
sCallbacks.put(request, callback);
@@ -4985,6 +5017,41 @@
}
/**
+ * Reserve a network to satisfy a set of {@link NetworkCapabilities}.
+ *
+ * Some types of networks require the system to generate (i.e. reserve) some set of information
+ * before a network can be connected. For such networks, {@link #reserveNetwork} can be used
+ * which may lead to a call to {@link NetworkCallback#onReserved(NetworkCapabilities)}
+ * containing the {@link NetworkCapabilities} that were reserved.
+ *
+ * A reservation reserves at most one network. If the network connects, a reservation request
+ * behaves similar to a request filed using {@link #requestNetwork}. The provided {@link
+ * NetworkCallback} will only be called for the reserved network.
+ *
+ * If the system determines that the requested reservation can never be fulfilled, {@link
+ * NetworkCallback#onUnavailable} is called, the reservation is released by the system, and the
+ * provided callback can be reused. Otherwise, the reservation remains in place until the
+ * requested network connects. There is no guarantee that the reserved network will ever
+ * connect.
+ *
+ * @param request {@link NetworkRequest} describing this request.
+ * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+ * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+ * the callback must not be shared - it uniquely specifies this request.
+ */
+ // TODO: add executor overloads for all network request methods. Any method that passed an
+ // Executor could process the messages on the singleton ConnectivityThread Handler.
+ @SuppressLint("ExecutorRegistration")
+ @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+ public void reserveNetwork(@NonNull NetworkRequest request,
+ @NonNull Handler handler,
+ @NonNull NetworkCallback networkCallback) {
+ final CallbackHandler cbHandler = new CallbackHandler(handler);
+ final NetworkCapabilities nc = request.networkCapabilities;
+ sendRequestForNetwork(nc, networkCallback, 0, RESERVATION, TYPE_NONE, cbHandler);
+ }
+
+ /**
* Request a network to satisfy a set of {@link NetworkCapabilities}, limited
* by a timeout.
*
@@ -5127,7 +5194,7 @@
try {
mService.pendingRequestForNetwork(
request.networkCapabilities, operation, mContext.getOpPackageName(),
- getAttributionTag());
+ mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (ServiceSpecificException e) {
@@ -5276,7 +5343,7 @@
try {
mService.pendingListenForNetwork(
request.networkCapabilities, operation, mContext.getOpPackageName(),
- getAttributionTag());
+ mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (ServiceSpecificException e) {
diff --git a/framework/src/android/net/ICaptivePortal.aidl b/framework/src/android/net/ICaptivePortal.aidl
index e35f8d4..5cbb428 100644
--- a/framework/src/android/net/ICaptivePortal.aidl
+++ b/framework/src/android/net/ICaptivePortal.aidl
@@ -16,6 +16,9 @@
package android.net;
+import android.net.IIntResultListener;
+import android.os.IBinder;
+
/**
* Interface to inform NetworkMonitor of decisions of app handling captive portal.
* @hide
@@ -23,4 +26,5 @@
oneway interface ICaptivePortal {
void appRequest(int request);
void appResponse(int response);
+ void setDelegateUid(int uid, IBinder binder, IIntResultListener listener);
}
diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java
new file mode 100644
index 0000000..93f9352
--- /dev/null
+++ b/framework/src/android/net/L2capNetworkSpecifier.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2025 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;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A {@link NetworkSpecifier} used to identify an L2CAP network over BLE.
+ *
+ * An L2CAP network is not symmetrical, meaning there exists both a server (Bluetooth peripheral)
+ * and a client (Bluetooth central) node. This specifier contains the information required to
+ * request a client L2CAP network using {@link ConnectivityManager#requestNetwork} while specifying
+ * the remote MAC address, and Protocol/Service Multiplexer (PSM). It can also contain information
+ * allocated by the system when reserving a server network using {@link
+ * ConnectivityManager#reserveNetwork} such as the Protocol/Service Multiplexer (PSM). In both
+ * cases, the header compression option must be specified.
+ *
+ * An L2CAP server network allocates a Protocol/Service Multiplexer (PSM) to be advertised to the
+ * client. A new server network must always be reserved using {@code
+ * ConnectivityManager#reserveNetwork}. The subsequent {@link
+ * ConnectivityManager.NetworkCallback#onReserved(NetworkCapabilities)} callback includes an {@code
+ * L2CapNetworkSpecifier}. The {@link getPsm()} method will return the Protocol/Service Multiplexer
+ * (PSM) of the reserved network so that the server can advertise it to the client and the client
+ * can connect.
+ * An L2CAP server network is backed by a {@link android.bluetooth.BluetoothServerSocket} which can,
+ * in theory, accept many connections. However, before SDK version {@link
+ * Build.VERSION_CODES.VANILLA_ICE_CREAM} Bluetooth APIs do not expose the channel ID, so these
+ * connections are indistinguishable. In practice, this means that the network matching semantics in
+ * ConnectivityService will tear down all but the first connection.
+ *
+ * When the connection between client and server completes, a {@link Network} whose capabilities
+ * satisfy this {@code L2capNetworkSpecifier} will connect and the usual callbacks, such as {@link
+ * NetworkCallback#onAvailable}, will be called on the callback object passed to {@code
+ * ConnectivityManager#reserveNetwork} or {@code ConnectivityManager#requestNetwork}.
+ */
+@FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+public final class L2capNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+ /**
+ * Match any role.
+ *
+ * This role is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+ * networks never have this role set.
+ */
+ public static final int ROLE_ANY = 0;
+ /** Specifier describes a client network, i.e., the device is the Bluetooth central. */
+ public static final int ROLE_CLIENT = 1;
+ /** Specifier describes a server network, i.e., the device is the Bluetooth peripheral. */
+ public static final int ROLE_SERVER = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "ROLE_", value = {
+ ROLE_ANY,
+ ROLE_CLIENT,
+ ROLE_SERVER
+ })
+ public @interface Role {}
+ /** Role used to distinguish client from server networks. */
+ @Role
+ private final int mRole;
+
+ /**
+ * Accept any form of header compression.
+ *
+ * This option is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+ * networks never have this option set.
+ */
+ public static final int HEADER_COMPRESSION_ANY = 0;
+ /** Do not compress packets on this network. */
+ public static final int HEADER_COMPRESSION_NONE = 1;
+ /** Use 6lowpan header compression as specified in rfc6282. */
+ public static final int HEADER_COMPRESSION_6LOWPAN = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "HEADER_COMPRESSION_", value = {
+ HEADER_COMPRESSION_ANY,
+ HEADER_COMPRESSION_NONE,
+ HEADER_COMPRESSION_6LOWPAN
+ })
+ public @interface HeaderCompression {}
+ /** Header compression mechanism used on this network. */
+ @HeaderCompression
+ private final int mHeaderCompression;
+
+ /** The MAC address of the remote. */
+ @Nullable
+ private final MacAddress mRemoteAddress;
+
+ /**
+ * Match any Protocol/Service Multiplexer (PSM).
+ *
+ * This PSM value is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+ * networks never have this value set.
+ */
+ public static final int PSM_ANY = 0;
+
+ /** The Bluetooth L2CAP Protocol/Service Multiplexer (PSM). */
+ private final int mPsm;
+
+ private L2capNetworkSpecifier(Parcel in) {
+ mRole = in.readInt();
+ mHeaderCompression = in.readInt();
+ mRemoteAddress = in.readParcelable(getClass().getClassLoader());
+ mPsm = in.readInt();
+ }
+
+ /** @hide */
+ public L2capNetworkSpecifier(@Role int role, @HeaderCompression int headerCompression,
+ MacAddress remoteAddress, int psm) {
+ mRole = role;
+ mHeaderCompression = headerCompression;
+ mRemoteAddress = remoteAddress;
+ mPsm = psm;
+ }
+
+ /** Returns the role to be used for this network. */
+ @Role
+ public int getRole() {
+ return mRole;
+ }
+
+ /** Returns the compression mechanism for this network. */
+ @HeaderCompression
+ public int getHeaderCompression() {
+ return mHeaderCompression;
+ }
+
+ /**
+ * Returns the remote MAC address for this network to connect to.
+ *
+ * The remote address is only meaningful for networks that have ROLE_CLIENT.
+ *
+ * When receiving this {@link L2capNetworkSpecifier} from Connectivity APIs such as a {@link
+ * ConnectivityManager.NetworkCallback}, the MAC address is redacted.
+ */
+ public @Nullable MacAddress getRemoteAddress() {
+ return mRemoteAddress;
+ }
+
+ /** Returns the Protocol/Service Multiplexer (PSM) for this network to connect to. */
+ public int getPsm() {
+ return mPsm;
+ }
+
+ /**
+ * Checks whether the given L2capNetworkSpecifier is valid as part of a server network
+ * reservation request.
+ *
+ * @hide
+ */
+ public boolean isValidServerReservationSpecifier() {
+ // The ROLE_SERVER offer can be satisfied by a ROLE_ANY request.
+ if (mRole != ROLE_SERVER) return false;
+
+ // HEADER_COMPRESSION_ANY is never valid in a request.
+ if (mHeaderCompression == HEADER_COMPRESSION_ANY) return false;
+
+ // Remote address must be null for ROLE_SERVER requests.
+ if (mRemoteAddress != null) return false;
+
+ // reservation must allocate a PSM, so only PSM_ANY can be passed.
+ if (mPsm != PSM_ANY) return false;
+
+ return true;
+ }
+
+ /**
+ * Checks whether the given L2capNetworkSpecifier is valid as part of a client network request.
+ *
+ * @hide
+ */
+ public boolean isValidClientRequestSpecifier() {
+ // The ROLE_CLIENT offer can be satisfied by a ROLE_ANY request.
+ if (mRole != ROLE_CLIENT) return false;
+
+ // HEADER_COMPRESSION_ANY is never valid in a request.
+ if (mHeaderCompression == HEADER_COMPRESSION_ANY) return false;
+
+ // Remote address must not be null for ROLE_CLIENT requests.
+ if (mRemoteAddress == null) return false;
+
+ // Client network requests require a PSM to be specified.
+ // Ensure the PSM is within the valid range of dynamic BLE L2CAP values.
+ if (mPsm < 0x80) return false;
+ if (mPsm > 0xFF) return false;
+
+ return true;
+ }
+
+ /** A builder class for L2capNetworkSpecifier. */
+ public static final class Builder {
+ @Role
+ private int mRole = ROLE_ANY;
+ @HeaderCompression
+ private int mHeaderCompression = HEADER_COMPRESSION_ANY;
+ @Nullable
+ private MacAddress mRemoteAddress;
+ private int mPsm = PSM_ANY;
+
+ /**
+ * Set the role to use for this network.
+ *
+ * If not set, defaults to {@link ROLE_ANY}.
+ *
+ * @param role the role to use.
+ */
+ @NonNull
+ public Builder setRole(@Role int role) {
+ mRole = role;
+ return this;
+ }
+
+ /**
+ * Set the header compression mechanism to use for this network.
+ *
+ * If not set, defaults to {@link HEADER_COMPRESSION_ANY}. This option must be specified
+ * (i.e. must not be set to {@link HEADER_COMPRESSION_ANY}) when requesting or reserving a
+ * new network.
+ *
+ * @param headerCompression the header compression mechanism to use.
+ */
+ @NonNull
+ public Builder setHeaderCompression(@HeaderCompression int headerCompression) {
+ mHeaderCompression = headerCompression;
+ return this;
+ }
+
+ /**
+ * Set the remote address for the client to connect to.
+ *
+ * Only valid for client networks. If not set, the specifier matches any MAC address.
+ *
+ * @param remoteAddress the MAC address to connect to, or null to match any MAC address.
+ */
+ @NonNull
+ public Builder setRemoteAddress(@Nullable MacAddress remoteAddress) {
+ mRemoteAddress = remoteAddress;
+ return this;
+ }
+
+ /**
+ * Set the Protocol/Service Multiplexer (PSM) for the client to connect to.
+ *
+ * If not set, defaults to {@link PSM_ANY}.
+ *
+ * @param psm the Protocol/Service Multiplexer (PSM) to connect to.
+ */
+ @NonNull
+ public Builder setPsm(@IntRange(from = 0, to = 255) int psm) {
+ if (psm < 0 /* PSM_ANY */ || psm > 0xFF) {
+ throw new IllegalArgumentException("PSM must be PSM_ANY or within range [1, 255]");
+ }
+ mPsm = psm;
+ return this;
+ }
+
+ /** Create the L2capNetworkSpecifier object. */
+ @NonNull
+ public L2capNetworkSpecifier build() {
+ if (mRole == ROLE_SERVER && mRemoteAddress != null) {
+ throw new IllegalArgumentException(
+ "Specifying a remote address is not valid for server role.");
+ }
+ return new L2capNetworkSpecifier(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+ if (!(other instanceof L2capNetworkSpecifier)) return false;
+ final L2capNetworkSpecifier rhs = (L2capNetworkSpecifier) other;
+
+ // A network / offer cannot be ROLE_ANY, but it is added for consistency.
+ if (mRole != rhs.mRole && mRole != ROLE_ANY && rhs.mRole != ROLE_ANY) {
+ return false;
+ }
+
+ if (mHeaderCompression != rhs.mHeaderCompression
+ && mHeaderCompression != HEADER_COMPRESSION_ANY
+ && rhs.mHeaderCompression != HEADER_COMPRESSION_ANY) {
+ return false;
+ }
+
+ if (!Objects.equals(mRemoteAddress, rhs.mRemoteAddress)
+ && mRemoteAddress != null && rhs.mRemoteAddress != null) {
+ return false;
+ }
+
+ if (mPsm != rhs.mPsm && mPsm != PSM_ANY && rhs.mPsm != PSM_ANY) {
+ return false;
+ }
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ @Nullable
+ public NetworkSpecifier redact() {
+ final NetworkSpecifier redactedSpecifier = new Builder()
+ .setRole(mRole)
+ .setHeaderCompression(mHeaderCompression)
+ // The remote address is redacted.
+ .setRemoteAddress(null)
+ .setPsm(mPsm)
+ .build();
+ return redactedSpecifier;
+ }
+
+ /** @hide */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+ }
+
+ /** @hide */
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof L2capNetworkSpecifier)) return false;
+
+ final L2capNetworkSpecifier rhs = (L2capNetworkSpecifier) obj;
+ return mRole == rhs.mRole
+ && mHeaderCompression == rhs.mHeaderCompression
+ && Objects.equals(mRemoteAddress, rhs.mRemoteAddress)
+ && mPsm == rhs.mPsm;
+ }
+
+ /** @hide */
+ @Override
+ public String toString() {
+ final String role;
+ switch (mRole) {
+ case ROLE_CLIENT:
+ role = "ROLE_CLIENT";
+ break;
+ case ROLE_SERVER:
+ role = "ROLE_SERVER";
+ break;
+ default:
+ role = "ROLE_ANY";
+ break;
+ }
+
+ final String headerCompression;
+ switch (mHeaderCompression) {
+ case HEADER_COMPRESSION_NONE:
+ headerCompression = "HEADER_COMPRESSION_NONE";
+ break;
+ case HEADER_COMPRESSION_6LOWPAN:
+ headerCompression = "HEADER_COMPRESSION_6LOWPAN";
+ break;
+ default:
+ headerCompression = "HEADER_COMPRESSION_ANY";
+ break;
+ }
+
+ final String psm = (mPsm == PSM_ANY) ? "PSM_ANY" : String.valueOf(mPsm);
+
+ return String.format("L2capNetworkSpecifier(%s, %s, RemoteAddress=%s, PSM=%s)",
+ role, headerCompression, Objects.toString(mRemoteAddress), psm);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mRole);
+ dest.writeInt(mHeaderCompression);
+ dest.writeParcelable(mRemoteAddress, flags);
+ dest.writeInt(mPsm);
+ }
+
+ public static final @NonNull Creator<L2capNetworkSpecifier> CREATOR = new Creator<>() {
+ @Override
+ public L2capNetworkSpecifier createFromParcel(Parcel in) {
+ return new L2capNetworkSpecifier(in);
+ }
+
+ @Override
+ public L2capNetworkSpecifier[] newArray(int size) {
+ return new L2capNetworkSpecifier[size];
+ }
+ };
+}
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 4a50397..c6b62ee 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -359,6 +359,7 @@
mSubIds = new ArraySet<>();
mUnderlyingNetworks = null;
mEnterpriseId = 0;
+ mReservationId = RES_ID_UNSET;
}
/**
@@ -393,6 +394,7 @@
// necessary.
mUnderlyingNetworks = nc.mUnderlyingNetworks;
mEnterpriseId = nc.mEnterpriseId;
+ mReservationId = nc.mReservationId;
}
/**
@@ -2233,7 +2235,8 @@
&& (onlyImmutable || satisfiedByUids(nc))
&& (onlyImmutable || satisfiedBySSID(nc))
&& (onlyImmutable || satisfiedByRequestor(nc))
- && (onlyImmutable || satisfiedBySubscriptionIds(nc)));
+ && (onlyImmutable || satisfiedBySubscriptionIds(nc)))
+ && satisfiedByReservationId(nc);
}
/**
@@ -2347,7 +2350,8 @@
&& equalsAdministratorUids(that)
&& equalsSubscriptionIds(that)
&& equalsUnderlyingNetworks(that)
- && equalsEnterpriseCapabilitiesId(that);
+ && equalsEnterpriseCapabilitiesId(that)
+ && equalsReservationId(that);
}
@Override
@@ -2373,7 +2377,9 @@
+ Arrays.hashCode(mAdministratorUids) * 67
+ Objects.hashCode(mSubIds) * 71
+ Objects.hashCode(mUnderlyingNetworks) * 73
- + mEnterpriseId * 79;
+ + mEnterpriseId * 79
+ + mReservationId * 83;
+
}
@Override
@@ -2411,6 +2417,7 @@
dest.writeIntArray(CollectionUtils.toIntArray(mSubIds));
dest.writeTypedList(mUnderlyingNetworks);
dest.writeInt(mEnterpriseId & ALL_VALID_ENTERPRISE_IDS);
+ dest.writeInt(mReservationId);
}
public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -2446,6 +2453,7 @@
}
netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR));
netCap.mEnterpriseId = in.readInt() & ALL_VALID_ENTERPRISE_IDS;
+ netCap.mReservationId = in.readInt();
return netCap;
}
@Override
@@ -2548,6 +2556,11 @@
NetworkCapabilities::enterpriseIdNameOf, "&");
}
+ if (mReservationId != RES_ID_UNSET) {
+ final boolean isReservationOffer = (mReservationId == RES_ID_MATCH_ALL_RESERVATIONS);
+ sb.append(" ReservationId: ").append(isReservationOffer ? "*" : mReservationId);
+ }
+
sb.append(" UnderlyingNetworks: ");
if (mUnderlyingNetworks != null) {
sb.append("[");
@@ -2876,6 +2889,65 @@
}
/**
+ * The reservation ID used by non-reservable Networks and "regular" NetworkOffers.
+ *
+ * Note that {@code NetworkRequest#FIRST_REQUEST_ID} is 1;
+ * @hide
+ */
+ public static final int RES_ID_UNSET = 0;
+
+ /**
+ * The reservation ID used by special NetworkOffers that handle RESERVATION requests.
+ *
+ * NetworkOffers with {@code RES_ID_MATCH_ALL_RESERVATIONS} *only* receive onNetworkNeeded()
+ * callbacks for {@code NetworkRequest.Type.RESERVATION}.
+ * @hide
+ */
+ public static final int RES_ID_MATCH_ALL_RESERVATIONS = -1;
+
+ /**
+ * Unique ID that identifies the network reservation.
+ */
+ private int mReservationId;
+
+ /**
+ * Returns the reservation ID
+ * @hide
+ */
+ public int getReservationId() {
+ return mReservationId;
+ }
+
+ /**
+ * Set the reservation ID
+ * @hide
+ */
+ public void setReservationId(int resId) {
+ mReservationId = resId;
+ }
+
+ private boolean equalsReservationId(@NonNull NetworkCapabilities nc) {
+ return mReservationId == nc.mReservationId;
+ }
+
+ private boolean satisfiedByReservationId(@NonNull NetworkCapabilities nc) {
+ if (mReservationId == RES_ID_UNSET) {
+ // To maintain regular NetworkRequest semantics, a request with a zero reservationId
+ // matches an offer or network with any reservationId except MATCH_ALL_RESERVATIONS.
+ return nc.mReservationId != RES_ID_MATCH_ALL_RESERVATIONS;
+ }
+ // A request with a non-zero reservationId matches only an offer or network with that exact
+ // reservationId (required to match the network that will eventually come up) or
+ // MATCH_ALL_RESERVATIONS (required to match the blanket reservation offer).
+ if (nc.mReservationId == RES_ID_MATCH_ALL_RESERVATIONS) {
+ return true;
+ }
+ return mReservationId == nc.mReservationId;
+ }
+
+
+
+ /**
* Returns a bitmask of all the applicable redactions (based on the permissions held by the
* receiving app) to be performed on this object.
*
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 89572b3..5a08d44 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -32,6 +32,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.RES_ID_UNSET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import android.annotation.FlaggedApi;
@@ -193,6 +194,16 @@
* callbacks about the single, highest scoring current network
* (if any) that matches the specified NetworkCapabilities, or
*
+ * - RESERVATION requests behave identically to those of type REQUEST.
+ * For example, unlike LISTEN, they cause networks to remain
+ * connected, and they match exactly one network (the best one).
+ * A RESERVATION generates a unique reservationId in its
+ * NetworkCapabilities by copying the requestId which affects
+ * matching. A NetworkProvider can register a "blanket" NetworkOffer
+ * with reservationId = MATCH_ALL_RESERVATIONS to indicate that it
+ * is capable of generating NetworkOffers in response to RESERVATION
+ * requests.
+ *
* - TRACK_DEFAULT, which causes the framework to issue callbacks for
* the single, highest scoring current network (if any) that will
* be chosen for an app, but which cannot cause the framework to
@@ -229,6 +240,7 @@
BACKGROUND_REQUEST,
TRACK_SYSTEM_DEFAULT,
LISTEN_FOR_BEST,
+ RESERVATION,
};
/**
@@ -245,8 +257,17 @@
if (nc == null) {
throw new NullPointerException();
}
+ if (nc.getReservationId() != RES_ID_UNSET) {
+ throw new IllegalArgumentException("ReservationId must only be set by the system");
+ }
requestId = rId;
networkCapabilities = nc;
+ if (type == Type.RESERVATION) {
+ // Conceptually, the reservationId is not related to the requestId; however, the
+ // requestId fulfills the same uniqueness requirements that are needed for the
+ // reservationId, so it can be reused for this purpose.
+ networkCapabilities.setReservationId(rId);
+ }
this.legacyType = legacyType;
this.type = type;
}
@@ -261,6 +282,13 @@
this.type = that.type;
}
+ private NetworkRequest(Parcel in) {
+ networkCapabilities = NetworkCapabilities.CREATOR.createFromParcel(in);
+ legacyType = in.readInt();
+ requestId = in.readInt();
+ type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
+ }
+
/**
* Builder used to create {@link NetworkRequest} objects. Specify the Network features
* needed in terms of {@link NetworkCapabilities} features
@@ -657,12 +685,7 @@
public static final @android.annotation.NonNull Creator<NetworkRequest> CREATOR =
new Creator<NetworkRequest>() {
public NetworkRequest createFromParcel(Parcel in) {
- NetworkCapabilities nc = NetworkCapabilities.CREATOR.createFromParcel(in);
- int legacyType = in.readInt();
- int requestId = in.readInt();
- Type type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
- NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type);
- return result;
+ return new NetworkRequest(in);
}
public NetworkRequest[] newArray(int size) {
return new NetworkRequest[size];
@@ -703,7 +726,7 @@
* @hide
*/
public boolean isRequest() {
- return type == Type.REQUEST || type == Type.BACKGROUND_REQUEST;
+ return type == Type.REQUEST || type == Type.BACKGROUND_REQUEST || type == Type.RESERVATION;
}
/**
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 416c6de..cbc7a4f 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -23,8 +23,10 @@
import android.os.IBinder;
import android.os.RemoteException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Objects;
/**
@@ -196,45 +198,6 @@
}
/**
- * Create a tap interface for testing purposes
- *
- * @param linkAddrs an array of LinkAddresses to assign to the TAP interface
- * @return A TestNetworkInterface representing the underlying TAP interface. Close the contained
- * ParcelFileDescriptor to tear down the TAP interface.
- * @hide
- */
- @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
- @NonNull
- public TestNetworkInterface createTapInterface(@NonNull LinkAddress[] linkAddrs) {
- try {
- return mService.createInterface(TAP, CARRIER_UP, BRING_UP, USE_IPV6_PROV_DELAY,
- linkAddrs, null /* iface */);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
- * Create a tap interface for testing purposes
- *
- * @param bringUp whether to bring up the interface before returning it.
- *
- * @return A ParcelFileDescriptor of the underlying TAP interface. Close this to tear down the
- * TAP interface.
- * @hide
- */
- @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
- @NonNull
- public TestNetworkInterface createTapInterface(boolean bringUp) {
- try {
- return mService.createInterface(TAP, CARRIER_UP, bringUp, USE_IPV6_PROV_DELAY,
- NO_ADDRS, null /* iface */);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
* Create a tap interface with a given interface name for testing purposes
*
* @param bringUp whether to bring up the interface before returning it.
@@ -258,26 +221,6 @@
}
/**
- * Create a tap interface with or without carrier for testing purposes.
- *
- * Note: setting carrierUp = false is not supported until kernel version 6.0.
- *
- * @param carrierUp whether the created interface has a carrier or not.
- * @param bringUp whether to bring up the interface before returning it.
- * @hide
- */
- @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
- @NonNull
- public TestNetworkInterface createTapInterface(boolean carrierUp, boolean bringUp) {
- try {
- return mService.createInterface(TAP, carrierUp, bringUp, USE_IPV6_PROV_DELAY, NO_ADDRS,
- null /* iface */);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
* Create a tap interface for testing purposes.
*
* Note: setting carrierUp = false is not supported until kernel version 6.0.
@@ -300,27 +243,6 @@
}
/**
- * Create a tap interface for testing purposes.
- *
- * @param disableIpv6ProvisioningDelay whether to disable DAD and RS delay.
- * @param linkAddrs an array of LinkAddresses to assign to the TAP interface
- * @return A TestNetworkInterface representing the underlying TAP interface. Close the contained
- * ParcelFileDescriptor to tear down the TAP interface.
- * @hide
- */
- @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
- @NonNull
- public TestNetworkInterface createTapInterface(boolean disableIpv6ProvisioningDelay,
- @NonNull LinkAddress[] linkAddrs) {
- try {
- return mService.createInterface(TAP, CARRIER_UP, BRING_UP, disableIpv6ProvisioningDelay,
- linkAddrs, null /* iface */);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
* Enable / disable carrier on TestNetworkInterface
*
* Note: TUNSETCARRIER is not supported until kernel version 5.0.
@@ -337,4 +259,110 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Represents a request to create a tun/tap interface for testing.
+ *
+ * @hide
+ */
+ public static class TestInterfaceRequest {
+ public final boolean isTun;
+ public final boolean hasCarrier;
+ public final boolean bringUp;
+ public final boolean disableIpv6ProvDelay;
+ @Nullable public final String ifname;
+ public final LinkAddress[] linkAddresses;
+
+ private TestInterfaceRequest(boolean isTun, boolean hasCarrier, boolean bringUp,
+ boolean disableProvDelay, @Nullable String ifname, LinkAddress[] linkAddresses) {
+ this.isTun = isTun;
+ this.hasCarrier = hasCarrier;
+ this.bringUp = bringUp;
+ this.disableIpv6ProvDelay = disableProvDelay;
+ this.ifname = ifname;
+ this.linkAddresses = linkAddresses;
+ }
+
+ /**
+ * Builder class for TestInterfaceRequest
+ *
+ * Defaults to a tap interface with carrier that has been brought up.
+ */
+ public static class Builder {
+ private boolean mIsTun = false;
+ private boolean mHasCarrier = true;
+ private boolean mBringUp = true;
+ private boolean mDisableIpv6ProvDelay = false;
+ @Nullable private String mIfname;
+ private List<LinkAddress> mLinkAddresses = new ArrayList<>();
+
+ /** Create tun interface. */
+ public Builder setTun() {
+ mIsTun = true;
+ return this;
+ }
+
+ /** Create tap interface. */
+ public Builder setTap() {
+ mIsTun = false;
+ return this;
+ }
+
+ /** Configure whether the interface has carrier. */
+ public Builder setHasCarrier(boolean hasCarrier) {
+ mHasCarrier = hasCarrier;
+ return this;
+ }
+
+ /** Configure whether the interface should be brought up. */
+ public Builder setBringUp(boolean bringUp) {
+ mBringUp = bringUp;
+ return this;
+ }
+
+ /** Disable DAD and RS delay. */
+ public Builder setDisableIpv6ProvisioningDelay(boolean disableProvDelay) {
+ mDisableIpv6ProvDelay = disableProvDelay;
+ return this;
+ }
+
+ /** Set the interface name. */
+ public Builder setInterfaceName(@Nullable String ifname) {
+ mIfname = ifname;
+ return this;
+ }
+
+ /** The addresses to configure on the interface. */
+ public Builder addLinkAddress(LinkAddress la) {
+ mLinkAddresses.add(la);
+ return this;
+ }
+
+ /** Build TestInterfaceRequest */
+ public TestInterfaceRequest build() {
+ return new TestInterfaceRequest(mIsTun, mHasCarrier, mBringUp,
+ mDisableIpv6ProvDelay, mIfname, mLinkAddresses.toArray(new LinkAddress[0]));
+ }
+ }
+ }
+
+ /**
+ * Create a TestNetworkInterface (tun or tap) for testing purposes.
+ *
+ * @param request The request describing the interface to create.
+ * @return A TestNetworkInterface representing the underlying tun/tap interface. Close the
+ * contained ParcelFileDescriptor to tear down the tun/tap interface.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+ @NonNull
+ public TestNetworkInterface createTestInterface(@NonNull TestInterfaceRequest request) {
+ try {
+ // TODO: Make TestInterfaceRequest parcelable and pass it instead.
+ return mService.createInterface(request.isTun, request.hasCarrier, request.bringUp,
+ request.disableIpv6ProvDelay, request.linkAddresses, request.ifname);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 51df8ab..317854b 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -135,6 +135,17 @@
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static final long ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE = 74210811L;
+ /**
+ * Restrict local network access.
+ *
+ * Apps targeting a release after V will require permissions to access the local network.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public static final long RESTRICT_LOCAL_NETWORK = 365139289L;
+
private ConnectivityCompatChanges() {
}
}
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 150394b..9d7d144 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -32,7 +32,6 @@
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;
@@ -47,6 +46,7 @@
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
/**
* This class provides a way to perform Nearby related operations such as scanning, broadcasting
@@ -129,16 +129,6 @@
private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY_PERSIST =
"persist.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";
-
@GuardedBy("sScanListeners")
private static final WeakHashMap<ScanCallback, WeakReference<ScanListenerTransport>>
sScanListeners = new WeakHashMap<>();
@@ -479,36 +469,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
@@ -544,7 +504,7 @@
PoweredOffFindingEphemeralId ephemeralId = new PoweredOffFindingEphemeralId();
ephemeralId.bytes = eid;
return ephemeralId;
- }).toList();
+ }).collect(Collectors.toUnmodifiableList());
try {
mService.setPoweredOffFindingEphemeralIds(ephemeralIdList);
} catch (RemoteException e) {
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 66ae79c..ac381b8 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -89,6 +89,9 @@
break;
case BroadcastRequest.PRESENCE_VERSION_V1:
if (adapter.isLeExtendedAdvertisingSupported()) {
+ if (mAdvertisingSetCallback == null) {
+ mAdvertisingSetCallback = getAdvertisingSetCallback();
+ }
bluetoothLeAdvertiser.startAdvertisingSet(
getAdvertisingSetParameters(),
advertiseData,
@@ -133,6 +136,11 @@
}
mBroadcastListener = null;
mIsAdvertising = false;
+ // If called startAdvertisingSet() but onAdvertisingSetStopped() is not invoked yet,
+ // using the same mAdvertisingSetCallback will cause new advertising cann't be stopped.
+ // Therefore, release the old mAdvertisingSetCallback and
+ // create a new mAdvertisingSetCallback when calling startAdvertisingSet.
+ mAdvertisingSetCallback = null;
}
}
diff --git a/nearby/tests/cts/fastpair/AndroidManifest.xml b/nearby/tests/cts/fastpair/AndroidManifest.xml
index 472f4f0..9e1ec70 100644
--- a/nearby/tests/cts/fastpair/AndroidManifest.xml
+++ b/nearby/tests/cts/fastpair/AndroidManifest.xml
@@ -21,6 +21,7 @@
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<application>
<uses-library android:name="android.test.runner"/>
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..58d1808 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;
@@ -25,7 +26,6 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeTrue;
import android.app.UiAutomation;
import android.bluetooth.test_utils.EnableBluetoothRule;
@@ -78,10 +78,10 @@
@ClassRule public static final EnableBluetoothRule sEnableBluetooth = new EnableBluetoothRule();
- private static final byte[] SALT = new byte[]{1, 2};
- private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] SALT = new byte[] {1, 2};
+ private static final byte[] SECRET_ID = new byte[] {1, 2, 3, 4};
private static final byte[] META_DATA_ENCRYPTION_KEY = new byte[14];
- private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+ private static final byte[] AUTHENTICITY_KEY = new byte[] {0, 1, 1, 1};
private static final String DEVICE_NAME = "test_device";
private static final int BLE_MEDIUM = 1;
@@ -90,43 +90,45 @@
private UiAutomation mUiAutomation =
InstrumentationRegistry.getInstrumentation().getUiAutomation();
- private ScanRequest mScanRequest = new ScanRequest.Builder()
- .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
- .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
- .setBleEnabled(true)
- .build();
+ private ScanRequest mScanRequest =
+ new ScanRequest.Builder()
+ .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
+ .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
+ .setBleEnabled(true)
+ .build();
private PresenceDevice.Builder mBuilder =
new PresenceDevice.Builder("deviceId", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY);
- private ScanCallback mScanCallback = new ScanCallback() {
- @Override
- public void onDiscovered(@NonNull NearbyDevice device) {
- }
+ private ScanCallback mScanCallback =
+ new ScanCallback() {
+ @Override
+ public void onDiscovered(@NonNull NearbyDevice device) {}
- @Override
- public void onUpdated(@NonNull NearbyDevice device) {
- }
+ @Override
+ public void onUpdated(@NonNull NearbyDevice device) {}
- @Override
- public void onLost(@NonNull NearbyDevice device) {
- }
+ @Override
+ public void onLost(@NonNull NearbyDevice device) {}
- @Override
- public void onError(int errorCode) {
- }
- };
+ @Override
+ public void onError(int errorCode) {}
+ };
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
@Before
public void setUp() {
- mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG,
+ mUiAutomation.adoptShellPermissionIdentity(
+ READ_DEVICE_CONFIG,
+ WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG,
BLUETOOTH_PRIVILEGED);
- String nameSpace = SdkLevel.isAtLeastU() ? DeviceConfig.NAMESPACE_NEARBY
- : DeviceConfig.NAMESPACE_TETHERING;
- DeviceConfig.setProperty(nameSpace,
- "nearby_enable_presence_broadcast_legacy",
- "true", false);
+ String nameSpace =
+ SdkLevel.isAtLeastU()
+ ? DeviceConfig.NAMESPACE_NEARBY
+ : DeviceConfig.NAMESPACE_TETHERING;
+ DeviceConfig.setProperty(
+ nameSpace, "nearby_enable_presence_broadcast_legacy", "true", false);
mContext = InstrumentationRegistry.getContext();
mNearbyManager = mContext.getSystemService(NearbyManager.class);
@@ -143,8 +145,9 @@
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void test_startScan_noPrivilegedPermission() {
mUiAutomation.dropShellPermissionIdentity();
- assertThrows(SecurityException.class, () -> mNearbyManager
- .startScan(mScanRequest, EXECUTOR, mScanCallback));
+ assertThrows(
+ SecurityException.class,
+ () -> mNearbyManager.startScan(mScanRequest, EXECUTOR, mScanCallback));
}
@Test
@@ -158,23 +161,25 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testStartStopBroadcast() throws InterruptedException {
- PrivateCredential credential = new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY,
- META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
- .setIdentityType(IDENTITY_TYPE_PRIVATE)
- .build();
+ PrivateCredential credential =
+ new PrivateCredential.Builder(
+ SECRET_ID, AUTHENTICITY_KEY, META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
BroadcastRequest broadcastRequest =
new PresenceBroadcastRequest.Builder(
- Collections.singletonList(BLE_MEDIUM), SALT, credential)
+ Collections.singletonList(BLE_MEDIUM), SALT, credential)
.addAction(123)
.build();
CountDownLatch latch = new CountDownLatch(1);
- BroadcastCallback callback = status -> {
- latch.countDown();
- assertThat(status).isEqualTo(BroadcastCallback.STATUS_OK);
- };
- mNearbyManager.startBroadcast(broadcastRequest, Executors.newSingleThreadExecutor(),
- callback);
+ BroadcastCallback callback =
+ status -> {
+ latch.countDown();
+ assertThat(status).isEqualTo(BroadcastCallback.STATUS_OK);
+ };
+ mNearbyManager.startBroadcast(
+ broadcastRequest, Executors.newSingleThreadExecutor(), callback);
latch.await(10, TimeUnit.SECONDS);
mNearbyManager.stopBroadcast(callback);
}
@@ -196,9 +201,8 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testsetPoweredOffFindingEphemeralIds() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -207,24 +211,22 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testsetPoweredOffFindingEphemeralIds_noPrivilegedPermission() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
mUiAutomation.dropShellPermissionIdentity();
- assertThrows(SecurityException.class,
+ assertThrows(
+ SecurityException.class,
() -> mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20])));
}
-
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testSetAndGetPoweredOffFindingMode_enabled() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -233,30 +235,26 @@
// enableLocation() has dropped shell permission identity.
mUiAutomation.adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
- mNearbyManager.setPoweredOffFindingMode(
- NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+ mNearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
assertThat(mNearbyManager.getPoweredOffFindingMode())
.isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testSetAndGetPoweredOffFindingMode_disabled() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
- mNearbyManager.setPoweredOffFindingMode(
- NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+ mNearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
assertThat(mNearbyManager.getPoweredOffFindingMode())
.isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testSetPoweredOffFindingMode_noPrivilegedPermission() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -264,14 +262,16 @@
enableLocation();
mUiAutomation.dropShellPermissionIdentity();
- assertThrows(SecurityException.class, () -> mNearbyManager
- .setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mNearbyManager.setPoweredOffFindingMode(
+ NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testGetPoweredOffFindingMode_noPrivilegedPermission() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
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/BluetoothFinderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
index 32286e1..a36084b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -18,7 +18,6 @@
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
@@ -29,12 +28,13 @@
import android.hardware.bluetooth.finder.Eid;
import android.hardware.bluetooth.finder.IBluetoothFinder;
import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.Build;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
-import com.android.modules.utils.build.SdkLevel;
+import androidx.test.filters.SdkSuppress;
import org.junit.Before;
import org.junit.Test;
@@ -44,6 +44,7 @@
import java.util.List;
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class BluetoothFinderManagerTest {
private BluetoothFinderManager mBluetoothFinderManager;
private boolean mGetServiceCalled = false;
@@ -71,8 +72,6 @@
@Before
public void setup() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
MockitoAnnotations.initMocks(this);
mBluetoothFinderManager = new BluetoothFinderManagerSpy();
}
@@ -80,16 +79,16 @@
@Test
public void testSendEids() throws Exception {
byte[] eidBytes1 = {
- (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
- (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
- (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
- (byte) 0xe1, (byte) 0xde
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde
};
byte[] eidBytes2 = {
- (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
- (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
- (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
- (byte) 0xf2, (byte) 0xef
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef
};
PoweredOffFindingEphemeralId ephemeralId1 = new PoweredOffFindingEphemeralId();
PoweredOffFindingEphemeralId ephemeralId2 = new PoweredOffFindingEphemeralId();
@@ -105,8 +104,7 @@
@Test
public void testSendEids_remoteException() throws Exception {
- doThrow(new RemoteException())
- .when(mIBluetoothFinderMock).sendEids(any());
+ doThrow(new RemoteException()).when(mIBluetoothFinderMock).sendEids(any());
mBluetoothFinderManager.sendEids(List.of());
// Verify that we get the service again following a RemoteException.
@@ -117,8 +115,7 @@
@Test
public void testSendEids_serviceSpecificException() throws Exception {
- doThrow(new ServiceSpecificException(1))
- .when(mIBluetoothFinderMock).sendEids(any());
+ doThrow(new ServiceSpecificException(1)).when(mIBluetoothFinderMock).sendEids(any());
mBluetoothFinderManager.sendEids(List.of());
}
@@ -134,7 +131,8 @@
@Test
public void testSetPoweredOffFinderMode_remoteException() throws Exception {
doThrow(new RemoteException())
- .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+ .when(mIBluetoothFinderMock)
+ .setPoweredOffFinderMode(anyBoolean());
mBluetoothFinderManager.setPoweredOffFinderMode(true);
// Verify that we get the service again following a RemoteException.
@@ -146,7 +144,8 @@
@Test
public void testSetPoweredOffFinderMode_serviceSpecificException() throws Exception {
doThrow(new ServiceSpecificException(1))
- .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+ .when(mIBluetoothFinderMock)
+ .setPoweredOffFinderMode(anyBoolean());
mBluetoothFinderManager.setPoweredOffFinderMode(true);
}
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/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
index 20ecbce..448ee84 100644
--- a/networksecurity/TEST_MAPPING
+++ b/networksecurity/TEST_MAPPING
@@ -1,5 +1,17 @@
{
- "postsubmit": [
+ "presubmit": [
+ {
+ "name": "CtsNetSecConfigCertificateTransparencyTestCases"
+ },
+ {
+ "name": "CtsNetSecConfigCertificateTransparencyDefaultTestCases"
+ },
+ {
+ "name": "NetSecConfigCertificateTransparencySctLogListTestCases"
+ },
+ {
+ "name": "NetSecConfigCertificateTransparencySctNoLogListTestCases"
+ },
{
"name": "NetworkSecurityUnitTests"
}
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
index 52667ae..d7aacdb 100644
--- a/networksecurity/service/Android.bp
+++ b/networksecurity/service/Android.bp
@@ -24,12 +24,24 @@
srcs: [
"src/**/*.java",
+ ":statslog-certificate-transparency-java-gen",
],
libs: [
"framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"service-connectivity-pre-jarjar",
+ "framework-statsd.stubs.module_lib",
+ ],
+
+ static_libs: [
+ "auto_value_annotations",
+ "android.security.flags-aconfig-java-export",
+ ],
+
+ plugins: [
+ "auto_value_plugin",
+ "auto_annotation_plugin",
],
// This is included in service-connectivity which is 30+
@@ -39,3 +51,10 @@
sdk_version: "system_server_current",
apex_available: ["com.android.tethering"],
}
+
+genrule {
+ name: "statslog-certificate-transparency-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module certificate_transparency --javaPackage com.android.server.net.ct --javaClass CertificateTransparencyStatsLog",
+ out: ["com/android/server/net/ct/CertificateTransparencyStatsLog.java"],
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index 16f32c4..fb42c03 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -13,31 +13,28 @@
* 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.app.DownloadManager;
import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
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.CertificateTransparencyLogger.CTLogListUpdateState;
+import com.android.server.net.ct.DownloadHelper.DownloadStatus;
+
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.ArrayList;
+import java.util.List;
import java.util.Optional;
/** Helper class to download certificate transparency log files. */
@@ -49,74 +46,69 @@
private final Context mContext;
private final DataStore mDataStore;
private final DownloadHelper mDownloadHelper;
- private final CertificateTransparencyInstaller mInstaller;
+ private final SignatureVerifier mSignatureVerifier;
+ private final CertificateTransparencyLogger mLogger;
- @NonNull private Optional<PublicKey> mPublicKey = Optional.empty();
+ private final List<CompatibilityVersion> mCompatVersions = new ArrayList<>();
- @VisibleForTesting
CertificateTransparencyDownloader(
Context context,
DataStore dataStore,
DownloadHelper downloadHelper,
- CertificateTransparencyInstaller installer) {
+ SignatureVerifier signatureVerifier,
+ CertificateTransparencyLogger logger) {
mContext = context;
+ mSignatureVerifier = signatureVerifier;
mDataStore = dataStore;
mDownloadHelper = downloadHelper;
- mInstaller = installer;
+ mLogger = logger;
}
- CertificateTransparencyDownloader(Context context, DataStore dataStore) {
- this(
- context,
- dataStore,
- new DownloadHelper(context),
- new CertificateTransparencyInstaller());
+ void addCompatibilityVersion(CompatibilityVersion compatVersion) {
+ mCompatVersions.add(compatVersion);
}
- void initialize() {
- mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
+ void clearCompatibilityVersions() {
+ mCompatVersions.clear();
+ }
- 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(Config.URL_PUBLIC_KEY);
+ if (downloadId != -1) {
+ mDataStore.setPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, downloadId);
+ mDataStore.store();
}
+ return downloadId;
}
- void setPublicKey(String publicKey) throws GeneralSecurityException {
- mPublicKey =
- Optional.of(
- KeyFactory.getInstance("RSA")
- .generatePublic(
- new X509EncodedKeySpec(
- Base64.getDecoder().decode(publicKey))));
+ private long startMetadataDownload(CompatibilityVersion compatVersion) {
+ long downloadId = download(compatVersion.getMetadataUrl());
+ if (downloadId != -1) {
+ mDataStore.setPropertyLong(compatVersion.getMetadataPropertyName(), downloadId);
+ mDataStore.store();
+ }
+ return downloadId;
}
@VisibleForTesting
- void resetPublicKey() {
- mPublicKey = Optional.empty();
+ void startMetadataDownload() {
+ for (CompatibilityVersion compatVersion : mCompatVersions) {
+ if (startMetadataDownload(compatVersion) == -1) {
+ Log.e(TAG, "Metadata download not started for " + compatVersion.getCompatVersion());
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Metadata download started for " + compatVersion.getCompatVersion());
+ }
+ }
}
- void startMetadataDownload(String metadataUrl) {
- long downloadId = download(metadataUrl);
- if (downloadId == -1) {
- Log.e(TAG, "Metadata download request failed for " + metadataUrl);
- return;
+ @VisibleForTesting
+ long startContentDownload(CompatibilityVersion compatVersion) {
+ long downloadId = download(compatVersion.getContentUrl());
+ if (downloadId != -1) {
+ mDataStore.setPropertyLong(compatVersion.getContentPropertyName(), downloadId);
+ mDataStore.store();
}
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, downloadId);
- mDataStore.store();
- }
-
- void startContentDownload(String contentUrl) {
- long downloadId = download(contentUrl);
- if (downloadId == -1) {
- Log.e(TAG, "Content download request failed for " + contentUrl);
- return;
- }
- mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, downloadId);
- mDataStore.store();
+ return downloadId;
}
@Override
@@ -127,94 +119,119 @@
return;
}
- long completedId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
+ long completedId =
+ intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, /* defaultValue= */ -1);
if (completedId == -1) {
Log.e(TAG, "Invalid completed download Id");
return;
}
- if (isMetadataDownloadId(completedId)) {
- handleMetadataDownloadCompleted(completedId);
+ if (getPublicKeyDownloadId() == completedId) {
+ handlePublicKeyDownloadCompleted(completedId);
return;
}
- if (isContentDownloadId(completedId)) {
- handleContentDownloadCompleted(completedId);
- return;
+ for (CompatibilityVersion compatVersion : mCompatVersions) {
+ if (getMetadataDownloadId(compatVersion) == completedId) {
+ handleMetadataDownloadCompleted(compatVersion, completedId);
+ return;
+ }
+
+ if (getContentDownloadId(compatVersion) == completedId) {
+ handleContentDownloadCompleted(compatVersion, completedId);
+ return;
+ }
}
- Log.e(TAG, "Download id " + completedId + " is neither metadata nor content.");
+ Log.i(TAG, "Download id " + completedId + " is not recognized.");
}
- private void handleMetadataDownloadCompleted(long downloadId) {
- if (!mDownloadHelper.isSuccessful(downloadId)) {
- Log.w(TAG, "Metadata download failed.");
- // TODO: re-attempt download
+ private void handlePublicKeyDownloadCompleted(long downloadId) {
+ DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
+ if (!status.isSuccessful()) {
+ handleDownloadFailed(status);
return;
}
- startContentDownload(mDataStore.getProperty(Config.CONTENT_URL_PENDING));
+ 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;
+ }
+
+ startMetadataDownload();
}
- private void handleContentDownloadCompleted(long downloadId) {
- if (!mDownloadHelper.isSuccessful(downloadId)) {
- Log.w(TAG, "Content download failed.");
- // TODO: re-attempt download
+ private void handleMetadataDownloadCompleted(
+ CompatibilityVersion compatVersion, long downloadId) {
+ DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
+ if (!status.isSuccessful()) {
+ handleDownloadFailed(status);
+ return;
+ }
+ if (startContentDownload(compatVersion) == -1) {
+ Log.e(TAG, "Content download failed for" + compatVersion.getCompatVersion());
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Content download started for" + compatVersion.getCompatVersion());
+ }
+ }
+
+ private void handleContentDownloadCompleted(
+ CompatibilityVersion compatVersion, long downloadId) {
+ DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
+ if (!status.isSuccessful()) {
+ handleDownloadFailed(status);
return;
}
- Uri contentUri = getContentDownloadUri();
- Uri metadataUri = getMetadataDownloadUri();
+ Uri contentUri = getContentDownloadUri(compatVersion);
+ Uri metadataUri = getMetadataDownloadUri(compatVersion);
if (contentUri == null || metadataUri == null) {
Log.e(TAG, "Invalid URIs");
return;
}
- boolean success = false;
- try {
- success = verify(contentUri, metadataUri);
- } catch (IOException | GeneralSecurityException e) {
- Log.e(TAG, "Could not verify new log list", e);
- }
- if (!success) {
+ LogListUpdateStatus updateStatus = mSignatureVerifier.verify(contentUri, metadataUri);
+
+ if (!updateStatus.isSignatureVerified()) {
Log.w(TAG, "Log list did not pass verification");
+
+ mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
+
return;
}
- // TODO: validate file content.
-
- String version = mDataStore.getProperty(Config.VERSION_PENDING);
- String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING);
- String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING);
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
- success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version);
+ updateStatus = compatVersion.install(inputStream, updateStatus.toBuilder());
} 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.setProperty(Config.CONTENT_URL, contentUrl);
- mDataStore.setProperty(Config.METADATA_URL, metadataUrl);
- mDataStore.store();
- }
+ mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
}
- private boolean verify(Uri file, Uri signature) throws IOException, GeneralSecurityException {
- 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();
+ private void handleDownloadFailed(DownloadStatus status) {
+ Log.e(TAG, "Download failed with " + status);
- try (InputStream fileStream = contentResolver.openInputStream(file);
- InputStream signatureStream = contentResolver.openInputStream(signature)) {
- verifier.update(fileStream.readAllBytes());
- return verifier.verify(signatureStream.readAllBytes());
+ LogListUpdateStatus.Builder updateStatusBuilder = LogListUpdateStatus.builder();
+ if (status.isHttpError()) {
+ updateStatusBuilder
+ .setState(CTLogListUpdateState.HTTP_ERROR)
+ .setHttpErrorStatusCode(status.reason());
+ } else {
+ // TODO(b/384935059): handle blocked domain logging
+ updateStatusBuilder.setDownloadStatus(Optional.of(status.reason()));
}
+
+ mLogger.logCTLogListUpdateStateChangedEvent(updateStatusBuilder.build());
}
private long download(String url) {
@@ -227,20 +244,50 @@
}
@VisibleForTesting
- boolean isMetadataDownloadId(long downloadId) {
- return mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1) == downloadId;
+ long getPublicKeyDownloadId() {
+ return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, /* defaultValue= */ -1);
}
@VisibleForTesting
- boolean isContentDownloadId(long downloadId) {
- return mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1) == downloadId;
+ long getMetadataDownloadId(CompatibilityVersion compatVersion) {
+ return mDataStore.getPropertyLong(
+ compatVersion.getMetadataPropertyName(), /* defaultValue */ -1);
}
- private Uri getMetadataDownloadUri() {
- return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1));
+ @VisibleForTesting
+ long getContentDownloadId(CompatibilityVersion compatVersion) {
+ return mDataStore.getPropertyLong(
+ compatVersion.getContentPropertyName(), /* defaultValue= */ -1);
}
- private Uri getContentDownloadUri() {
- return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1));
+ @VisibleForTesting
+ boolean hasPublicKeyDownloadId() {
+ return getPublicKeyDownloadId() != -1;
+ }
+
+ @VisibleForTesting
+ boolean hasMetadataDownloadId() {
+ return mCompatVersions.stream()
+ .map(this::getMetadataDownloadId)
+ .anyMatch(downloadId -> downloadId != -1);
+ }
+
+ @VisibleForTesting
+ boolean hasContentDownloadId() {
+ return mCompatVersions.stream()
+ .map(this::getContentDownloadId)
+ .anyMatch(downloadId -> downloadId != -1);
+ }
+
+ private Uri getPublicKeyDownloadUri() {
+ return mDownloadHelper.getUri(getPublicKeyDownloadId());
+ }
+
+ private Uri getMetadataDownloadUri(CompatibilityVersion compatVersion) {
+ return mDownloadHelper.getUri(getMetadataDownloadId(compatVersion));
+ }
+
+ private Uri getContentDownloadUri(CompatibilityVersion compatVersion) {
+ return mDownloadHelper.getUri(getContentDownloadId(compatVersion));
}
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
deleted file mode 100644
index 0ae982d..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ /dev/null
@@ -1,122 +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.server.net.ct;
-
-import android.annotation.RequiresApi;
-import android.content.Context;
-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 CertificateTransparencyDownloader mCertificateTransparencyDownloader;
-
- CertificateTransparencyFlagsListener(Context context) {
- mDataStore = new DataStore(Config.PREFERENCES_FILE);
- mCertificateTransparencyDownloader =
- new CertificateTransparencyDownloader(context, mDataStore);
- }
-
- 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 {
- mCertificateTransparencyDownloader.setPublicKey(newPublicKey);
- } catch (GeneralSecurityException 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.VERSION_PENDING, newVersion);
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, newContentUrl);
- mDataStore.setProperty(Config.METADATA_URL_PENDING, newMetadataUrl);
- mDataStore.store();
-
- mCertificateTransparencyDownloader.startMetadataDownload(newMetadataUrl);
- }
-}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
deleted file mode 100644
index 4ca97eb..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
+++ /dev/null
@@ -1,92 +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.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..e6f1379
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.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.server.net.ct;
+
+import android.annotation.RequiresApi;
+import android.app.AlarmManager;
+import android.app.DownloadManager;
+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.ConfigUpdate;
+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 final Context mContext;
+ private final DataStore mDataStore;
+ private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+ private final CompatibilityVersion mCompatVersion;
+ private final AlarmManager mAlarmManager;
+ private final PendingIntent mPendingIntent;
+
+ private boolean mScheduled = false;
+ private boolean mDependenciesReady = false;
+
+ /** Creates a new {@link CertificateTransparencyJob} object. */
+ public CertificateTransparencyJob(
+ Context context,
+ DataStore dataStore,
+ CertificateTransparencyDownloader certificateTransparencyDownloader,
+ CompatibilityVersion compatVersion) {
+ mContext = context;
+ mDataStore = dataStore;
+ mCertificateTransparencyDownloader = certificateTransparencyDownloader;
+ mCompatVersion = compatVersion;
+
+ mAlarmManager = context.getSystemService(AlarmManager.class);
+ mPendingIntent =
+ PendingIntent.getBroadcast(
+ mContext,
+ /* requestCode= */ 0,
+ new Intent(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
+ PendingIntent.FLAG_IMMUTABLE);
+ }
+
+ void schedule() {
+ if (!mScheduled) {
+ mContext.registerReceiver(
+ this,
+ new IntentFilter(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
+ Context.RECEIVER_EXPORTED);
+ mAlarmManager.setInexactRepeating(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock
+ .elapsedRealtime(), // schedule first job at earliest convenient time.
+ AlarmManager.INTERVAL_DAY,
+ mPendingIntent);
+ }
+ mScheduled = true;
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob scheduled.");
+ }
+ }
+
+ void cancel() {
+ if (mScheduled) {
+ mContext.unregisterReceiver(this);
+ mAlarmManager.cancel(mPendingIntent);
+ }
+ mScheduled = false;
+
+ if (mDependenciesReady) {
+ stopDependencies();
+ }
+ mDependenciesReady = false;
+
+ mCompatVersion.delete();
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob canceled.");
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!ConfigUpdate.ACTION_UPDATE_CT_LOGS.equals(intent.getAction())) {
+ Log.w(TAG, "Received unexpected broadcast with action " + intent);
+ return;
+ }
+ if (Config.DEBUG) {
+ Log.d(TAG, "Starting CT daily job.");
+ }
+ if (!mDependenciesReady) {
+ startDependencies();
+ mDependenciesReady = true;
+ }
+
+ if (mCertificateTransparencyDownloader.startPublicKeyDownload() == -1) {
+ Log.e(TAG, "Public key download not started.");
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Public key download started successfully.");
+ }
+ }
+
+ private void startDependencies() {
+ mDataStore.load();
+ mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
+ mContext.registerReceiver(
+ mCertificateTransparencyDownloader,
+ new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+ Context.RECEIVER_EXPORTED);
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob dependencies ready.");
+ }
+ }
+
+ private void stopDependencies() {
+ mContext.unregisterReceiver(mCertificateTransparencyDownloader);
+ mCertificateTransparencyDownloader.clearCompatibilityVersions();
+ mDataStore.delete();
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob dependencies stopped.");
+ }
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
new file mode 100644
index 0000000..2a37d8f
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -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 com.android.server.net.ct;
+
+/** Interface with logging to statsd for Certificate Transparency. */
+public interface CertificateTransparencyLogger {
+
+ /**
+ * Logs a CTLogListUpdateStateChanged event to statsd.
+ *
+ * @param updateStatus status object containing details from this update event (e.g. log list
+ * signature, log list timestamp, failure reason if applicable)
+ */
+ void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus);
+
+ /**
+ * Intermediate enum for use with CertificateTransparencyStatsLog.
+ *
+ * This enum primarily exists to avoid 100+ char line alert fatigue.
+ */
+ enum CTLogListUpdateState {
+ UNKNOWN_STATE,
+ HTTP_ERROR,
+ LOG_LIST_INVALID,
+ PUBLIC_KEY_NOT_FOUND,
+ SIGNATURE_INVALID,
+ SIGNATURE_NOT_FOUND,
+ SIGNATURE_VERIFICATION_FAILED,
+ SUCCESS,
+ VERSION_ALREADY_EXISTS
+ }
+}
\ No newline at end of file
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
new file mode 100644
index 0000000..f617523
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -0,0 +1,153 @@
+/*
+ * 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.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_TOO_MANY_REDIRECTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
+
+import android.app.DownloadManager;
+
+/** Implementation for logging to statsd for Certificate Transparency. */
+class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger {
+
+ private final DataStore mDataStore;
+
+ CertificateTransparencyLoggerImpl(DataStore dataStore) {
+ mDataStore = dataStore;
+ }
+
+ @Override
+ public void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus) {
+ if (updateStatus.isSuccessful()) {
+ resetFailureCount();
+ } else {
+ updateFailureCount();
+ }
+
+ int updateState =
+ updateStatus
+ .downloadStatus()
+ .map(s -> downloadStatusToFailureReason(s))
+ .orElseGet(() -> localEnumToStatsLogEnum(updateStatus.state()));
+ int failureCount =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+
+ logCTLogListUpdateStateChangedEvent(
+ updateState,
+ failureCount,
+ updateStatus.httpErrorStatusCode(),
+ updateStatus.signature(),
+ updateStatus.logListTimestamp());
+ }
+
+ private void logCTLogListUpdateStateChangedEvent(
+ int updateState,
+ int failureCount,
+ int httpErrorStatusCode,
+ String signature,
+ long logListTimestamp) {
+ CertificateTransparencyStatsLog.write(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED,
+ updateState,
+ failureCount,
+ httpErrorStatusCode,
+ signature,
+ logListTimestamp);
+ }
+
+ /**
+ * Resets the number of consecutive log list update failures in the data store back to zero.
+ */
+ private void resetFailureCount() {
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
+ mDataStore.store();
+ }
+
+ /**
+ * Updates the data store with the current number of consecutive log list update failures.
+ */
+ private void updateFailureCount() {
+ int failure_count =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+ int new_failure_count = failure_count + 1;
+
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
+ mDataStore.store();
+ }
+
+ /** Converts DownloadStatus reason into failure reason to log. */
+ private int downloadStatusToFailureReason(int downloadStatusReason) {
+ switch (downloadStatusReason) {
+ case DownloadManager.PAUSED_WAITING_TO_RETRY:
+ case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
+ case DownloadManager.ERROR_UNHANDLED_HTTP_CODE:
+ case DownloadManager.ERROR_HTTP_DATA_ERROR:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+ case DownloadManager.ERROR_TOO_MANY_REDIRECTS:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_TOO_MANY_REDIRECTS;
+ case DownloadManager.ERROR_CANNOT_RESUME:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
+ case DownloadManager.ERROR_INSUFFICIENT_SPACE:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
+ case DownloadManager.PAUSED_QUEUED_FOR_WIFI:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
+ default:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+ }
+ }
+
+ /** Converts the local enum to the corresponding auto-generated one used by CTStatsLog. */
+ private int localEnumToStatsLogEnum(CTLogListUpdateState updateState) {
+ switch (updateState) {
+ case HTTP_ERROR:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+ case LOG_LIST_INVALID:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
+ case PUBLIC_KEY_NOT_FOUND:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
+ case SIGNATURE_INVALID:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
+ case SIGNATURE_NOT_FOUND:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
+ case SIGNATURE_VERIFICATION_FAILED:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+ case SUCCESS:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
+ case VERSION_ALREADY_EXISTS:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
+ case UNKNOWN_STATE:
+ default:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
+ }
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index edf7c56..a71ff7c 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -13,35 +13,60 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.server.net.ct;
+import static android.security.Flags.certificateTransparencyConfiguration;
+
+import static com.android.net.ct.flags.Flags.certificateTransparencyService;
+
import android.annotation.RequiresApi;
import android.content.Context;
import android.net.ct.ICertificateTransparencyManager;
import android.os.Build;
import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.util.Log;
-import com.android.net.ct.flags.Flags;
import com.android.server.SystemService;
+import java.util.concurrent.Executors;
+
/** Implementation of the Certificate Transparency service. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub
+ implements DeviceConfig.OnPropertiesChangedListener {
- private final CertificateTransparencyFlagsListener mFlagsListener;
+ private static final String TAG = "CertificateTransparencyService";
+
+ 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();
+ return certificateTransparencyService() && certificateTransparencyConfiguration();
}
/** Creates a new {@link CertificateTransparencyService} object. */
public CertificateTransparencyService(Context context) {
- mFlagsListener = new CertificateTransparencyFlagsListener(context);
+ DataStore dataStore = new DataStore(Config.PREFERENCES_FILE);
+
+ mCertificateTransparencyJob =
+ new CertificateTransparencyJob(
+ context,
+ dataStore,
+ new CertificateTransparencyDownloader(
+ context,
+ dataStore,
+ new DownloadHelper(context),
+ new SignatureVerifier(context),
+ new CertificateTransparencyLoggerImpl(dataStore)),
+ new CompatibilityVersion(
+ Config.COMPATIBILITY_VERSION,
+ Config.URL_SIGNATURE,
+ Config.URL_LOG_LIST,
+ Config.CT_ROOT_DIRECTORY_PATH));
}
/**
@@ -50,12 +75,46 @@
* @see com.android.server.SystemService#onBootPhase
*/
public void onBootPhase(int phase) {
-
switch (phase) {
case SystemService.PHASE_BOOT_COMPLETED:
- mFlagsListener.initialize();
+ DeviceConfig.addOnPropertiesChangedListener(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Executors.newSingleThreadExecutor(),
+ this);
+ onPropertiesChanged(
+ new Properties.Builder(Config.NAMESPACE_NETWORK_SECURITY).build());
break;
default:
}
}
+
+ @Override
+ public void onPropertiesChanged(Properties properties) {
+ if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
+ return;
+ }
+
+ if (DeviceConfig.getBoolean(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Config.FLAG_SERVICE_ENABLED,
+ /* defaultValue= */ true)) {
+ startService();
+ } else {
+ stopService();
+ }
+ }
+
+ private void startService() {
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyService start");
+ }
+ mCertificateTransparencyJob.schedule();
+ }
+
+ private void stopService() {
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyService stop");
+ }
+ mCertificateTransparencyJob.cancel();
+ }
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index 27488b5..e8a6e64 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -15,58 +15,104 @@
*/
package com.android.server.net.ct;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.RequiresApi;
+import android.os.Build;
import android.system.ErrnoException;
import android.system.Os;
+import android.util.Log;
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
/** Represents a compatibility version directory. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class CompatibilityVersion {
+ private static final String TAG = "CompatibilityVersion";
+
static final String LOGS_DIR_PREFIX = "logs-";
static final String LOGS_LIST_FILE_NAME = "log_list.json";
+ static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
- private static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
+ private final String mCompatVersion;
+ private final String mMetadataUrl;
+ private final String mContentUrl;
private final File mRootDirectory;
+ private final File mVersionDirectory;
private final File mCurrentLogsDirSymlink;
- private File mCurrentLogsDir = null;
-
- CompatibilityVersion(File rootDirectory) {
+ CompatibilityVersion(
+ String compatVersion, String metadataUrl, String contentUrl, File rootDirectory) {
+ mCompatVersion = compatVersion;
+ mMetadataUrl = metadataUrl;
+ mContentUrl = contentUrl;
mRootDirectory = rootDirectory;
- mCurrentLogsDirSymlink = new File(mRootDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+ mVersionDirectory = new File(rootDirectory, compatVersion);
+ mCurrentLogsDirSymlink = new File(mVersionDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+ }
+
+ CompatibilityVersion(
+ String compatVersion, String metadataUrl, String contentUrl, String rootDirectoryPath) {
+ this(compatVersion, metadataUrl, contentUrl, new File(rootDirectoryPath));
}
/**
* 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
+ * @param statusBuilder status obj builder containing details of the log list update process
* @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);
+ LogListUpdateStatus install(
+ InputStream newContent, LogListUpdateStatus.Builder statusBuilder) throws IOException {
+ String content = new String(newContent.readAllBytes(), UTF_8);
+ try {
+ JSONObject contentJson = new JSONObject(content);
+ return install(
+ new ByteArrayInputStream(content.getBytes()),
+ contentJson.getString("version"),
+ statusBuilder.setLogListTimestamp(contentJson.getLong("log_list_timestamp")));
+ } catch (JSONException e) {
+ Log.e(TAG, "invalid log list format", e);
- File newLogsDir = new File(mRootDirectory, LOGS_DIR_PREFIX + version);
+ return statusBuilder.setState(CTLogListUpdateState.LOG_LIST_INVALID).build();
+ }
+ }
+
+ LogListUpdateStatus install(
+ InputStream newContent, String version, LogListUpdateStatus.Builder statusBuilder)
+ 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 the path to the root and version directories exist and are readable.
+ DirectoryUtils.makeDir(mRootDirectory);
+ DirectoryUtils.makeDir(mVersionDirectory);
+
+ File newLogsDir = new File(mVersionDirectory, 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 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())) {
+ Log.i(TAG, newLogsDir + " already exists, skipping install.");
deleteOldLogDirectories();
- return false;
+ return statusBuilder.setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS).build();
}
- // 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.
+ // 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 {
@@ -80,8 +126,8 @@
}
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");
+ // 5. Create temp symlink. We rename to the target symlink for an atomic update.
+ File tempSymlink = new File(mVersionDirectory, "new_symlink");
try {
Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
} catch (ErrnoException e) {
@@ -95,17 +141,33 @@
throw e;
}
// 7. Cleanup
- mCurrentLogsDir = newLogsDir;
+ Log.i(TAG, "New logs installed at " + newLogsDir);
deleteOldLogDirectories();
- return true;
+ return statusBuilder.setState(CTLogListUpdateState.SUCCESS).build();
}
- File getRootDir() {
- return mRootDirectory;
+ String getCompatVersion() {
+ return mCompatVersion;
}
- File getLogsDir() {
- return mCurrentLogsDir;
+ String getMetadataUrl() {
+ return mMetadataUrl;
+ }
+
+ String getMetadataPropertyName() {
+ return mCompatVersion + "_" + Config.METADATA_DOWNLOAD_ID;
+ }
+
+ String getContentUrl() {
+ return mContentUrl;
+ }
+
+ String getContentPropertyName() {
+ return mCompatVersion + "_" + Config.CONTENT_DOWNLOAD_ID;
+ }
+
+ File getVersionDir() {
+ return mVersionDirectory;
}
File getLogsDirSymlink() {
@@ -113,19 +175,21 @@
}
File getLogsFile() {
- return new File(mCurrentLogsDir, LOGS_LIST_FILE_NAME);
+ return new File(mCurrentLogsDirSymlink, LOGS_LIST_FILE_NAME);
}
- boolean delete() {
- return DirectoryUtils.removeDir(mRootDirectory);
+ void delete() {
+ if (!DirectoryUtils.removeDir(mVersionDirectory)) {
+ Log.w(TAG, "Could not delete compatibility version directory " + mVersionDirectory);
+ }
}
private void deleteOldLogDirectories() throws IOException {
- if (!mRootDirectory.exists()) {
+ if (!mVersionDirectory.exists()) {
return;
}
File currentTarget = mCurrentLogsDirSymlink.getCanonicalFile();
- for (File file : mRootDirectory.listFiles()) {
+ for (File file : mVersionDirectory.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
index 242f13a..5fdba09 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -47,12 +47,15 @@
static final String FLAG_PUBLIC_KEY = FLAGS_PREFIX + "public_key";
// properties
- static final String VERSION_PENDING = "version_pending";
static final String VERSION = "version";
- static final String CONTENT_URL_PENDING = "content_url_pending";
- static final String CONTENT_URL = "content_url";
- static final String CONTENT_URL_KEY = "content_url_key";
- static final String METADATA_URL_PENDING = "metadata_url_pending";
- static final String METADATA_URL = "metadata_url";
- static final String METADATA_URL_KEY = "metadata_url_key";
+ static final String CONTENT_DOWNLOAD_ID = "content_download_id";
+ static final String METADATA_DOWNLOAD_ID = "metadata_download_id";
+ static final String PUBLIC_KEY_DOWNLOAD_ID = "public_key_download_id";
+ static final String LOG_LIST_UPDATE_FAILURE_COUNT = "log_list_update_failure_count";
+
+ // 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
index cd6aebf..1f99efa 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DataStore.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -44,8 +44,9 @@
}
try (InputStream in = new FileInputStream(mPropertyFile)) {
load(in);
- } catch (IOException e) {
+ } catch (IOException | IllegalArgumentException e) {
Log.e(TAG, "Error loading property store", e);
+ delete();
}
}
@@ -57,6 +58,11 @@
}
}
+ boolean delete() {
+ clear();
+ return mPropertyFile.delete();
+ }
+
long getPropertyLong(String key, long defaultValue) {
return Optional.ofNullable(getProperty(key)).map(Long::parseLong).orElse(defaultValue);
}
@@ -64,4 +70,12 @@
Object setPropertyLong(String key, long value) {
return setProperty(key, Long.toString(value));
}
+
+ int getPropertyInt(String key, int defaultValue) {
+ return Optional.ofNullable(getProperty(key)).map(Integer::parseInt).orElse(defaultValue);
+ }
+
+ Object setPropertyInt(String key, int value) {
+ return setProperty(key, Integer.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
index e3b4124..ba42a82 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.server.net.ct;
import android.annotation.SuppressLint;
@@ -29,16 +30,33 @@
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(true, false)) {
+ 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);
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
index cc8c4c0..5748416 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
@@ -24,6 +24,8 @@
import androidx.annotation.VisibleForTesting;
+import com.google.auto.value.AutoValue;
+
/** Class to handle downloads for Certificate Transparency. */
public class DownloadHelper {
@@ -53,25 +55,22 @@
}
/**
- * Returns true if the specified download completed successfully.
+ * Returns the status of the provided download id.
*
* @param downloadId the download.
- * @return true if the download completed successfully.
+ * @return {@link DownloadStatus} of the download.
*/
- public boolean isSuccessful(long downloadId) {
+ public DownloadStatus getDownloadStatus(long downloadId) {
+ DownloadStatus.Builder builder = DownloadStatus.builder().setDownloadId(downloadId);
try (Cursor cursor = mDownloadManager.query(new Query().setFilterById(downloadId))) {
- if (cursor == null) {
- return false;
- }
- if (cursor.moveToFirst()) {
- int status =
- cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
- if (DownloadManager.STATUS_SUCCESSFUL == status) {
- return true;
- }
+ if (cursor != null && cursor.moveToFirst()) {
+ builder.setStatus(
+ cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)));
+ builder.setReason(
+ cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON)));
}
}
- return false;
+ return builder.build();
}
/**
@@ -87,4 +86,58 @@
}
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/LogListUpdateStatus.java b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
new file mode 100644
index 0000000..3f9b762
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.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.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
+
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Optional;
+
+/** Class to represent the signature verification status for Certificate Transparency. */
+@AutoValue
+public abstract class LogListUpdateStatus {
+
+ abstract CTLogListUpdateState state();
+
+ abstract String signature();
+
+ abstract long logListTimestamp();
+
+ abstract int httpErrorStatusCode();
+
+ abstract Optional<Integer> downloadStatus();
+
+ boolean isSignatureVerified() {
+ // Check that none of the signature verification failures have been set as the state
+ return state() != PUBLIC_KEY_NOT_FOUND
+ && state() != SIGNATURE_INVALID
+ && state() != SIGNATURE_NOT_FOUND
+ && state() != SIGNATURE_VERIFICATION_FAILED;
+ }
+
+ boolean hasSignature() {
+ return signature() != null && signature().length() > 0;
+ }
+
+ boolean isSuccessful() {
+ return state() == SUCCESS;
+ }
+
+ static LogListUpdateStatus getDefaultInstance() {
+ return builder().build();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setState(CTLogListUpdateState updateState);
+
+ abstract Builder setSignature(String signature);
+
+ abstract Builder setLogListTimestamp(long timestamp);
+
+ abstract Builder setHttpErrorStatusCode(int httpStatusCode);
+
+ abstract Builder setDownloadStatus(Optional<Integer> downloadStatus);
+
+ abstract LogListUpdateStatus build();
+ }
+
+ abstract LogListUpdateStatus.Builder toBuilder();
+
+ static Builder builder() {
+ return new AutoValue_LogListUpdateStatus.Builder()
+ .setState(CTLogListUpdateState.UNKNOWN_STATE)
+ .setSignature("")
+ .setLogListTimestamp(0L)
+ .setHttpErrorStatusCode(0)
+ .setDownloadStatus(Optional.empty());
+ }
+}
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..6040ef6
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -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.server.net.ct;
+
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+
+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 android.util.Log;
+
+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;
+ private static final String TAG = "SignatureVerifier";
+
+ @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 {
+ byte[] decodedPublicKey = null;
+ try {
+ decodedPublicKey = Base64.getDecoder().decode(publicKey);
+ } catch (IllegalArgumentException e) {
+ throw new GeneralSecurityException("Invalid public key base64 encoding", e);
+ }
+ setPublicKey(
+ KeyFactory.getInstance("RSA")
+ .generatePublic(new X509EncodedKeySpec(decodedPublicKey)));
+ }
+
+ @VisibleForTesting
+ void setPublicKey(PublicKey publicKey) {
+ mPublicKey = Optional.of(publicKey);
+ }
+
+ LogListUpdateStatus verify(Uri file, Uri signature) {
+ LogListUpdateStatus.Builder statusBuilder = LogListUpdateStatus.builder();
+
+ if (!mPublicKey.isPresent()) {
+ statusBuilder.setState(PUBLIC_KEY_NOT_FOUND);
+ Log.e(TAG, "No public key found for log list verification");
+ return statusBuilder.build();
+ }
+
+ ContentResolver contentResolver = mContext.getContentResolver();
+
+ try (InputStream fileStream = contentResolver.openInputStream(file);
+ InputStream signatureStream = contentResolver.openInputStream(signature)) {
+ Signature verifier = Signature.getInstance("SHA256withRSA");
+ verifier.initVerify(mPublicKey.get());
+ verifier.update(fileStream.readAllBytes());
+
+ byte[] signatureBytes = signatureStream.readAllBytes();
+ statusBuilder.setSignature(new String(signatureBytes));
+ try {
+ byte[] decodedSigBytes = Base64.getDecoder().decode(signatureBytes);
+
+ if (!verifier.verify(decodedSigBytes)) {
+ // Leave the UpdateState as UNKNOWN_STATE if successful as there are other
+ // potential failures past the signature verification step
+ statusBuilder.setState(SIGNATURE_VERIFICATION_FAILED);
+ }
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Invalid signature base64 encoding", e);
+ statusBuilder.setState(SIGNATURE_INVALID);
+ return statusBuilder.build();
+ }
+ } catch (InvalidKeyException e) {
+ Log.e(TAG, "Signature invalid for log list verification", e);
+ statusBuilder.setState(SIGNATURE_INVALID);
+ return statusBuilder.build();
+ } catch (IOException | GeneralSecurityException e) {
+ Log.e(TAG, "Could not verify new log list", e);
+ statusBuilder.setState(SIGNATURE_VERIFICATION_FAILED);
+ return statusBuilder.build();
+ }
+
+ // Double check if the signature is empty that we set the state correctly
+ if (!statusBuilder.build().hasSignature()) {
+ statusBuilder.setState(SIGNATURE_NOT_FOUND);
+ }
+
+ return statusBuilder.build();
+ }
+}
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
index df02446..2af0122 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -13,29 +13,38 @@
* 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.io.Files.toByteArray;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
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 com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
+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.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -52,215 +61,557 @@
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
+import java.util.Optional;
/** Tests for the {@link CertificateTransparencyDownloader}. */
@RunWith(JUnit4.class)
public class CertificateTransparencyDownloaderTest {
- @Mock private DownloadHelper mDownloadHelper;
- @Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
+ @Mock private DownloadManager mDownloadManager;
+ @Mock private CertificateTransparencyLogger mLogger;
+ private ArgumentCaptor<LogListUpdateStatus> mUpdateStatusCaptor =
+ ArgumentCaptor.forClass(LogListUpdateStatus.class);
private PrivateKey mPrivateKey;
private PublicKey mPublicKey;
private Context mContext;
- private File mTempFile;
private DataStore mDataStore;
+ private SignatureVerifier mSignatureVerifier;
+ private CompatibilityVersion mCompatVersion;
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+ private long mNextDownloadId = 666;
+ private static final long LOG_LIST_TIMESTAMP = 123456789L;
+
@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);
- mDataStore.load();
-
+ mDataStore = new DataStore(File.createTempFile("datastore-test", ".properties"));
+ mSignatureVerifier = new SignatureVerifier(mContext);
mCertificateTransparencyDownloader =
new CertificateTransparencyDownloader(
- mContext, mDataStore, mDownloadHelper, mCertificateTransparencyInstaller);
+ mContext,
+ mDataStore,
+ new DownloadHelper(mDownloadManager),
+ mSignatureVerifier,
+ mLogger);
+ mCompatVersion =
+ new CompatibilityVersion(
+ /* compatVersion= */ "v666",
+ Config.URL_SIGNATURE,
+ Config.URL_LOG_LIST,
+ mContext.getFilesDir());
+
+ prepareDownloadManager();
+ mDataStore.load();
+ mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
}
@After
public void tearDown() {
- mTempFile.delete();
- mCertificateTransparencyDownloader.resetPublicKey();
+ mSignatureVerifier.resetPublicKey();
+ mCompatVersion.delete();
+ mDataStore.delete();
+ }
+
+ @Test
+ public void testDownloader_startPublicKeyDownload() {
+ assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isFalse();
+
+ long downloadId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+ assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isTrue();
+ assertThat(mCertificateTransparencyDownloader.getPublicKeyDownloadId())
+ .isEqualTo(downloadId);
}
@Test
public void testDownloader_startMetadataDownload() {
- String metadataUrl = "http://test-metadata.org";
- long downloadId = 666;
- when(mDownloadHelper.startDownload(metadataUrl)).thenReturn(downloadId);
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
- assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isFalse();
- mCertificateTransparencyDownloader.startMetadataDownload(metadataUrl);
- assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
+ mCertificateTransparencyDownloader.startMetadataDownload();
+
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
}
@Test
public void testDownloader_startContentDownload() {
- String contentUrl = "http://test-content.org";
- long downloadId = 666;
- when(mDownloadHelper.startDownload(contentUrl)).thenReturn(downloadId);
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
- assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isFalse();
- mCertificateTransparencyDownloader.startContentDownload(contentUrl);
- assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
}
@Test
- public void testDownloader_handleMetadataCompleteSuccessful() {
- long metadataId = 123;
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
- when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(true);
-
- long contentId = 666;
- String contentUrl = "http://test-content.org";
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUrl);
- when(mDownloadHelper.startDownload(contentUrl)).thenReturn(contentId);
-
- mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(metadataId));
-
- assertThat(mCertificateTransparencyDownloader.isContentDownloadId(contentId)).isTrue();
- }
-
- @Test
- public void testDownloader_handleMetadataCompleteFailed() {
- long metadataId = 123;
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
- when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(false);
-
- String contentUrl = "http://test-content.org";
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUrl);
-
- mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(metadataId));
-
- verify(mDownloadHelper, never()).startDownload(contentUrl);
- }
-
- @Test
- public void testDownloader_handleContentCompleteInstallSuccessful() throws Exception {
- String version = "666";
- long contentId = 666;
- File logListFile = File.createTempFile("log_list", "json");
- Uri contentUri = Uri.fromFile(logListFile);
- long metadataId = 123;
- File metadataFile = sign(logListFile);
- Uri metadataUri = Uri.fromFile(metadataFile);
- mCertificateTransparencyDownloader.setPublicKey(
- Base64.getEncoder().encodeToString(mPublicKey.getEncoded()));
-
- setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), eq(version)))
- .thenReturn(true);
-
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
-
- mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
-
- verify(mCertificateTransparencyInstaller, times(1))
- .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
- assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isEqualTo(contentUri.toString());
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isEqualTo(metadataUri.toString());
- }
-
- @Test
- public void testDownloader_handleContentCompleteInstallFails() throws Exception {
- String version = "666";
- long contentId = 666;
- File logListFile = File.createTempFile("log_list", "json");
- Uri contentUri = Uri.fromFile(logListFile);
- long metadataId = 123;
- File metadataFile = sign(logListFile);
- Uri metadataUri = Uri.fromFile(metadataFile);
-
- setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
- when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), eq(version)))
- .thenReturn(false);
-
- mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
-
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
- }
-
- @Test
- public void testDownloader_handleContentCompleteVerificationFails() throws IOException {
- String version = "666";
- long contentId = 666;
- Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
- long metadataId = 123;
- Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-wrong_metadata", "sig"));
-
- setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
-
- mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
-
- verify(mCertificateTransparencyInstaller, never())
- .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
- }
-
- @Test
- public void testDownloader_handleContentCompleteMissingVerificationPublicKey()
+ public void testDownloader_publicKeyDownloadSuccess_updatePublicKey_startMetadataDownload()
throws Exception {
- String version = "666";
- long contentId = 666;
- File logListFile = File.createTempFile("log_list", "json");
- Uri contentUri = Uri.fromFile(logListFile);
- long metadataId = 123;
- File metadataFile = sign(logListFile);
- Uri metadataUri = Uri.fromFile(metadataFile);
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
- setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
-
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makePublicKeyDownloadCompleteIntent(writePublicKeyToFile(mPublicKey)));
- verify(mCertificateTransparencyInstaller, never())
- .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+ assertThat(mSignatureVerifier.getPublicKey()).hasValue(mPublicKey);
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
}
- private Intent makeDownloadCompleteIntent(long downloadId) {
+ @Test
+ public void
+ testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
+ throws Exception {
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makePublicKeyDownloadCompleteIntent(
+ writeToFile("i_am_not_a_base64_encoded_public_key".getBytes())));
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ }
+
+ @Test
+ public void testDownloader_publicKeyDownloadFail_doNotUpdatePublicKey() throws Exception {
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_HTTP_DATA_ERROR));
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ }
+
+ @Test
+ public void testDownloader_publicKeyDownloadFail_logsFailure()
+ throws Exception {
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(
+ LogListUpdateStatus.builder()
+ .setDownloadStatus(
+ Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+ .build());
+ }
+
+ @Test
+ public void testDownloader_metadataDownloadSuccess_startContentDownload() {
+ mCertificateTransparencyDownloader.startMetadataDownload();
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadCompleteIntent(mCompatVersion, new File("log_list.sig")));
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
+ }
+
+ @Test
+ public void testDownloader_metadataDownloadFail_doNotStartContentDownload() {
+ mCertificateTransparencyDownloader.startMetadataDownload();
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
+ }
+
+ @Test
+ public void testDownloader_metadataDownloadFail_logsFailure()
+ throws Exception {
+ mCertificateTransparencyDownloader.startMetadataDownload();
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(
+ LogListUpdateStatus.builder()
+ .setDownloadStatus(
+ Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+ .build());
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_installSuccess() throws Exception {
+ String newVersion = "456";
+ File logListFile = makeLogListFile(newVersion);
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ mCertificateTransparencyDownloader.startMetadataDownload();
+
+ assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ assertInstallSuccessful(newVersion);
+ }
+
+ @Test
+ public void testDownloader_contentDownloadFail_doNotInstall() throws Exception {
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
+
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void testDownloader_contentDownloadFail_logsFailure()
+ throws Exception {
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(
+ LogListUpdateStatus.builder()
+ .setDownloadStatus(
+ Optional.of(DownloadManager.ERROR_INSUFFICIENT_SPACE))
+ .build());
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_invalidLogList_installFails()
+ throws Exception {
+ File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+ File metadataFile = sign(invalidLogListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ mCertificateTransparencyDownloader.startMetadataDownload();
+
+ assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
+
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_noPublicKeyFound_logsSingleFailure()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ mCertificateTransparencyDownloader.startMetadataDownload();
+
+ // Set the public key to be missing
+ mSignatureVerifier.resetPublicKey();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ assertThat(mUpdateStatusCaptor.getValue().state())
+ .isEqualTo(CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND);
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_wrongSignatureAlgo_logsSingleFailure()
+ throws Exception {
+ // Arrange
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+
+ // Set the key to be deliberately wrong by using diff algorithm
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("EC");
+ mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+ // Act
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ // Assert
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ assertThat(mUpdateStatusCaptor.getValue().state())
+ .isEqualTo(CTLogListUpdateState.SIGNATURE_INVALID);
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_signatureNotVerified_logsSingleFailure()
+ throws Exception {
+ // Arrange
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+
+ // Set the key to be deliberately wrong by using diff key pair
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+ // Act
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ // Assert
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+ assertThat(statusValue.state())
+ .isEqualTo(CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED);
+ assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_installFail_logsFailure()
+ throws Exception {
+ File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+ File metadataFile = sign(invalidLogListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
+
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+ LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+ assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.LOG_LIST_INVALID);
+ assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
+ }
+
+ @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);
+
+ assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_missingVerificationPublicKey_doNotInstall()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.resetPublicKey();
+
+ assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void testDownloader_endToEndSuccess_installNewVersion_andLogsSuccess() throws Exception {
+ // Arrange
+ String newVersion = "456";
+ File logListFile = makeLogListFile(newVersion);
+ File metadataFile = sign(logListFile);
+ File publicKeyFile = writePublicKeyToFile(mPublicKey);
+
+ assertNoVersionIsInstalled();
+
+ // Act
+ // 1. Start download of public key.
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+ // 2. On successful public key download, set the key and start the metatadata
+ // download.
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makePublicKeyDownloadCompleteIntent(publicKeyFile));
+
+ // 3. On successful metadata download, start the content download.
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+
+ // 4. On successful content download, verify the signature and install the new
+ // version.
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ // Assert
+ assertInstallSuccessful(newVersion);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+
+ LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+ assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.SUCCESS);
+ assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
+ assertThat(statusValue.logListTimestamp()).isEqualTo(LOG_LIST_TIMESTAMP);
+ }
+
+ private void assertNoVersionIsInstalled() {
+ assertThat(mCompatVersion.getVersionDir().exists()).isFalse();
+ }
+
+ private void assertInstallSuccessful(String version) {
+ File logsDir =
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + version);
+ assertThat(logsDir.exists()).isTrue();
+ File logsFile = new File(logsDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+ assertThat(logsFile.exists()).isTrue();
+ }
+
+ private void prepareDownloadManager() {
+ when(mDownloadManager.enqueue(any(Request.class)))
+ .thenAnswer(invocation -> mNextDownloadId++);
+ }
+
+ private Intent makePublicKeyDownloadCompleteIntent(File publicKeyfile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getPublicKeyDownloadId(), publicKeyfile);
+ }
+
+ private Intent makeMetadataDownloadCompleteIntent(
+ CompatibilityVersion compatVersion, File signatureFile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getMetadataDownloadId(compatVersion),
+ signatureFile);
+ }
+
+ private Intent makeContentDownloadCompleteIntent(
+ CompatibilityVersion compatVersion, File logListFile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getContentDownloadId(compatVersion),
+ logListFile);
+ }
+
+ private Intent makeDownloadCompleteIntent(long downloadId, File file) {
+ when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
}
- private void setUpDownloadComplete(
- String version, long metadataId, Uri metadataUri, long contentId, Uri contentUri)
- throws IOException {
- mDataStore.setProperty(Config.VERSION_PENDING, version);
+ 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;
+ }
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
- mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
- when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
+ private Intent makePublicKeyDownloadFailedIntent(int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getPublicKeyDownloadId(), error);
+ }
- mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
- when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
- when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+ private Intent makeMetadataDownloadFailedIntent(CompatibilityVersion compatVersion, int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getMetadataDownloadId(compatVersion), error);
+ }
+
+ private Intent makeContentDownloadFailedIntent(CompatibilityVersion compatVersion, int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getContentDownloadId(compatVersion), error);
+ }
+
+ private Intent makeDownloadFailedIntent(long downloadId, int error) {
+ when(mDownloadManager.query(any(Query.class))).thenReturn(makeFailedDownloadCursor(error));
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
+ }
+
+ 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 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)) {
+ JSONObject contentJson =
+ new JSONObject()
+ .put("version", version)
+ .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
+ outputStream.write(contentJson.toString().getBytes());
+ }
+
+ return logListFile;
}
private File sign(File file) throws IOException, GeneralSecurityException {
@@ -271,7 +622,7 @@
try (InputStream fileStream = new FileInputStream(file);
OutputStream outputStream = new FileOutputStream(signatureFile)) {
signer.update(fileStream.readAllBytes());
- outputStream.write(signer.sign());
+ outputStream.write(Base64.getEncoder().encode(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
deleted file mode 100644
index 50d3f23..0000000
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
+++ /dev/null
@@ -1,183 +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.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/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
new file mode 100644
index 0000000..2b8b3cd
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.LOG_LIST_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.UNKNOWN_STATE;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.VERSION_ALREADY_EXISTS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+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.IOException;
+import java.io.InputStream;
+
+/** Tests for the {@link CompatibilityVersion}. */
+@RunWith(JUnit4.class)
+public class CompatibilityVersionTest {
+
+ private static final String TEST_VERSION = "v123";
+ private static final long LOG_LIST_TIMESTAMP = 123456789L;
+ private static final String SIGNATURE = "fake_signature";
+
+ private final File mTestDir =
+ InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
+ private final CompatibilityVersion mCompatVersion =
+ new CompatibilityVersion(
+ TEST_VERSION, Config.URL_SIGNATURE, Config.URL_LOG_LIST, mTestDir);
+
+ @After
+ public void tearDown() {
+ mCompatVersion.delete();
+ }
+
+ @Test
+ public void testCompatibilityVersion_versionDirectory_setupSuccessful() {
+ File versionDir = mCompatVersion.getVersionDir();
+
+ assertThat(versionDir.exists()).isFalse();
+ assertThat(versionDir.getAbsolutePath()).startsWith(mTestDir.getAbsolutePath());
+ assertThat(versionDir.getAbsolutePath()).endsWith(TEST_VERSION);
+ }
+
+ @Test
+ public void testCompatibilityVersion_symlink_setupSuccessful() {
+ File dirSymlink = mCompatVersion.getLogsDirSymlink();
+
+ assertThat(dirSymlink.exists()).isFalse();
+ assertThat(dirSymlink.getAbsolutePath())
+ .startsWith(mCompatVersion.getVersionDir().getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_logsFile_setupSuccessful() {
+ File logsFile = mCompatVersion.getLogsFile();
+
+ assertThat(logsFile.exists()).isFalse();
+ assertThat(logsFile.getAbsolutePath())
+ .startsWith(mCompatVersion.getLogsDirSymlink().getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_installSuccessful_keepsStatusDetails() throws Exception {
+ String version = "i_am_version";
+ JSONObject logList = makeLogList(version, "i_am_content");
+
+ try (InputStream inputStream = asStream(logList)) {
+ assertThat(
+ mCompatVersion.install(
+ inputStream,
+ LogListUpdateStatus.builder()
+ .setSignature(SIGNATURE)
+ .setState(UNKNOWN_STATE)))
+ .isEqualTo(
+ LogListUpdateStatus.builder()
+ .setSignature(SIGNATURE)
+ .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+ // Ensure the state is correctly overridden to SUCCESS
+ .setState(SUCCESS)
+ .build());
+ }
+ }
+
+ @Test
+ public void testCompatibilityVersion_installSuccessful() throws Exception {
+ String version = "i_am_version";
+ JSONObject logList = makeLogList(version, "i_am_content");
+
+ try (InputStream inputStream = asStream(logList)) {
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
+ }
+
+ File logListFile = mCompatVersion.getLogsFile();
+ assertThat(logListFile.exists()).isTrue();
+ assertThat(logListFile.getCanonicalPath())
+ .isEqualTo(
+ // <path-to-test-files>/v123/logs-i_am_version/log_list.json
+ new File(
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + version),
+ CompatibilityVersion.LOGS_LIST_FILE_NAME)
+ .getCanonicalPath());
+ assertThat(logListFile.getAbsolutePath())
+ .isEqualTo(
+ // <path-to-test-files>/v123/current/log_list.json
+ new File(
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.CURRENT_LOGS_DIR_SYMLINK_NAME),
+ CompatibilityVersion.LOGS_LIST_FILE_NAME)
+ .getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
+ try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
+ }
+
+ mCompatVersion.delete();
+
+ assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
+ }
+
+ @Test
+ public void testCompatibilityVersion_invalidLogList() throws Exception {
+ try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(LogListUpdateStatus.builder().setState(LOG_LIST_INVALID).build());
+ }
+
+ assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
+ }
+
+ @Test
+ public void testCompatibilityVersion_incompleteVersionExists_replacesOldVersion()
+ throws Exception {
+ String existingVersion = "666";
+ File existingLogDir =
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
+ assertThat(existingLogDir.mkdirs()).isTrue();
+ File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+ assertThat(logsListFile.createNewFile()).isTrue();
+
+ JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
+ try (InputStream inputStream = asStream(newLogList)) {
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
+ }
+
+ assertThat(readAsString(logsListFile)).isEqualTo(newLogList.toString());
+ }
+
+ @Test
+ public void testCompatibilityVersion_versionAlreadyExists_installFails() throws Exception {
+ String existingVersion = "666";
+ JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
+ try (InputStream inputStream = asStream(existingLogList)) {
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
+ }
+
+ try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(
+ LogListUpdateStatus.builder()
+ .setState(VERSION_ALREADY_EXISTS)
+ .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+ .build());
+ }
+
+ assertThat(readAsString(mCompatVersion.getLogsFile()))
+ .isEqualTo(existingLogList.toString());
+ }
+
+ private static InputStream asStream(JSONObject logList) throws IOException {
+ return new ByteArrayInputStream(logList.toString().getBytes());
+ }
+
+ private static JSONObject makeLogList(String version) throws JSONException {
+ return new JSONObject()
+ .put("version", version)
+ .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
+ }
+
+ private static JSONObject makeLogList(String version, String content) throws JSONException {
+ return makeLogList(version).put("content", content);
+ }
+
+ private static LogListUpdateStatus getSuccessfulUpdateStatus() {
+ return LogListUpdateStatus.builder()
+ .setState(SUCCESS)
+ .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+ .build();
+ }
+
+ private static String readAsString(File file) throws IOException {
+ try (InputStream in = new FileInputStream(file)) {
+ return new String(in.readAllBytes());
+ }
+ }
+}
diff --git a/remoteauth/OWNERS b/remoteauth/OWNERS
index 25a32b9..ee46c1c 100644
--- a/remoteauth/OWNERS
+++ b/remoteauth/OWNERS
@@ -2,7 +2,6 @@
# Bug template url: http://b/new?component=1145231&template=1715387
billyhuang@google.com
boetger@google.com
-casbor@google.com
derekjedral@google.com
dlm@google.com
igorzas@google.com
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index 57e3ec9..c7ad738 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -24,9 +24,6 @@
"libasync_trait",
],
prefer_rlib: true,
- apex_available: [
- "com.android.remoteauth",
- ],
host_supported: true,
}
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
index 9add6df..1d43d38 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -140,6 +140,7 @@
}
impl Platform for JavaPlatform {
+ #[allow(clippy::unit_arg)]
fn send_request(
&mut self,
connection_id: i32,
diff --git a/remoteauth/service/jni/src/unique_jvm.rs b/remoteauth/service/jni/src/unique_jvm.rs
index 46cc361..ddbb16f 100644
--- a/remoteauth/service/jni/src/unique_jvm.rs
+++ b/remoteauth/service/jni/src/unique_jvm.rs
@@ -41,6 +41,7 @@
Ok(())
}
/// Gets a 'static reference to the unique JavaVM. Returns None if set_once() was never called.
+#[allow(static_mut_refs)]
pub(crate) fn get_static_ref() -> Option<&'static Arc<JavaVM>> {
// Safety: follows [this pattern](https://doc.rust-lang.org/std/sync/struct.Once.html).
// Modification to static mut is nested inside call_once.
diff --git a/service-b/Android.bp b/service-b/Android.bp
new file mode 100644
index 0000000..47439ee
--- /dev/null
+++ b/service-b/Android.bp
@@ -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 {
+ default_team: "trendy_team_enigma",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// TODO: b/380331248 This lib is a non-jarjared version of "service-connectivity-b-platform"
+// It will only be included in the Tethering module when the build system flag
+// RELEASE_MOVE_VCN_TO_MAINLINE is enabled. Including "service-connectivity-b-platform"
+// in Tethering will break art branch check because that lib lives in framework/base.
+// Once VCN is moved to Connectivity/, "service-connectivity-b-platform" can be cleaned up.
+java_library {
+ name: "service-connectivity-b-pre-jarjar",
+ defaults: ["service-connectivity-b-pre-jarjar-defaults"],
+ libs: ["service-connectivity-pre-jarjar"],
+
+ sdk_version: "system_server_current",
+
+ // TODO(b/210962470): Bump this to B
+ min_sdk_version: "30",
+
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 787e94e..d2e2a80 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -101,7 +101,7 @@
min_sdk_version: "21",
lint: {
error_checks: ["NewApi"],
-
+ baseline_filename: "lint-baseline-service-connectivity-mdns-standalone-build-test.xml",
},
srcs: [
"src/com/android/server/connectivity/mdns/**/*.java",
diff --git a/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml b/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml
new file mode 100644
index 0000000..232d31c
--- /dev/null
+++ b/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml
@@ -0,0 +1,972 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+</issues>
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 0adb290..555549c 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -23,6 +23,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.nsd.AdvertisingRequest.FLAG_SKIP_PROBING;
import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
@@ -981,7 +982,7 @@
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
}
- boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig()
+ boolean isUpdateOnly = (advertisingRequest.getFlags()
& AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0;
// If it is an update request, then reuse the old transactionId
if (isUpdateOnly) {
@@ -1046,9 +1047,12 @@
serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
+ final boolean skipProbing = (advertisingRequest.getFlags()
+ & FLAG_SKIP_PROBING) > 0;
final MdnsAdvertisingOptions mdnsAdvertisingOptions =
MdnsAdvertisingOptions.newBuilder()
.setIsOnlyUpdate(isUpdateOnly)
+ .setSkipProbing(skipProbing)
.setTtl(advertisingRequest.getTtl())
.build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
@@ -1943,6 +1947,8 @@
.setCachedServicesRetentionTime(mDeps.getDeviceConfigPropertyInt(
MdnsFeatureFlags.NSD_CACHED_SERVICES_RETENTION_TIME,
MdnsFeatureFlags.DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS))
+ .setIsShortHostnamesEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+ mContext, MdnsFeatureFlags.NSD_USE_SHORT_HOSTNAMES))
.setOverrideProvider(new MdnsFeatureFlags.FlagOverrideProvider() {
@Override
public boolean isForceEnabledForTest(@NonNull String flag) {
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 f55db93..81ba530 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -139,11 +139,8 @@
// Base service type
questions.add(new MdnsPointerRecord(serviceTypeLabels, expectUnicastResponse));
for (String subtype : subtypes) {
- final String[] labels = new String[serviceTypeLabels.length + 2];
- labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
- labels[1] = MdnsConstants.SUBTYPE_LABEL;
- System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
-
+ final String[] labels = MdnsUtils.constructFullSubtype(serviceTypeLabels,
+ MdnsConstants.SUBTYPE_PREFIX + subtype);
questions.add(new MdnsPointerRecord(labels, expectUnicastResponse));
}
}
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 9c52eca..c3306bd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -46,6 +46,7 @@
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.connectivity.mdns.util.MdnsUtils;
+import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -117,7 +118,7 @@
* Generates a unique hostname to be used by the device.
*/
@NonNull
- public String[] generateHostname() {
+ public String[] generateHostname(boolean useShortFormat) {
// Generate a very-probably-unique hostname. This allows minimizing possible conflicts
// to the point that probing for it is no longer necessary (as per RFC6762 8.1 last
// paragraph), and does not leak more information than what could already be obtained by
@@ -127,10 +128,24 @@
// Having a different hostname per interface is an acceptable option as per RFC6762 14.
// This hostname will change every time the interface is reconnected, so this does not
// allow tracking the device.
- // TODO: consider deriving a hostname from other sources, such as the IPv6 addresses
- // (reusing the same privacy-protecting mechanics).
- return new String[] {
- "Android_" + UUID.randomUUID().toString().replace("-", ""), LOCAL_TLD };
+ if (useShortFormat) {
+ // A short hostname helps reduce the size of APF mDNS filtering programs, and
+ // is necessary for compatibility with some Matter 1.0 devices which assumed
+ // 16 characters is the maximum length.
+ // Generate a hostname matching Android_[0-9A-Z]{8}, which has 36^8 possibilities.
+ // Even with 100 devices advertising the probability of collision is around 2E-9,
+ // which is negligible.
+ final SecureRandom sr = new SecureRandom();
+ final String allowedChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ final StringBuilder sb = new StringBuilder(8);
+ for (int i = 0; i < 8; i++) {
+ sb.append(allowedChars.charAt(sr.nextInt(allowedChars.length())));
+ }
+ return new String[]{ "Android_" + sb.toString(), LOCAL_TLD };
+ } else {
+ return new String[]{
+ "Android_" + UUID.randomUUID().toString().replace("-", ""), LOCAL_TLD};
+ }
}
}
@@ -825,7 +840,7 @@
mCb = cb;
mSocketProvider = socketProvider;
mDeps = deps;
- mDeviceHostName = deps.generateHostname();
+ mDeviceHostName = deps.generateHostname(mDnsFeatureFlags.isShortHostnamesEnabled());
mSharedLog = sharedLog;
mMdnsFeatureFlags = mDnsFeatureFlags;
final ConnectivityResources res = new ConnectivityResources(context);
@@ -943,7 +958,7 @@
mRegistrations.remove(id);
// Regenerates host name when registrations removed.
if (mRegistrations.size() == 0) {
- mDeviceHostName = mDeps.generateHostname();
+ mDeviceHostName = mDeps.generateHostname(mMdnsFeatureFlags.isShortHostnamesEnabled());
}
}
@@ -992,6 +1007,19 @@
});
}
+ private List<String> getOffloadSubtype(@NonNull NsdServiceInfo nsdServiceInfo) {
+ // Workaround: Google Cast doesn't announce subtypes per DNS-SD/mDNS spec.
+ // Thus, subtypes aren't offloaded; only "_googlecast._tcp" is.
+ // Subtype responses occur when hardware offload is off.
+ // This solution works because Google Cast doesn't follow the intended usage of subtypes in
+ // the spec, as it always discovers for both the subtype+base type, and only uses the mDNS
+ // subtype as an optimization.
+ if (nsdServiceInfo.getServiceType().equals("_googlecast._tcp")) {
+ return new ArrayList<>();
+ }
+ return new ArrayList<>(nsdServiceInfo.getSubtypes());
+ }
+
private OffloadServiceInfoWrapper createOffloadService(int serviceId,
@NonNull Registration registration, byte[] rawOffloadPacket) {
final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
@@ -1002,7 +1030,7 @@
final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
nsdServiceInfo.getServiceType()),
- new ArrayList<>(nsdServiceInfo.getSubtypes()),
+ getOffloadSubtype(nsdServiceInfo),
String.join(".", mDeviceHostName),
rawOffloadPacket,
priority,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
index a81d1e4..5133d4f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -34,13 +34,15 @@
private final boolean mIsOnlyUpdate;
@Nullable
private final Duration mTtl;
+ private final boolean mSkipProbing;
/**
* Parcelable constructs for a {@link MdnsAdvertisingOptions}.
*/
- MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) {
+ MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl, boolean skipProbing) {
this.mIsOnlyUpdate = isOnlyUpdate;
this.mTtl = ttl;
+ this.mSkipProbing = skipProbing;
}
/**
@@ -68,6 +70,13 @@
}
/**
+ * @return {@code true} if the probing step should be skipped.
+ */
+ public boolean skipProbing() {
+ return mSkipProbing;
+ }
+
+ /**
* Returns the TTL for all records in a service.
*/
@Nullable
@@ -104,6 +113,7 @@
*/
public static final class Builder {
private boolean mIsOnlyUpdate = false;
+ private boolean mSkipProbing = false;
@Nullable
private Duration mTtl;
@@ -127,10 +137,18 @@
}
/**
+ * Sets whether to skip the probing step.
+ */
+ public Builder setSkipProbing(boolean skipProbing) {
+ this.mSkipProbing = skipProbing;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
*/
public MdnsAdvertisingOptions build() {
- return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl);
+ return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl, mSkipProbing);
}
}
}
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 4e27fef..2f3bdc5 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -81,6 +81,12 @@
public static final String NSD_CACHED_SERVICES_REMOVAL = "nsd_cached_services_removal";
/**
+ * A feature flag to control whether to use shorter (16 characters + .local) hostnames, instead
+ * of Android_[32 characters] hostnames.
+ */
+ public static final String NSD_USE_SHORT_HOSTNAMES = "nsd_use_short_hostnames";
+
+ /**
* A feature flag to control the retention time for cached services.
*
* <p> Making the retention time configurable allows for testing and future adjustments.
@@ -122,6 +128,9 @@
// Retention Time for cached services
public final long mCachedServicesRetentionTime;
+ // Flag to use shorter (16 characters + .local) hostnames
+ public final boolean mIsShortHostnamesEnabled;
+
@Nullable
private final FlagOverrideProvider mOverrideProvider;
@@ -217,6 +226,10 @@
NSD_CACHED_SERVICES_RETENTION_TIME, (int) mCachedServicesRetentionTime);
}
+ public boolean isShortHostnamesEnabled() {
+ return mIsShortHostnamesEnabled || isForceEnabledForTest(NSD_USE_SHORT_HOSTNAMES);
+ }
+
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
@@ -231,6 +244,7 @@
boolean avoidAdvertisingEmptyTxtRecords,
boolean isCachedServicesRemovalEnabled,
long cachedServicesRetentionTime,
+ boolean isShortHostnamesEnabled,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -243,6 +257,7 @@
mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
mCachedServicesRetentionTime = cachedServicesRetentionTime;
+ mIsShortHostnamesEnabled = isShortHostnamesEnabled;
mOverrideProvider = overrideProvider;
}
@@ -266,6 +281,7 @@
private boolean mAvoidAdvertisingEmptyTxtRecords;
private boolean mIsCachedServicesRemovalEnabled;
private long mCachedServicesRetentionTime;
+ private boolean mIsShortHostnamesEnabled;
private FlagOverrideProvider mOverrideProvider;
/**
@@ -283,6 +299,7 @@
mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
mIsCachedServicesRemovalEnabled = false;
mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
+ mIsShortHostnamesEnabled = true; // Default enabled.
mOverrideProvider = null;
}
@@ -409,6 +426,16 @@
}
/**
+ * Set whether the short hostnames feature is enabled.
+ *
+ * @see #NSD_USE_SHORT_HOSTNAMES
+ */
+ public Builder setIsShortHostnamesEnabled(boolean isShortHostnamesEnabled) {
+ mIsShortHostnamesEnabled = isShortHostnamesEnabled;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
@@ -423,6 +450,7 @@
mAvoidAdvertisingEmptyTxtRecords,
mIsCachedServicesRemovalEnabled,
mCachedServicesRetentionTime,
+ mIsShortHostnamesEnabled,
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..b9b09ed 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -122,28 +122,32 @@
}
@Override
public void onFinished(MdnsProber.ProbingInfo info) {
- final MdnsAnnouncer.AnnouncementInfo announcementInfo;
- mSharedLog.i("Probing finished for service " + info.getServiceId());
- mCbHandler.post(() -> mCb.onServiceProbingSucceeded(
- MdnsInterfaceAdvertiser.this, info.getServiceId()));
- try {
- announcementInfo = mRecordRepository.onProbingSucceeded(info);
- } catch (IOException e) {
- mSharedLog.e("Error building announcements", e);
- return;
- }
+ handleProbingFinished(info);
+ }
+ }
- mAnnouncer.startSending(info.getServiceId(), announcementInfo,
- 0L /* initialDelayMs */);
+ private void handleProbingFinished(MdnsProber.ProbingInfo info) {
+ final MdnsAnnouncer.AnnouncementInfo announcementInfo;
+ mSharedLog.i("Probing finished for service " + info.getServiceId());
+ mCbHandler.post(() -> mCb.onServiceProbingSucceeded(
+ MdnsInterfaceAdvertiser.this, info.getServiceId()));
+ try {
+ announcementInfo = mRecordRepository.onProbingSucceeded(info);
+ } catch (IOException e) {
+ mSharedLog.e("Error building announcements", e);
+ return;
+ }
- // Re-announce the services which have the same custom hostname.
- final String hostname = mRecordRepository.getHostnameForServiceId(info.getServiceId());
- if (hostname != null) {
- final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos =
- new ArrayList<>(mRecordRepository.restartAnnouncingForHostname(hostname));
- announcementInfos.removeIf((i) -> i.getServiceId() == info.getServiceId());
- reannounceServices(announcementInfos);
- }
+ mAnnouncer.startSending(info.getServiceId(), announcementInfo,
+ 0L /* initialDelayMs */);
+
+ // Re-announce the services which have the same custom hostname.
+ final String hostname = mRecordRepository.getHostnameForServiceId(info.getServiceId());
+ if (hostname != null) {
+ final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos =
+ new ArrayList<>(mRecordRepository.restartAnnouncingForHostname(hostname));
+ announcementInfos.removeIf((i) -> i.getServiceId() == info.getServiceId());
+ reannounceServices(announcementInfos);
}
}
@@ -280,7 +284,12 @@
+ " getting re-added, cancelling exit announcements");
mAnnouncer.stop(replacedExitingService);
}
- mProber.startProbing(mRecordRepository.setServiceProbing(id));
+ final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(id);
+ if (advertisingOptions.skipProbing()) {
+ handleProbingFinished(probingInfo);
+ } else {
+ mProber.startProbing(probingInfo);
+ }
}
/**
@@ -416,13 +425,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 +442,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/MdnsPointerRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
index 39bf653..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.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;
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 e52dd2f..7495aec 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -16,9 +16,14 @@
package com.android.server.connectivity.mdns;
+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;
+
/**
* The query scheduler class for calculating next query tasks parameters.
* <p>
@@ -26,6 +31,25 @@
*/
public class MdnsQueryScheduler {
+ @VisibleForTesting
+ // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
+ static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
+ private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
+ (int) MdnsConfigs.initialTimeBetweenBurstsMs();
+ private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS =
+ (int) MdnsConfigs.timeBetweenBurstsMs();
+ private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
+ private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
+ (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
+ private static final int QUERIES_PER_BURST_PASSIVE_MODE =
+ (int) MdnsConfigs.queriesPerBurstPassive();
+ @VisibleForTesting
+ // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
+ // chances that a query will be received if devices are using a DTIM multiplier (in which case
+ // they only listen once every [multiplier] DTIM intervals).
+ static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
+ static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
+
/**
* The argument for tracking the query tasks status.
*/
@@ -72,19 +96,21 @@
if (mLastScheduledQueryTaskArgs == null) {
return null;
}
- if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff(numOfQueriesBeforeBackoff)) {
+ final QueryTaskConfig lastConfig = mLastScheduledQueryTaskArgs.config;
+ if (!shouldUseQueryBackoff(lastConfig.queryIndex, lastConfig.queryMode,
+ numOfQueriesBeforeBackoff)) {
return null;
}
final long timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
- mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime,
+ lastConfig.queryIndex, lastConfig.queryMode, now, minRemainingTtl, lastSentTime,
numOfQueriesBeforeBackoff, false /* forceEnableBackoff */);
if (timeToRun <= mLastScheduledQueryTaskArgs.timeToRun) {
return null;
}
- mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(mLastScheduledQueryTaskArgs.config,
+ mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(lastConfig,
timeToRun,
minRemainingTtl + now,
sessionId);
@@ -104,17 +130,19 @@
int queryMode,
int numOfQueriesBeforeBackoff,
boolean forceEnableBackoff) {
- final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
+ final int newQueryIndex = currentConfig.getConfigForNextRun(queryMode).queryIndex;
long timeToRun;
if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
- timeToRun = now + nextRunConfig.delayBeforeTaskWithoutBackoffMs;
+ timeToRun = now + getDelayBeforeTaskWithoutBackoff(
+ newQueryIndex, queryMode);
} else {
- timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
- nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff,
- forceEnableBackoff);
+ timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs, newQueryIndex,
+ queryMode, now, minRemainingTtl, lastSentTime,
+ numOfQueriesBeforeBackoff, forceEnableBackoff);
}
- mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(nextRunConfig, timeToRun,
- minRemainingTtl + now,
+ mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(
+ currentConfig.getConfigForNextRun(queryMode),
+ timeToRun, minRemainingTtl + now,
sessionId);
return mLastScheduledQueryTaskArgs;
}
@@ -131,11 +159,11 @@
}
private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
- QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
+ int queryIndex, int queryMode, long now, long minRemainingTtl, long lastSentTime,
int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
- final long baseDelayInMs = queryTaskConfig.delayBeforeTaskWithoutBackoffMs;
+ final long baseDelayInMs = getDelayBeforeTaskWithoutBackoff(queryIndex, queryMode);
if (!(forceEnableBackoff
- || queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff))) {
+ || shouldUseQueryBackoff(queryIndex, queryMode, numOfQueriesBeforeBackoff))) {
return lastSentTime + baseDelayInMs;
}
if (minRemainingTtl <= 0) {
@@ -152,4 +180,93 @@
}
return Math.max(now + (long) (0.8 * minRemainingTtl), lastSentTime + baseDelayInMs);
}
+
+ 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;
+ }
+ }
+
+ 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;
+ }
+
+ static boolean isFirstQueryInBurst(int queryIndex, int queryMode) {
+ return getQueryIndexInBurst(queryIndex, queryMode) == 0;
+ }
+
+ 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;
+ }
+
+ /**
+ * 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;
+ }
+ 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);
+ }
+ }
+
+ /**
+ * Determine if the query backoff should be used.
+ */
+ public static boolean shouldUseQueryBackoff(int queryIndex, int queryMode,
+ int numOfQueriesBeforeBackoff) {
+ // Don't enable backoff mode during the burst or in the first burst
+ if (!isFirstQueryInBurst(queryIndex, queryMode) || isFirstBurst(queryIndex, queryMode)) {
+ return false;
+ }
+ return queryIndex > numOfQueriesBeforeBackoff;
+ }
}
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 c3cb776..bfef5d9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -1482,22 +1482,14 @@
private static String[] splitFullyQualifiedName(
@NonNull NsdServiceInfo info, @NonNull String[] serviceType) {
- final String[] split = new String[serviceType.length + 1];
- split[0] = info.getServiceName();
- System.arraycopy(serviceType, 0, split, 1, serviceType.length);
-
- return split;
+ return CollectionUtils.prependArray(String.class, serviceType, info.getServiceName());
}
private static String[] splitServiceType(@NonNull NsdServiceInfo info) {
// String.split(pattern, 0) removes trailing empty strings, which would appear when
// splitting "domain.name." (with a dot a the end), so this is what is needed here.
final String[] split = info.getServiceType().split("\\.", 0);
- final String[] type = new String[split.length + 1];
- System.arraycopy(split, 0, type, 0, split.length);
- type[split.length] = LOCAL_TLD;
-
- return type;
+ return CollectionUtils.appendArray(String.class, split, LOCAL_TLD);
}
/** Returns whether there will be an SRV record when registering the {@code info}. */
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 a89b004..4708cb6 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -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/MdnsServiceRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
index fd716d2..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,8 +18,6 @@
import android.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
import com.android.net.module.util.DnsUtils;
import java.io.IOException;
@@ -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;
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 a43486e..8c86fb8 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -730,7 +730,7 @@
serviceType,
subtypes,
taskArgs.config.expectUnicastResponse,
- taskArgs.config.transactionId,
+ taskArgs.config.getTransactionId(),
socketKey,
onlyUseIpv6OnIpv6OnlyNetworks,
sendDiscoveryQueries,
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 b640c32..d91bd11 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
@@ -26,6 +26,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresApi;
+import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -164,6 +165,7 @@
this(context, looper, new Dependencies(), sharedLog, socketRequestMonitor);
}
+ @SuppressLint("NewApi")
MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper,
@NonNull Dependencies deps, @NonNull SharedLog sharedLog,
@NonNull SocketRequestMonitor socketRequestMonitor) {
@@ -335,6 +337,7 @@
}
/*** Start monitoring sockets by listening callbacks for sockets creation or removal */
+ @SuppressLint("NewApi")
public void startMonitoringSockets() {
ensureRunningOnHandlerThread(mHandler);
mRequestStop = false; // Reset stop request flag.
@@ -365,6 +368,7 @@
}
}
+ @SuppressLint("NewApi")
private void maybeStopMonitoringSockets() {
if (!mMonitoringSockets) return; // Already unregistered.
if (!mRequestStop) return; // No stop request.
@@ -560,7 +564,6 @@
// Never try mDNS on cellular, or on interfaces with incompatible flags
if (CollectionUtils.contains(transports, TRANSPORT_CELLULAR)
|| iface.isLoopback()
- || iface.isPointToPoint()
|| iface.isVirtual()
|| !iface.isUp()) {
return false;
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 77d1d7a..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;
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 4e74159..2ac5b74 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -17,7 +17,6 @@
package com.android.server.connectivity.mdns;
import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
-import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
import com.android.internal.annotations.VisibleForTesting;
@@ -26,154 +25,42 @@
* Call to getConfigForNextRun returns a config that can be used to build the next query task.
*/
public class QueryTaskConfig {
-
- private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
- (int) MdnsConfigs.initialTimeBetweenBurstsMs();
- private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS =
- (int) MdnsConfigs.timeBetweenBurstsMs();
- private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
- private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
- (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
- private static final int QUERIES_PER_BURST_PASSIVE_MODE =
- (int) MdnsConfigs.queriesPerBurstPassive();
private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
- @VisibleForTesting
- // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
- static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
- @VisibleForTesting
- // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
- // chances that a query will be received if devices are using a DTIM multiplier (in which case
- // they only listen once every [multiplier] DTIM intervals).
- static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
- static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
private final boolean alwaysAskForUnicastResponse =
MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
@VisibleForTesting
- final int transactionId;
- @VisibleForTesting
final boolean expectUnicastResponse;
- private final int queriesPerBurst;
- private final int timeBetweenBurstsInMs;
- private final int burstCounter;
- final long delayBeforeTaskWithoutBackoffMs;
- private final boolean isFirstBurst;
- private final long queryIndex;
+ final int queryIndex;
+ final int queryMode;
- QueryTaskConfig(long queryIndex, int transactionId,
- boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
- int queriesPerBurst, int timeBetweenBurstsInMs,
- long delayBeforeTaskWithoutBackoffMs) {
- this.transactionId = transactionId;
- this.expectUnicastResponse = expectUnicastResponse;
- this.queriesPerBurst = queriesPerBurst;
- this.timeBetweenBurstsInMs = timeBetweenBurstsInMs;
- this.burstCounter = burstCounter;
- this.delayBeforeTaskWithoutBackoffMs = delayBeforeTaskWithoutBackoffMs;
- this.isFirstBurst = isFirstBurst;
+ QueryTaskConfig(int queryMode, int queryIndex) {
+ this.queryMode = queryMode;
this.queryIndex = queryIndex;
+ this.expectUnicastResponse = getExpectUnicastResponse();
}
QueryTaskConfig(int queryMode) {
- 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 (queryMode == AGGRESSIVE_QUERY_MODE) {
- this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
- this.delayBeforeTaskWithoutBackoffMs =
- TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
- } else if (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.delayBeforeTaskWithoutBackoffMs = 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.delayBeforeTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
- }
- this.queryIndex = 0;
- }
-
- long getDelayBeforeNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
- boolean isLastQueryInBurst, int queryMode) {
- if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
- return 0;
- }
- if (isLastQueryInBurst) {
- return timeBetweenBurstsInMs;
- }
- return queryMode == AGGRESSIVE_QUERY_MODE
- ? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS
- : TIME_BETWEEN_QUERIES_IN_BURST_MS;
- }
-
- boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst, int queryMode) {
- if (!isLastQueryInBurst) {
- return false;
- }
- if (queryMode == AGGRESSIVE_QUERY_MODE) {
- return true;
- }
- return alwaysAskForUnicastResponse;
- }
-
- int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst, int queryMode) {
- if (!isLastQueryInBurst) {
- return timeBetweenBurstsInMs;
- }
- 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);
+ this(queryMode, 0);
}
/**
* Get new QueryTaskConfig for next run.
*/
public QueryTaskConfig getConfigForNextRun(int queryMode) {
- long newQueryCount = 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(newQueryCount, newTransactionId,
- getNextExpectUnicastResponse(isLastQueryInBurst, queryMode), newIsFirstBurst,
- newBurstCounter, newQueriesPerBurst,
- getNextTimeBetweenBurstsMs(isLastQueryInBurst, queryMode),
- getDelayBeforeNextTaskWithoutBackoff(
- isFirstQueryInBurst, isLastQueryInBurst, queryMode));
+ final int newQueryIndex = queryIndex + 1;
+ return new QueryTaskConfig(queryMode, newQueryIndex);
}
- /**
- * Determine if the query backoff should be used.
- */
- public boolean shouldUseQueryBackoff(int numOfQueriesBeforeBackoff) {
- // Don't enable backoff mode during the burst or in the first burst
- if (burstCounter != 0 || isFirstBurst) {
- return false;
+ public int getTransactionId() {
+ return (queryIndex % (UNSIGNED_SHORT_MAX_VALUE - 1)) + 1;
+ }
+
+ private boolean getExpectUnicastResponse() {
+ if (queryMode == AGGRESSIVE_QUERY_MODE) {
+ if (MdnsQueryScheduler.isFirstQueryInBurst(queryIndex, queryMode)) {
+ return true;
+ }
}
- return queryIndex > numOfQueriesBeforeBackoff;
+ return queryIndex == 0 || alwaysAskForUnicastResponse;
}
}
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 41b15dd..282ca9a 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
@@ -28,6 +28,7 @@
import android.util.ArraySet;
import android.util.Pair;
+import com.android.net.module.util.CollectionUtils;
import com.android.server.connectivity.mdns.MdnsConstants;
import com.android.server.connectivity.mdns.MdnsInetAddressRecord;
import com.android.server.connectivity.mdns.MdnsPacket;
@@ -273,11 +274,8 @@
* of ["_printer", "_sub", "_http", "_tcp"].
*/
public static String[] constructFullSubtype(String[] serviceType, String subtype) {
- String[] fullSubtype = new String[serviceType.length + 2];
- fullSubtype[0] = subtype;
- fullSubtype[1] = MdnsConstants.SUBTYPE_LABEL;
- System.arraycopy(serviceType, 0, fullSubtype, 2, serviceType.length);
- return fullSubtype;
+ return CollectionUtils.prependArray(String.class, serviceType, subtype,
+ MdnsConstants.SUBTYPE_LABEL);
}
/** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index b8689d6..21b9b1d 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -317,6 +317,6 @@
@Override
public List<String> getInterfaceList() {
PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
- return mTracker.getInterfaceList();
+ return mTracker.getEthernetInterfaceList();
}
}
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 67d0891..9b3c7ba 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -21,6 +21,7 @@
import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -40,7 +41,6 @@
import android.os.Handler;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
-import android.os.ServiceSpecificException;
import android.system.OsConstants;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -51,18 +51,21 @@
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;
import com.android.net.module.util.ip.NetlinkMonitor;
import com.android.net.module.util.netlink.NetlinkConstants;
import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.RtNetlinkLinkMessage;
import com.android.net.module.util.netlink.StructIfinfoMsg;
import com.android.server.connectivity.ConnectivityResources;
import java.io.FileDescriptor;
import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
import java.util.ArrayList;
+import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
@@ -105,7 +108,7 @@
/**
* Track test interfaces if true, don't track otherwise.
- * Volatile is needed as getInterfaceList() does not run on the handler thread.
+ * Volatile is needed as getEthernetInterfaceList() does not run on the handler thread.
*/
private volatile boolean mIncludeTestInterfaces = false;
@@ -139,8 +142,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> {
@@ -215,9 +218,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 {
@@ -397,30 +397,41 @@
return mFactory.hasInterface(iface);
}
+ private List<String> getAllInterfaces() {
+ final ArrayList<String> interfaces = new ArrayList<>(
+ List.of(mFactory.getAvailableInterfaces(/* includeRestricted */ true)));
+
+ if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER && mTetheringInterface != null) {
+ interfaces.add(mTetheringInterface);
+ }
+ return interfaces;
+ }
+
String[] getClientModeInterfaces(boolean includeRestricted) {
return mFactory.getAvailableInterfaces(includeRestricted);
}
- List<String> getInterfaceList() {
+ List<String> getEthernetInterfaceList() {
final List<String> interfaceList = new ArrayList<String>();
- final String[] ifaces;
+ final Enumeration<NetworkInterface> ifaces;
try {
- ifaces = mNetd.interfaceGetList();
- } catch (RemoteException e) {
- Log.e(TAG, "Could not get list of interfaces " + e);
+ ifaces = NetworkInterface.getNetworkInterfaces();
+ } catch (SocketException e) {
+ Log.e(TAG, "Failed to get ethernet interfaces: ", e);
return interfaceList;
}
// There is a possible race with setIncludeTestInterfaces() which can affect
// isValidEthernetInterface (it returns true for test interfaces if setIncludeTestInterfaces
// is set to true).
- // setIncludeTestInterfaces() is only used in tests, and since getInterfaceList() does not
- // run on the handler thread, the behavior around setIncludeTestInterfaces() is
+ // setIncludeTestInterfaces() is only used in tests, and since getEthernetInterfaceList()
+ // does not run on the handler thread, the behavior around setIncludeTestInterfaces() is
// indeterminate either way. This can easily be circumvented by waiting on a callback from
// a test interface after calling setIncludeTestInterfaces() before calling this function.
// In production code, this has no effect.
- for (String iface : ifaces) {
- if (isValidEthernetInterface(iface)) interfaceList.add(iface);
+ while (ifaces.hasMoreElements()) {
+ NetworkInterface iface = ifaces.nextElement();
+ if (isValidEthernetInterface(iface.getName())) interfaceList.add(iface.getName());
}
return interfaceList;
}
@@ -447,7 +458,7 @@
unicastInterfaceStateChange(listener, mTetheringInterface);
}
- unicastEthernetStateChange(listener, mEthernetState);
+ unicastEthernetStateChange(listener, mIsEthernetEnabled);
});
}
@@ -458,10 +469,16 @@
public void setIncludeTestInterfaces(boolean include) {
mHandler.post(() -> {
mIncludeTestInterfaces = include;
- if (!include) {
+ if (include) {
+ trackAvailableInterfaces();
+ } else {
removeTestData();
+ // remove all test interfaces
+ for (String iface : getAllInterfaces()) {
+ if (isValidEthernetInterface(iface)) continue;
+ stopTrackingInterface(iface);
+ }
}
- trackAvailableInterfaces();
});
}
@@ -592,15 +609,11 @@
InterfaceConfigurationParcel config = null;
// Bring up the interface so we get link status indications.
try {
- PermissionUtils.enforceNetworkStackPermission(mContext);
// 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)) {
- // 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);
- }
+ // Only bring the interface up when ethernet is enabled, otherwise set interface down.
+ setInterfaceUpState(iface, mIsEthernetEnabled);
} catch (IllegalStateException e) {
// Either the system is crashing or the interface has disappeared. Just ignore the
// error; we haven't modified any state because we only do that if our calls succeed.
@@ -626,7 +639,7 @@
nc = mNetworkCapabilities.get(hwAddress);
if (nc == null) {
final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP);
- nc = createDefaultNetworkCapabilities(isTestIface);
+ nc = createDefaultNetworkCapabilities(isTestIface, /* overrideTransport */ null);
}
}
@@ -646,6 +659,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;
@@ -656,15 +673,7 @@
return;
}
- if (up) {
- // WARNING! setInterfaceUp() clears the IPv4 address and readds it. Calling
- // enableInterface() on an active interface can lead to a provisioning failure which
- // will cause IpClient to be restarted.
- // TODO: use netlink directly rather than calling into netd.
- NetdUtils.setInterfaceUp(mNetd, iface);
- } else {
- NetdUtils.setInterfaceDown(mNetd, iface);
- }
+ setInterfaceUpState(iface, up);
cb.onResult(iface);
}
@@ -703,10 +712,6 @@
}
private void maybeTrackInterface(String iface) {
- if (!isValidEthernetInterface(iface)) {
- return;
- }
-
// If we don't already track this interface, and if this interface matches
// our regex, start tracking it.
if (mFactory.hasInterface(iface) || iface.equals(mTetheringInterface)) {
@@ -726,13 +731,9 @@
}
private void trackAvailableInterfaces() {
- try {
- final String[] ifaces = mNetd.interfaceGetList();
- for (String iface : ifaces) {
- maybeTrackInterface(iface);
- }
- } catch (RemoteException | ServiceSpecificException e) {
- Log.e(TAG, "Could not get list of interfaces " + e);
+ final List<String> ifaces = getEthernetInterfaceList();
+ for (String iface : ifaces) {
+ maybeTrackInterface(iface);
}
}
@@ -753,9 +754,13 @@
*/
private void parseEthernetConfig(String configString) {
final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
- NetworkCapabilities nc = createNetworkCapabilities(
- !TextUtils.isEmpty(config.mCapabilities) /* clear default capabilities */,
- config.mCapabilities, config.mTransport).build();
+ NetworkCapabilities nc;
+ if (TextUtils.isEmpty(config.mCapabilities)) {
+ boolean isTestIface = config.mIface.matches(TEST_IFACE_REGEXP);
+ nc = createDefaultNetworkCapabilities(isTestIface, config.mTransport);
+ } else {
+ nc = createNetworkCapabilities(config.mCapabilities, config.mTransport).build();
+ }
mNetworkCapabilities.put(config.mIface, nc);
if (null != config.mIpConfig) {
@@ -770,15 +775,16 @@
return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4));
}
- private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) {
- NetworkCapabilities.Builder builder = createNetworkCapabilities(
- false /* clear default capabilities */, null, null)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+ private static NetworkCapabilities createDefaultNetworkCapabilities(
+ boolean isTestIface, @Nullable String overrideTransport) {
+ NetworkCapabilities.Builder builder =
+ createNetworkCapabilities(/* commaSeparatedCapabilities */ null, overrideTransport)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
if (isTestIface) {
builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
@@ -792,7 +798,6 @@
/**
* Parses a static list of network capabilities
*
- * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities
* @param commaSeparatedCapabilities A comma separated string list of integer encoded
* NetworkCapability.NET_CAPABILITY_* values
* @param overrideTransport A string representing a single integer encoded override transport
@@ -802,12 +807,12 @@
*/
@VisibleForTesting
static NetworkCapabilities.Builder createNetworkCapabilities(
- boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities,
- @Nullable String overrideTransport) {
+ @Nullable String commaSeparatedCapabilities, @Nullable String overrideTransport) {
- final NetworkCapabilities.Builder builder = clearDefaultCapabilities
- ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
- : new NetworkCapabilities.Builder();
+ final NetworkCapabilities.Builder builder =
+ TextUtils.isEmpty(commaSeparatedCapabilities)
+ ? new NetworkCapabilities.Builder()
+ : NetworkCapabilities.Builder.withoutDefaultCapabilities();
// Determine the transport type. If someone has tried to define an override transport then
// attempt to add it. Since we can only have one override, all errors with it will
@@ -960,43 +965,37 @@
@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;
-
- if (enabled) {
- trackAvailableInterfaces();
- } else {
- // TODO: maybe also disable server mode interface as well.
- untrackFactoryInterfaces();
+ mIsEthernetEnabled = enabled;
+ for (String iface : getAllInterfaces()) {
+ setInterfaceUpState(iface, enabled);
}
- broadcastEthernetStateChange(mEthernetState);
+ 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.
}
@@ -1004,11 +1003,17 @@
mListeners.finishBroadcast();
}
+ private void setInterfaceUpState(@NonNull String interfaceName, boolean up) {
+ if (!NetlinkUtils.setInterfaceFlags(interfaceName, up ? IFF_UP : ~IFF_UP)) {
+ Log.e(TAG, "Failed to set interface " + interfaceName + (up ? " up" : " down"));
+ }
+ }
+
void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
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 294a85a..5c5f4ca 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -52,7 +52,6 @@
import static android.net.TrafficStats.KB_IN_BYTES;
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.net.TrafficStats.UID_TETHERING;
-import static android.net.TrafficStats.getValueForTypeFromFirstEntry;
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;
@@ -127,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;
@@ -182,15 +183,17 @@
import com.android.net.module.util.NetworkStatsUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.SkDestroyListener;
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U8;
import com.android.net.module.util.bpf.CookieTagMapKey;
import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.net.module.util.netlink.InetDiagMessage;
+import com.android.net.module.util.netlink.StructInetDiagSockId;
import com.android.networkstack.apishim.BroadcastOptionsShimImpl;
import com.android.networkstack.apishim.ConstantsShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-import com.android.server.BpfNetMaps;
import com.android.server.connectivity.ConnectivityResources;
import java.io.File;
@@ -215,6 +218,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
/**
* Collect and persist detailed network statistics, and provide this data to
@@ -485,16 +489,24 @@
@GuardedBy("mStatsLock")
private long mLatestNetworkStatsUpdatedBroadcastScheduledTime = Long.MIN_VALUE;
+ @Nullable
private final TrafficStatsRateLimitCache mTrafficStatsTotalCache;
+ @Nullable
private final TrafficStatsRateLimitCache mTrafficStatsIfaceCache;
+ @Nullable
private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
+ // A feature flag to control whether the client-side rate limit cache should be enabled.
+ @VisibleForTesting
+ public 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";
static final String BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG =
"broadcast_network_stats_updated_rate_limit_enabled_flag";
- private final boolean mAlwaysUseTrafficStatsServiceRateLimitCache;
+ private final boolean mIsTrafficStatsServiceRateLimitCacheEnabled;
private final int mTrafficStatsRateLimitCacheExpiryDuration;
private final int mTrafficStatsServiceRateLimitCacheMaxEntries;
+ private final TrafficStatsRateLimitCacheConfig mTrafficStatsRateLimitCacheClientSideConfig;
private final boolean mBroadcastNetworkStatsUpdatedRateLimitEnabled;
@@ -688,30 +700,43 @@
mEventLogger = null;
}
- mAlwaysUseTrafficStatsServiceRateLimitCache =
- mDeps.alwaysUseTrafficStatsServiceRateLimitCache(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();
mTrafficStatsServiceRateLimitCacheMaxEntries =
mDeps.getTrafficStatsServiceRateLimitCacheMaxEntries();
- mTrafficStatsTotalCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration,
- mTrafficStatsServiceRateLimitCacheMaxEntries);
- mTrafficStatsIfaceCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration,
- mTrafficStatsServiceRateLimitCacheMaxEntries);
- mTrafficStatsUidCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration,
- mTrafficStatsServiceRateLimitCacheMaxEntries);
+ 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
- // on the experiment flag, BpfNetMaps starts C SkDestroyListener (existing code) or
- // NetworkStatsService starts Java SkDestroyListener (new code).
- final BpfNetMaps bpfNetMaps = mDeps.makeBpfNetMaps(mContext);
- mSkDestroyListener = mDeps.makeSkDestroyListener(mCookieTagMap, mHandler);
+ mSkDestroyListener = mDeps.makeSkDestroyListener((message) -> {
+ final StructInetDiagSockId sockId = message.inetDiagMsg.id;
+ try {
+ mCookieTagMap.deleteEntry(new CookieTagMapKey(sockId.cookie));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to delete CookieTagMap entry for " + sockId.cookie + ": " + e);
+ }
+ }, mHandler);
mHandler.post(mSkDestroyListener::start);
}
@@ -932,16 +957,11 @@
return Build.isDebuggable();
}
- /** Create a new BpfNetMaps. */
- public BpfNetMaps makeBpfNetMaps(Context ctx) {
- return new BpfNetMaps(ctx);
- }
-
/** Create a new SkDestroyListener. */
- public SkDestroyListener makeSkDestroyListener(
- IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
- return new SkDestroyListener(
- cookieTagMap, handler, new SharedLog(MAX_SOCKET_DESTROY_LISTENER_LOGS, TAG));
+ public SkDestroyListener makeSkDestroyListener(Consumer<InetDiagMessage> consumer,
+ Handler handler) {
+ return SkDestroyListener.makeSkDestroyListener(consumer, handler,
+ new SharedLog(MAX_SOCKET_DESTROY_LISTENER_LOGS, TAG));
}
/**
@@ -964,13 +984,42 @@
}
/**
- * Get whether TrafficStats service side rate-limit cache is always applied.
+ * 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.
*/
- public boolean alwaysUseTrafficStatsServiceRateLimitCache(@NonNull Context ctx) {
- return SdkLevel.isAtLeastV() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+ @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);
}
@@ -2133,30 +2182,48 @@
}
}
- @Override
- public long getUidStats(int uid, int type) {
- return getValueForTypeFromFirstEntry(getTypelessUidStats(uid), type);
+ /**
+ * 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));
}
- @NonNull
+ /**
+ * 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 NetworkStats getTypelessUidStats(int uid) {
- final NetworkStats stats = new NetworkStats(0, 0);
+ public StatsResult getUidStats(int uid) {
final int callingUid = Binder.getCallingUid();
if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
- return stats;
+ return null;
}
final NetworkStats.Entry entry;
- if (mAlwaysUseTrafficStatsServiceRateLimitCache
- || mDeps.isChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, callingUid)) {
+ if (useServiceSideCache(callingUid)) {
entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
() -> mDeps.nativeGetUidStat(uid));
} else entry = mDeps.nativeGetUidStat(uid);
- if (entry != null) {
- stats.insertEntry(entry);
- }
- return stats;
+ return getStatsResultFromEntryOrNull(entry);
}
@Nullable
@@ -2173,24 +2240,24 @@
return entry;
}
- @NonNull
+ @Nullable
@Override
- public NetworkStats getTypelessIfaceStats(@NonNull String iface) {
+ public StatsResult getIfaceStats(@NonNull String iface) {
Objects.requireNonNull(iface);
final NetworkStats.Entry entry;
- if (mAlwaysUseTrafficStatsServiceRateLimitCache
- || mDeps.isChangeEnabled(
- ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
+ if (useServiceSideCache(Binder.getCallingUid())) {
entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
() -> getIfaceStatsInternal(iface));
} else entry = getIfaceStatsInternal(iface);
- NetworkStats stats = new NetworkStats(0, 0);
- if (entry != null) {
- stats.insertEntry(entry);
- }
- return stats;
+ return getStatsResultFromEntryOrNull(entry);
+ }
+
+ @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
@@ -2203,30 +2270,39 @@
return entry;
}
- @NonNull
+ @Nullable
@Override
- public NetworkStats getTypelessTotalStats() {
+ public StatsResult getTotalStats() {
final NetworkStats.Entry entry;
- if (mAlwaysUseTrafficStatsServiceRateLimitCache
- || mDeps.isChangeEnabled(
- ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
+ if (useServiceSideCache(Binder.getCallingUid())) {
entry = mTrafficStatsTotalCache.getOrCompute(
IFACE_ALL, UID_ALL, () -> getTotalStatsInternal());
} else entry = getTotalStatsInternal();
- final NetworkStats stats = new NetworkStats(0, 0);
- if (entry != null) {
- stats.insertEntry(entry);
- }
- return stats;
+ 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) {
@@ -2996,8 +3072,8 @@
} catch (IOException e) {
pw.println("(failed to dump FastDataInput counters)");
}
- pw.print("trafficstats.service.cache.alwaysuse",
- mAlwaysUseTrafficStatsServiceRateLimitCache);
+ pw.print("trafficstats.service.cache.isenabled",
+ mIsTrafficStatsServiceRateLimitCacheEnabled);
pw.println();
pw.print(TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
mTrafficStatsRateLimitCacheExpiryDuration);
@@ -3005,6 +3081,9 @@
pw.print(TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME,
mTrafficStatsServiceRateLimitCacheMaxEntries);
pw.println();
+ pw.print("trafficstats.client.cache.config",
+ mTrafficStatsRateLimitCacheClientSideConfig);
+ pw.println();
pw.decreaseIndent();
diff --git a/service-t/src/com/android/server/net/SkDestroyListener.java b/service-t/src/com/android/server/net/SkDestroyListener.java
deleted file mode 100644
index a6cc2b5..0000000
--- a/service-t/src/com/android/server/net/SkDestroyListener.java
+++ /dev/null
@@ -1,84 +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.server.net;
-
-import static android.system.OsConstants.NETLINK_INET_DIAG;
-
-import android.os.Handler;
-import android.system.ErrnoException;
-
-import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.bpf.CookieTagMapKey;
-import com.android.net.module.util.bpf.CookieTagMapValue;
-import com.android.net.module.util.ip.NetlinkMonitor;
-import com.android.net.module.util.netlink.InetDiagMessage;
-import com.android.net.module.util.netlink.NetlinkMessage;
-import com.android.net.module.util.netlink.StructInetDiagSockId;
-
-import java.io.PrintWriter;
-
-/**
- * Monitor socket destroy and delete entry from cookie tag bpf map.
- */
-public class SkDestroyListener extends NetlinkMonitor {
- private static final int SKNLGRP_INET_TCP_DESTROY = 1;
- private static final int SKNLGRP_INET_UDP_DESTROY = 2;
- private static final int SKNLGRP_INET6_TCP_DESTROY = 3;
- private static final int SKNLGRP_INET6_UDP_DESTROY = 4;
-
- // TODO: if too many sockets are closed too quickly, this can overflow the socket buffer, and
- // some entries in mCookieTagMap will not be freed. In order to fix this it would be needed to
- // periodically dump all sockets and remove the tag entries for sockets that have been closed.
- // For now, set a large-enough buffer that hundreds of sockets can be closed without getting
- // ENOBUFS and leaking mCookieTagMap entries.
- private static final int SOCK_RCV_BUF_SIZE = 512 * 1024;
-
- private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap;
-
- SkDestroyListener(final IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap,
- final Handler handler, final SharedLog log) {
- super(handler, log, "SkDestroyListener", NETLINK_INET_DIAG,
- 1 << (SKNLGRP_INET_TCP_DESTROY - 1)
- | 1 << (SKNLGRP_INET_UDP_DESTROY - 1)
- | 1 << (SKNLGRP_INET6_TCP_DESTROY - 1)
- | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1),
- SOCK_RCV_BUF_SIZE);
- mCookieTagMap = cookieTagMap;
- }
-
- @Override
- public void processNetlinkMessage(final NetlinkMessage nlMsg, final long whenMs) {
- if (!(nlMsg instanceof InetDiagMessage)) {
- mLog.e("Received non InetDiagMessage");
- return;
- }
- final StructInetDiagSockId sockId = ((InetDiagMessage) nlMsg).inetDiagMsg.id;
- try {
- mCookieTagMap.deleteEntry(new CookieTagMapKey(sockId.cookie));
- } catch (ErrnoException e) {
- mLog.e("Failed to delete CookieTagMap entry for " + sockId.cookie + ": " + e);
- }
- }
-
- /**
- * Dump the contents of SkDestroyListener log.
- */
- public void dump(PrintWriter pw) {
- mLog.reverseDump(pw);
- }
-}
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index ca97d07..4f99d1b 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;
@@ -30,10 +29,12 @@
/**
* A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
* with an adjustable expiry duration to manage data freshness.
+ *
+ * @deprecated Use {@link LruCacheWithExpiry} instead.
*/
-class TrafficStatsRateLimitCache {
- private final Clock mClock;
- private final long mExpiryDurationMs;
+// TODO: Remove this when service side rate limit cache solution is removed.
+class TrafficStatsRateLimitCache extends
+ LruCacheWithExpiry<TrafficStatsRateLimitCache.TrafficStatsCacheKey, NetworkStats.Entry> {
/**
* Constructs a new {@link TrafficStatsRateLimitCache} with the specified expiry duration.
@@ -43,19 +44,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.millis(), 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 +62,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 +80,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 +98,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 +109,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 567c079..8b469e4 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -90,7 +90,6 @@
static_libs: [
"libnet_utils_device_common_bpfjni",
"libnet_utils_device_common_bpfutils",
- "libnet_utils_device_common_timerfdjni",
],
shared_libs: [
"liblog",
@@ -114,7 +113,6 @@
":services.connectivity-netstats-jni-sources",
"jni/com_android_server_connectivity_ClatCoordinator.cpp",
"jni/com_android_server_ServiceManagerWrapper.cpp",
- "jni/com_android_server_TestNetworkService.cpp",
"jni/onload.cpp",
],
header_libs: [
@@ -126,6 +124,7 @@
"libmodules-utils-build",
"libnetjniutils",
"libnet_utils_device_common_bpfjni",
+ "libserviceconnectivityjni",
"netd_aidl_interface-lateststable-ndk",
],
shared_libs: [
@@ -162,6 +161,7 @@
],
libs: [
"framework-annotations-lib",
+ "framework-bluetooth.stubs.module_lib",
"framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
// The framework-connectivity-t library is only available on T+ platforms
@@ -207,6 +207,7 @@
},
visibility: [
"//packages/modules/Connectivity/service-t",
+ "//packages/modules/Connectivity/service-b",
"//packages/modules/Connectivity/networksecurity:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/service:__subpackages__",
@@ -252,7 +253,11 @@
"service-networksecurity-pre-jarjar",
service_remoteauth_pre_jarjar_lib,
"service-thread-pre-jarjar",
- ],
+ ] + select(release_flag("RELEASE_MOVE_VCN_TO_MAINLINE"), {
+ true: ["service-connectivity-b-pre-jarjar"],
+ default: [],
+ }),
+
// The below libraries are not actually needed to build since no source is compiled
// (only combining prebuilt static_libs), but they are necessary so that R8 has the right
// references to optimize the code. Without these, there will be missing class warnings and
@@ -338,6 +343,7 @@
name: "service-connectivity-jarjar-gen",
tool_files: [
":service-connectivity-pre-jarjar{.jar}",
+ ":service-connectivity-b-pre-jarjar{.jar}",
":service-connectivity-tiramisu-pre-jarjar{.jar}",
"jarjar-excludes.txt",
],
@@ -347,6 +353,7 @@
out: ["service_connectivity_jarjar_rules.txt"],
cmd: "$(location jarjar-rules-generator) " +
"$(location :service-connectivity-pre-jarjar{.jar}) " +
+ "$(location :service-connectivity-b-pre-jarjar{.jar}) " +
"$(location :service-connectivity-tiramisu-pre-jarjar{.jar}) " +
"--prefix android.net.connectivity " +
"--excludes $(location jarjar-excludes.txt) " +
diff --git a/service/ServiceConnectivityResources/OWNERS b/service/ServiceConnectivityResources/OWNERS
index df41ff2..c3c08ee 100644
--- a/service/ServiceConnectivityResources/OWNERS
+++ b/service/ServiceConnectivityResources/OWNERS
@@ -1,2 +1,3 @@
+per-file res/raw/ct_public_keys.pem = file:platform/packages/modules/Connectivity:main:/networksecurity/OWNERS
per-file res/values/config_thread.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
per-file res/values/overlayable.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem b/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
new file mode 100644
index 0000000..80dccbe
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
@@ -0,0 +1,42 @@
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnmb1lacOnP5H1bwb06mG
+fEUeC9PZRwNQskSs9KaWrpfrSkLKuHXkVCbgeagbUR/Sh1OeIhyJRSS0PLCO0JjC
+UpGhYMrIGRgEET4IrP9f8aMFqxxxBUEanI+OxAhIJlP9tiWfGdKAASYcxg/DyXXz
+bqSXBEFJqBLqmnfHcLB/NhO1ejV6AiU1NMrT+iWSrJG8b6mq/LlAqWvidK8oPBsW
+87M4pPLpUoA54ultjx2wEzJ9dBy6jtnKZ/dz4DkDhYug5izRDcYtEfzQBoum0etV
+s4EoogW1AMeqW5G+L4HjPNgp3gNGZ/2RaBy7gp8Br+byYu2wHwdQIBQjS310yaKc
+nuNFOd+Q0DrzvHKB7yYzzdwo+hNocPpkvOzSw74jd09kDZQ+S2peCJ5NPU7VKT6/
+tkvc3tYA9pAu4/T+BGqRft4FjgeNANfSIX/WhWDzzVWymTUGFUvt+D0fF3Cw+XSa
+b8uTgRZ1Ma/FvSGgXHVoG9E4QAFFG4I9mmRqsnzA+8fqSNAfieL5OWecq4PU+pMa
+uNVJ9hbvmW2yXuMgEg6K9kFLdxggRn+OcxowgXJJh79L0r7RN1d8kuHelDhOzcte
+dUTtLNOb/1PA0d/I2IVJwc9xSQZXurqqT/Z+c01B3/R0BgGDkIT/yZ5iHPoZFYPD
+U8UdQzUK0KXgGkc8P5pJW8kCAwEAAQ==
+-----END PUBLIC KEY-----
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAn1ssK1m56VVurVJ/fNKe
++aXDnytBy/hWY48ZT+ZC0S29llfjkaCBlmkngoI2hgwz9BI6pHwUINS15pT1sznw
+kHoEaNKr8sANHTQ0PYlDuk5iQSjnaGz7XmqbZ3c92BQrmLG/kwX2c17YNJI2qCk3
+MaBJBeS5YErR3xcucT9M7qtNWWIT9O0sLV1lDUZVCYedWBSnNz1/mAiLhttWmU5u
+GKl/5LmjWP/piNjh8whx0abJUGeGS2HH0JAOb0pjBV6UQvj3tO+gTiNDhdrE4CKh
+Qn8SKNjW/BY320f0A5t581Q0++cQUAisRgBQos0Pkvg5vb6wgII+pJq0SnZoYFfH
+oycuR4q3eOCgJmpEAPC0MhNpIDUQS6p3QabD9ID+21ymiQa/Zf8Mv2xMM6ZItKxX
+77vSKvBbimTGmB2IU+Zi484PKI5QwxBUCHVSmNpvHyXyjhBmpqik9Op26QYYT10b
+ADnJY1L/Q+i44nI4pfwgIncqAWuLnxg/XggJDWj48Un9SMNoyN7gzQX75M7rh7/t
+F6QtKvJreP0pP2UoVSgZKjXnL9tqeZbOdZU1kBHi1HOhlUKTfq5dn2fVUeYkE769
+clFF7Y1FiI259IPhTKiOIfARJ4BL4Sn8D9c9vpxDYPFl5bCJbspmFpwfzTMDnGVS
+/IlY6Putpv2/lD1B7aQGt1sCAwEAAQ==
+-----END PUBLIC KEY-----
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwMPNecDhLDamK9Rs8W5V
+c0LWZPbk9FNP+budBoFMX46dWyJ75O5e8xY9UhJSBFl8+Itm/rWO/h2zZ6qE3Cq8
+1LYbOg+x+rLnYOcAvD2O7EU73A3RD/vqoUDDVK3cwKMq3ry7CYu+NJW7TRIKV7Ct
+BMCBrvpmC1WlZ/jxSV0Soapza4/H+UV0hHYh/Wn3EWObGWYdI3yxwZ81AyU1QCR/
+oaO0PQPXqvo3gPBINnK3Qr0aLtYc4YCfTXe6i4g3DeAlkpqNLZtC2hyqiRB4Dg47
+zDzYECGofRAu8w9d8uI+eccacXfvI9zEcL7FAl5AzBKmMFOfBNTr08V7+aROWfGO
+7imWsj2MQ6RG17zqJak5QX/1bqDxwhG0KolB86mPZu0WeKz1B3iP5qAUlDNBHLDV
+pQIez0mrMsVsimVguuLYHMpIgijphA9WhijCJW2x7c6aocB6IpnMIV1sqnUQTwLG
+t32AMrckxqFmaKGj/8I9M+Xj+Cy4fIa5YSOdb/tlaYZSfjH5ch41xucQ2HWFyZ/9
+hkTFodvF5ajCQ5maHeIjDkS/Bc/s9CB+/fbSkstDsPMRp/ExyQcEYjKTG5o9Ewyo
++KGGXS2dSS10Ibl0Zx/S/0ZuZx8ZAxMOIIPpugdkWqHU9thh71dR8zM4KMkEfB8C
+sWLGB1yMuztn9nRUcpiEZTECAwEAAQ==
+-----END PUBLIC KEY-----
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/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 4027038..d1d9e52 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -71,4 +71,18 @@
-->
<string-array name="config_thread_mdns_vendor_specific_txts">
</string-array>
+
+ <!-- Whether to enable / start SRP server only when border routing is ready. SRP server and
+ border routing are mandatory features required by a Thread Border Router, and it takes 10 to
+ 20 seconds to establish border routing. Starting SRP server earlier is useful for use cases
+ where the user needs to know what are the devices in the network before actually needs to reach
+ to the devices, or reaching to Thread end devices doesn't require border routing to work.
+ -->
+ <bool name="config_thread_srp_server_wait_for_border_routing_enabled">true</bool>
+
+ <!-- Whether this border router will automatically join the previous connected network after
+ device reboots. Setting this value to false can allow the user to control the lifecycle of
+ the Thread border router state on this device.
+ -->
+ <bool name="config_thread_border_router_auto_join_enabled">true</bool>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index fbaae05..7ac86aa 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -52,6 +52,8 @@
<item type="string" name="config_thread_vendor_oui" />
<item type="string" name="config_thread_model_name" />
<item type="array" name="config_thread_mdns_vendor_specific_txts" />
+ <item type="bool" name="config_thread_srp_server_wait_for_border_routing_enabled" />
+ <item type="bool" name="config_thread_border_router_auto_join_enabled" />
</policy>
</overlayable>
</resources>
diff --git a/service/jarjar-excludes.txt b/service/jarjar-excludes.txt
index 7bd3862..9076b53 100644
--- a/service/jarjar-excludes.txt
+++ b/service/jarjar-excludes.txt
@@ -1,3 +1,4 @@
# Classes loaded by SystemServer via their hardcoded name, so they can't be jarjared
com\.android\.server\.ConnectivityServiceInitializer(\$.+)?
+com\.android\.server\.ConnectivityServiceInitializerB(\$.+)?
com\.android\.server\.NetworkStatsServiceInitializer(\$.+)?
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
deleted file mode 100644
index 08d31a3..0000000
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2018 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_NDEBUG 0
-
-#define LOG_TAG "TestNetworkServiceJni"
-
-#include <arpa/inet.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <linux/if.h>
-#include <linux/if_tun.h>
-#include <linux/ipv6_route.h>
-#include <linux/route.h>
-#include <netinet/in.h>
-#include <stdio.h>
-#include <string.h>
-#include <sys/ioctl.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-
-#include <log/log.h>
-
-#include "jni.h"
-#include <android-base/stringprintf.h>
-#include <android-base/unique_fd.h>
-#include <bpf/KernelUtils.h>
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedUtfChars.h>
-
-#ifndef IFF_NO_CARRIER
-#define IFF_NO_CARRIER 0x0040
-#endif
-
-namespace android {
-
-//------------------------------------------------------------------------------
-
-static void throwException(JNIEnv* env, int error, const char* action, const char* iface) {
- const std::string& msg = "Error: " + std::string(action) + " " + std::string(iface) + ": "
- + std::string(strerror(error));
- jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
-}
-
-// enable or disable carrier on tun / tap interface.
-static void setTunTapCarrierEnabledImpl(JNIEnv* env, const char* iface, int tunFd, bool enabled) {
- uint32_t carrierOn = enabled;
- if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
- throwException(env, errno, "set carrier", iface);
- }
-}
-
-static int createTunTapImpl(JNIEnv* env, bool isTun, bool hasCarrier, bool setIffMulticast,
- const char* iface) {
- base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
- ifreq ifr{};
-
- // Allocate interface.
- ifr.ifr_flags = (isTun ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
- if (!hasCarrier) {
- // Using IFF_NO_CARRIER is supported starting in kernel version >= 6.0
- // Up until then, unsupported flags are ignored.
- if (!bpf::isAtLeastKernelVersion(6, 0, 0)) {
- throwException(env, EOPNOTSUPP, "IFF_NO_CARRIER not supported", ifr.ifr_name);
- return -1;
- }
- ifr.ifr_flags |= IFF_NO_CARRIER;
- }
- strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
- if (ioctl(tun.get(), TUNSETIFF, &ifr)) {
- throwException(env, errno, "allocating", ifr.ifr_name);
- return -1;
- }
-
- // Mark some TAP interfaces as supporting multicast
- if (setIffMulticast && !isTun) {
- base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
- ifr.ifr_flags = IFF_MULTICAST;
-
- if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
- throwException(env, errno, "set IFF_MULTICAST", ifr.ifr_name);
- return -1;
- }
- }
-
- return tun.release();
-}
-
-static void bringUpInterfaceImpl(JNIEnv* env, const char* iface) {
- // Activate interface using an unconnected datagram socket.
- base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
-
- ifreq ifr{};
- strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
- if (ioctl(inet6CtrlSock.get(), SIOCGIFFLAGS, &ifr)) {
- throwException(env, errno, "read flags", iface);
- return;
- }
- ifr.ifr_flags |= IFF_UP;
- if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
- throwException(env, errno, "set IFF_UP", iface);
- return;
- }
-}
-
-//------------------------------------------------------------------------------
-
-
-
-static void setTunTapCarrierEnabled(JNIEnv* env, jclass /* clazz */, jstring
- jIface, jint tunFd, jboolean enabled) {
- ScopedUtfChars iface(env, jIface);
- if (!iface.c_str()) {
- jniThrowNullPointerException(env, "iface");
- return;
- }
- setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
-}
-
-static jint createTunTap(JNIEnv* env, jclass /* clazz */, jboolean isTun,
- jboolean hasCarrier, jboolean setIffMulticast, jstring jIface) {
- ScopedUtfChars iface(env, jIface);
- if (!iface.c_str()) {
- jniThrowNullPointerException(env, "iface");
- return -1;
- }
-
- return createTunTapImpl(env, isTun, hasCarrier, setIffMulticast, iface.c_str());
-}
-
-static void bringUpInterface(JNIEnv* env, jclass /* clazz */, jstring jIface) {
- ScopedUtfChars iface(env, jIface);
- if (!iface.c_str()) {
- jniThrowNullPointerException(env, "iface");
- return;
- }
- bringUpInterfaceImpl(env, iface.c_str());
-}
-
-//------------------------------------------------------------------------------
-
-static const JNINativeMethod gMethods[] = {
- {"nativeSetTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V", (void*)setTunTapCarrierEnabled},
- {"nativeCreateTunTap", "(ZZZLjava/lang/String;)I", (void*)createTunTap},
- {"nativeBringUpInterface", "(Ljava/lang/String;)V", (void*)bringUpInterface},
-};
-
-int register_com_android_server_TestNetworkService(JNIEnv* env) {
- return jniRegisterNativeMethods(env,
- "android/net/connectivity/com/android/server/TestNetworkService", gMethods,
- NELEM(gMethods));
-}
-
-}; // namespace android
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index bb70d4f..f87470d 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -21,11 +21,12 @@
namespace android {
-int register_com_android_server_TestNetworkService(JNIEnv* env);
int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env);
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_ServiceConnectivityJni(JNIEnv *env,
+ char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv *env;
@@ -34,10 +35,6 @@
return JNI_ERR;
}
- if (register_com_android_server_TestNetworkService(env) < 0) {
- return JNI_ERR;
- }
-
if (register_com_android_server_ServiceManagerWrapper(env) < 0) {
return JNI_ERR;
}
@@ -56,6 +53,12 @@
}
}
+ if (register_com_android_net_module_util_ServiceConnectivityJni(
+ env, "android/net/connectivity/com/android/net/module/util/"
+ "ServiceConnectivityJni") < 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/libconnectivity/include/connectivity_native.h b/service/libconnectivity/include/connectivity_native.h
index f4676a9..f264b68 100644
--- a/service/libconnectivity/include/connectivity_native.h
+++ b/service/libconnectivity/include/connectivity_native.h
@@ -20,12 +20,6 @@
#include <sys/cdefs.h>
#include <netinet/in.h>
-// For branches that do not yet have __ANDROID_API_U__ defined, like module
-// release branches.
-#ifndef __ANDROID_API_U__
-#define __ANDROID_API_U__ 34
-#endif
-
__BEGIN_DECLS
/**
@@ -41,7 +35,7 @@
*
* @param port Int corresponding to port number.
*/
-int AConnectivityNative_blockPortForBind(in_port_t port) __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_blockPortForBind(in_port_t port) __INTRODUCED_IN(34);
/**
* Unblocks a port that has previously been blocked.
@@ -54,7 +48,7 @@
*
* @param port Int corresponding to port number.
*/
-int AConnectivityNative_unblockPortForBind(in_port_t port) __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_unblockPortForBind(in_port_t port) __INTRODUCED_IN(34);
/**
* Unblocks all ports that have previously been blocked.
@@ -64,7 +58,7 @@
* - EPERM if the UID of the client doesn't have network stack permission
* - Other errors as per https://man7.org/linux/man-pages/man2/bpf.2.html
*/
-int AConnectivityNative_unblockAllPortsForBind() __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_unblockAllPortsForBind() __INTRODUCED_IN(34);
/**
* Gets the list of ports that have been blocked.
@@ -79,7 +73,7 @@
* blocked ports, which may be larger than the ports array that was filled.
*/
int AConnectivityNative_getPortsBlockedForBind(in_port_t* _Nonnull ports, size_t* _Nonnull count)
- __INTRODUCED_IN(__ANDROID_API_U__);
+ __INTRODUCED_IN(34);
__END_DECLS
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 6c1c2c4..9554bd8 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -47,6 +47,7 @@
srcs: [
"clatutils_test.cpp",
],
+ stl: "libc++_static",
header_libs: [
"bpf_connectivity_headers",
],
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 44868b2d..36c0cf9 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -25,6 +25,8 @@
import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
import static android.net.BpfNetMapsConstants.IIF_MATCH;
import static android.net.BpfNetMapsConstants.INGRESS_DISCARD_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCAL_NET_ACCESS_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCAL_NET_BLOCKED_UID_MAP_PATH;
import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
@@ -36,16 +38,16 @@
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
-import static android.net.INetd.PERMISSION_INTERNET;
-import static android.net.INetd.PERMISSION_NONE;
-import static android.net.INetd.PERMISSION_UNINSTALLED;
-import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
import static android.system.OsConstants.EINVAL;
import static android.system.OsConstants.ENODEV;
import static android.system.OsConstants.ENOENT;
import static android.system.OsConstants.EOPNOTSUPP;
import static com.android.server.ConnectivityStatsLog.NETWORK_BPF_MAP_INFO;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_NONE;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_INTERNET;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
import android.app.StatsManager;
import android.content.Context;
@@ -74,6 +76,7 @@
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.SingleWriterBpfMap;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Bool;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U32;
import com.android.net.module.util.Struct.U8;
@@ -81,6 +84,7 @@
import com.android.net.module.util.bpf.CookieTagMapValue;
import com.android.net.module.util.bpf.IngressDiscardKey;
import com.android.net.module.util.bpf.IngressDiscardValue;
+import com.android.net.module.util.bpf.LocalNetAccessKey;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -131,9 +135,12 @@
private static IBpfMap<S32, U8> sDataSaverEnabledMap = null;
private static IBpfMap<IngressDiscardKey, IngressDiscardValue> sIngressDiscardMap = null;
+ private static IBpfMap<LocalNetAccessKey, Bool> sLocalNetAccessMap = null;
+ private static IBpfMap<U32, Bool> sLocalNetBlockedUidMap = null;
+
private static final List<Pair<Integer, String>> PERMISSION_LIST = Arrays.asList(
- Pair.create(PERMISSION_INTERNET, "PERMISSION_INTERNET"),
- Pair.create(PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
+ Pair.create(TRAFFIC_PERMISSION_INTERNET, "PERMISSION_INTERNET"),
+ Pair.create(TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
);
/**
@@ -186,6 +193,25 @@
sIngressDiscardMap = ingressDiscardMap;
}
+ /**
+ * Set localNetAccessMap for test.
+ */
+ @VisibleForTesting
+ public static void setLocalNetAccessMapForTest(
+ IBpfMap<LocalNetAccessKey, Bool> localNetAccessMap) {
+ sLocalNetAccessMap = localNetAccessMap;
+ }
+
+ /**
+ * Set localNetBlockedUidMap for test.
+ */
+ @VisibleForTesting
+ public static void setLocalNetBlockedUidMapForTest(
+ IBpfMap<U32, Bool> localNetBlockedUidMap) {
+ sLocalNetBlockedUidMap = localNetBlockedUidMap;
+ }
+
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static IBpfMap<S32, U32> getConfigurationMap() {
try {
@@ -247,6 +273,26 @@
}
}
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ private static IBpfMap<U32, Bool> getLocalNetBlockedUidMap() {
+ try {
+ return SingleWriterBpfMap.getSingleton(LOCAL_NET_BLOCKED_UID_MAP_PATH,
+ U32.class, Bool.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open local_net_blocked_uid map", e);
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ private static IBpfMap<LocalNetAccessKey, Bool> getLocalNetAccessMap() {
+ try {
+ return SingleWriterBpfMap.getSingleton(LOCAL_NET_ACCESS_MAP_PATH,
+ LocalNetAccessKey.class, Bool.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open local_net_access map", e);
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static void initBpfMaps() {
if (sConfigurationMap == null) {
@@ -299,6 +345,27 @@
} catch (ErrnoException e) {
throw new IllegalStateException("Failed to initialize ingress discard map", e);
}
+
+ if (isAtLeast25Q2()) {
+ if (sLocalNetAccessMap == null) {
+ sLocalNetAccessMap = getLocalNetAccessMap();
+ }
+ try {
+ sLocalNetAccessMap.clear();
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize local_net_access map", e);
+ }
+
+ if (sLocalNetBlockedUidMap == null) {
+ sLocalNetBlockedUidMap = getLocalNetBlockedUidMap();
+ }
+ try {
+ sLocalNetBlockedUidMap.clear();
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize local_net_blocked_uid map",
+ e);
+ }
+ }
}
/**
@@ -387,6 +454,21 @@
}
}
+ private void throwIfPre25Q2(final String msg) {
+ if (!isAtLeast25Q2()) {
+ throw new UnsupportedOperationException(msg);
+ }
+ }
+
+ /*
+ ToDo : Remove this method when SdkLevel.isAtLeastB() is fixed, aosp is at sdk level 36 or use
+ NetworkStackUtils.isAtLeast25Q2 when it is moved to a static lib.
+ */
+ public static boolean isAtLeast25Q2() {
+ return SdkLevel.isAtLeastB() || (SdkLevel.isAtLeastV()
+ && "Baklava".equals(Build.VERSION.CODENAME));
+ }
+
private void removeRule(final int uid, final long match, final String caller) {
try {
synchronized (sUidOwnerMap) {
@@ -788,7 +870,8 @@
}
// Remove the entry if package is uninstalled or uid has only INTERNET permission.
- if (permissions == PERMISSION_UNINSTALLED || permissions == PERMISSION_INTERNET) {
+ if (permissions == TRAFFIC_PERMISSION_UNINSTALLED
+ || permissions == TRAFFIC_PERMISSION_INTERNET) {
for (final int uid : uids) {
try {
sUidPermissionMap.deleteEntry(new S32(uid));
@@ -810,6 +893,158 @@
}
/**
+ * Add configuration to local_net_access trie map.
+ * @param lpmBitlen prefix length that will be used for longest matching
+ * @param iface interface name
+ * @param address remote address. ipv4 addresses would be mapped to v6
+ * @param protocol required for longest match in special cases
+ * @param remotePort src/dst port for ingress/egress
+ * @param isAllowed is the local network call allowed or blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void addLocalNetAccess(final int lpmBitlen, final String iface,
+ final InetAddress address, final int protocol, final int remotePort,
+ final boolean isAllowed) {
+ throwIfPre25Q2("addLocalNetAccess is not available on pre-B devices");
+ final int ifIndex;
+ if (iface == null) {
+ ifIndex = 0;
+ } else {
+ ifIndex = mDeps.getIfIndex(iface);
+ }
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, skip addLocalNetAccess for " + address
+ + "(" + iface + ")");
+ return;
+ }
+ final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+ address, protocol, remotePort);
+
+ try {
+ sLocalNetAccessMap.updateEntry(localNetAccessKey, new Bool(isAllowed));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to add local network access for localNetAccessKey : "
+ + localNetAccessKey + ", isAllowed : " + isAllowed);
+ }
+ }
+
+ /**
+ * Remove configuration to local_net_access trie map.
+ * @param lpmBitlen prefix length that will be used for longest matching
+ * @param iface interface name
+ * @param address remote address. ipv4 addresses would be mapped to v6
+ * @param protocol required for longest match in special cases
+ * @param remotePort src/dst port for ingress/egress
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void removeLocalNetAccess(final int lpmBitlen, final String iface,
+ final InetAddress address, final int protocol, final int remotePort) {
+ throwIfPre25Q2("removeLocalNetAccess is not available on pre-B devices");
+ final int ifIndex;
+ if (iface == null) {
+ ifIndex = 0;
+ } else {
+ ifIndex = mDeps.getIfIndex(iface);
+ }
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, skip removeLocalNetAccess for " + address
+ + "(" + iface + ")");
+ return;
+ }
+ final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+ address, protocol, remotePort);
+
+ try {
+ sLocalNetAccessMap.deleteEntry(localNetAccessKey);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to remove local network access for localNetAccessKey : "
+ + localNetAccessKey);
+ }
+ }
+
+ /**
+ * Fetches value available in local_net_access bpf map for provided configuration
+ * @param lpmBitlen prefix length that will be used for longest matching
+ * @param iface interface name
+ * @param address remote address. ipv4 addresses would be mapped to v6
+ * @param protocol required for longest match in special cases
+ * @param remotePort src/dst port for ingress/egress
+ * @return false if the configuration is disallowed, true if the configuration is absent i.e. it
+ * is not local network or if configuration is allowed like local dns servers.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public boolean getLocalNetAccess(final int lpmBitlen, final String iface,
+ final InetAddress address, final int protocol, final int remotePort) {
+ throwIfPre25Q2("getLocalNetAccess is not available on pre-B devices");
+ final int ifIndex;
+ if (iface == null) {
+ ifIndex = 0;
+ } else {
+ ifIndex = mDeps.getIfIndex(iface);
+ }
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, returning default from getLocalNetAccess for "
+ + address + "(" + iface + ")");
+ return true;
+ }
+ final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+ address, protocol, remotePort);
+ try {
+ final Bool value = sLocalNetAccessMap.getValue(localNetAccessKey);
+ return value == null ? true : value.val;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to find local network access configuration for "
+ + "localNetAccessKey : " + localNetAccessKey);
+ }
+ return true;
+ }
+
+ /**
+ * Add uid to local_net_blocked_uid map.
+ * @param uid application uid that needs to block local network calls.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void addUidToLocalNetBlockMap(final int uid) {
+ throwIfPre25Q2("addUidToLocalNetBlockMap is not available on pre-B devices");
+ try {
+ sLocalNetBlockedUidMap.updateEntry(new U32(uid), new Bool(true));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to add local network blocked for uid : " + uid);
+ }
+ }
+
+ /**
+ * True if local network calls are blocked for application.
+ * @param uid application uid that needs check if local network calls are blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public boolean isUidBlockedFromUsingLocalNetwork(final int uid) {
+ throwIfPre25Q2("isUidBlockedFromUsingLocalNetwork is not available on pre-B devices");
+ try {
+ final Bool value = sLocalNetBlockedUidMap.getValue(new U32(uid));
+ return value == null ? false : value.val;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to find uid(" + uid
+ + ") is present in local network blocked map");
+ }
+ return false;
+ }
+
+ /**
+ * Remove uid from local_net_blocked_uid map(if present).
+ * @param uid application uid that needs check if local network calls are blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void removeUidFromLocalNetBlockMap(final int uid) {
+ throwIfPre25Q2("removeUidFromLocalNetBlockMap is not available on pre-B devices");
+ try {
+ sLocalNetBlockedUidMap.deleteEntry(new U32(uid));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to remove uid(" + uid + ") from local network blocked map");
+ }
+ }
+
+ /**
* Get granted permissions for specified uid. If uid is not in the map, this method returns
* {@link android.net.INetd.PERMISSION_INTERNET} since this is a default permission.
* See {@link #setNetPermForUids}
@@ -824,10 +1059,10 @@
// Key of uid permission map is appId
// TODO: Rename map name
final U8 permissions = sUidPermissionMap.getValue(new S32(appId));
- return permissions != null ? permissions.val : PERMISSION_INTERNET;
+ return permissions != null ? permissions.val : TRAFFIC_PERMISSION_INTERNET;
} catch (ErrnoException e) {
Log.wtf(TAG, "Failed to get permission for uid: " + uid);
- return PERMISSION_INTERNET;
+ return TRAFFIC_PERMISSION_INTERNET;
}
}
@@ -976,7 +1211,7 @@
if (permissionMask == PERMISSION_NONE) {
return "PERMISSION_NONE";
}
- if (permissionMask == PERMISSION_UNINSTALLED) {
+ if (permissionMask == TRAFFIC_PERMISSION_UNINSTALLED) {
// PERMISSION_UNINSTALLED should never appear in the map
return "PERMISSION_UNINSTALLED error!";
}
@@ -1079,6 +1314,14 @@
(key, value) -> "[" + key.dstAddr + "]: "
+ value.iif1 + "(" + mDeps.getIfName(value.iif1) + "), "
+ value.iif2 + "(" + mDeps.getIfName(value.iif2) + ")");
+ if (sLocalNetBlockedUidMap != null) {
+ BpfDump.dumpMap(sLocalNetAccessMap, pw, "sLocalNetAccessMap",
+ (key, value) -> "[" + key + "]: " + value);
+ }
+ if (sLocalNetBlockedUidMap != null) {
+ BpfDump.dumpMap(sLocalNetBlockedUidMap, pw, "sLocalNetBlockedUidMap",
+ (key, value) -> "[" + key + "]: " + value);
+ }
dumpDataSaverConfig(pw);
pw.decreaseIndent();
}
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 e503312..2a66e4b
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -48,6 +48,7 @@
import static android.net.ConnectivityManager.CALLBACK_LOSING;
import static android.net.ConnectivityManager.CALLBACK_LOST;
import static android.net.ConnectivityManager.CALLBACK_PRECHECK;
+import static android.net.ConnectivityManager.CALLBACK_RESERVED;
import static android.net.ConnectivityManager.CALLBACK_RESUMED;
import static android.net.ConnectivityManager.CALLBACK_SUSPENDED;
import static android.net.ConnectivityManager.CALLBACK_UNAVAIL;
@@ -108,6 +109,8 @@
import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
+import static android.net.NetworkCapabilities.RES_ID_UNSET;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -121,6 +124,9 @@
import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
import static android.os.Process.INVALID_UID;
import static android.os.Process.VPN_UID;
+import static android.system.OsConstants.ENOENT;
+import static android.system.OsConstants.ENOTCONN;
+import static android.system.OsConstants.EOPNOTSUPP;
import static android.system.OsConstants.ETH_P_ALL;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
@@ -140,15 +146,20 @@
import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_RECVMSG;
import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_SENDMSG;
import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_LOCAL_PREFIXES;
+import static com.android.net.module.util.NetworkStackConstants.MULTICAST_AND_BROADCAST_PREFIXES;
import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
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.NAMESPACE_TETHERING_BOOT;
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.WIFI_DATA_INACTIVITY_TIMEOUT;
import android.Manifest;
import android.annotation.CheckResult;
@@ -192,6 +203,7 @@
import android.net.IConnectivityDiagnosticsCallback;
import android.net.IConnectivityManager;
import android.net.IDnsResolver;
+import android.net.IIntResultListener;
import android.net.INetd;
import android.net.INetworkActivityListener;
import android.net.INetworkAgent;
@@ -204,6 +216,7 @@
import android.net.InetAddresses;
import android.net.IpMemoryStore;
import android.net.IpPrefix;
+import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.LocalNetworkConfig;
import android.net.LocalNetworkInfo;
@@ -410,14 +423,14 @@
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
* @hide
*/
-public class ConnectivityService extends IConnectivityManager.Stub
- implements PendingIntent.OnFinished {
+public class ConnectivityService extends IConnectivityManager.Stub {
private static final String TAG = ConnectivityService.class.getSimpleName();
private static final String DIAG_ARG = "--diag";
@@ -551,6 +564,7 @@
// The Context is created for UserHandle.ALL.
private final Context mUserAllContext;
private final Dependencies mDeps;
+ private final PermissionMonitor.Dependencies mPermissionMonitorDeps;
private final ConnectivityFlags mFlags;
// 0 is full bad, 100 is full good
private int mDefaultInetConditionPublished = 0;
@@ -1010,6 +1024,8 @@
private final LingerMonitor mLingerMonitor;
private final SatelliteAccessController mSatelliteAccessController;
+ private final L2capNetworkProvider mL2capNetworkProvider;
+
// sequence number of NetworkRequests
private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID;
@@ -1610,6 +1626,23 @@
connectivityServiceInternalHandler);
}
+ /** Creates an L2capNetworkProvider */
+ public L2capNetworkProvider makeL2capNetworkProvider(Context context) {
+ return new L2capNetworkProvider(context);
+ }
+
+ /** Returns the data inactivity timeout to be used for cellular networks */
+ public int getDefaultCellularDataInactivityTimeout() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING_BOOT,
+ CELLULAR_DATA_INACTIVITY_TIMEOUT, 10);
+ }
+
+ /** Returns the data inactivity timeout to be used for WiFi networks */
+ public int getDefaultWifiDataInactivityTimeout() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING_BOOT,
+ WIFI_DATA_INACTIVITY_TIMEOUT, 15);
+ }
+
/**
* @see DeviceConfigUtils#isTetheringFeatureEnabled
*/
@@ -1776,12 +1809,13 @@
public ConnectivityService(Context context) {
this(context, getDnsResolver(context), new IpConnectivityLog(),
INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
- new Dependencies());
+ new Dependencies(), new PermissionMonitor.Dependencies());
}
@VisibleForTesting
protected ConnectivityService(Context context, IDnsResolver dnsresolver,
- IpConnectivityLog logger, INetd netd, Dependencies deps) {
+ IpConnectivityLog logger, INetd netd, Dependencies deps,
+ PermissionMonitor.Dependencies mPermDeps) {
if (DBG) log("ConnectivityService starting up");
mDeps = Objects.requireNonNull(deps, "missing Dependencies");
@@ -1854,8 +1888,10 @@
mNetd = netd;
mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd);
mHandlerThread = mDeps.makeHandlerThread("ConnectivityServiceThread");
+ mPermissionMonitorDeps = mPermDeps;
mPermissionMonitor =
- new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mHandlerThread);
+ new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mPermissionMonitorDeps,
+ mHandlerThread);
mHandlerThread.start();
mHandler = new InternalHandler(mHandlerThread.getLooper());
mTrackerHandler = new NetworkStateTrackerHandler(mHandlerThread.getLooper());
@@ -1877,11 +1913,12 @@
&& mDeps.isFeatureEnabled(context, REQUEST_RESTRICTED_WIFI);
mBackgroundFirewallChainEnabled = mDeps.isAtLeastV() && mDeps.isFeatureNotChickenedOut(
context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN);
- mUseDeclaredMethodsForCallbacksEnabled = mDeps.isFeatureEnabled(context,
- ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
+ mUseDeclaredMethodsForCallbacksEnabled =
+ mDeps.isFeatureNotChickenedOut(context,
+ ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
// registerUidFrozenStateChangedCallback is only available on U+
mQueueCallbacksForFrozenApps = mDeps.isAtLeastU()
- && mDeps.isFeatureEnabled(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
+ && mDeps.isFeatureNotChickenedOut(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
this::handleUidCarrierPrivilegesLost, mHandler);
@@ -1958,8 +1995,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 {
@@ -2064,6 +2106,8 @@
}
mIngressToVpnAddressFiltering = mDeps.isAtLeastT()
&& mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
+
+ mL2capNetworkProvider = mDeps.makeL2capNetworkProvider(mContext);
}
/**
@@ -4099,6 +4143,10 @@
mCarrierPrivilegeAuthenticator.start();
}
+ if (mL2capNetworkProvider != null) {
+ mL2capNetworkProvider.start();
+ }
+
// On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
if (mDeps.isAtLeastT()) {
mBpfNetMaps.setPullAtomCallback(mContext);
@@ -5531,7 +5579,7 @@
}
// Delayed teardown.
- if (nai.isCreated()) {
+ if (nai.isCreated() && !nai.isDestroyed()) {
try {
mNetd.networkSetPermissionForNetwork(nai.network.netId, INetd.PERMISSION_SYSTEM);
} catch (RemoteException e) {
@@ -5656,6 +5704,7 @@
// destroyed pending replacement they will be sent when it is disconnected.
maybeDisableForwardRulesForDisconnectingNai(nai, false /* sendCallbacks */);
updateIngressToVpnAddressFiltering(null, nai.linkProperties, nai);
+ updateLocalNetworkAddresses(null, nai.linkProperties);
try {
mNetd.networkDestroy(nai.network.getNetId());
} catch (RemoteException | ServiceSpecificException e) {
@@ -6320,8 +6369,20 @@
}
}
- private class CaptivePortalImpl extends ICaptivePortal.Stub {
+ public class CaptivePortalImpl extends ICaptivePortal.Stub implements IBinder.DeathRecipient {
private final Network mNetwork;
+ // Binder object to track the lifetime of the setDelegateUid caller for cleanup purposes.
+ //
+ // Note that in theory it can happen that there are multiple callers for a given
+ // object. For example, the app that receives the CaptivePortal object from the Intent
+ // fired by startCaptivePortalAppInternal could send the object to another process, or
+ // clone it. Only the first of these objects that calls setDelegateUid will properly
+ // register a death recipient. Calls from the other objects will work, but only the
+ // first object's death will cause the death recipient to fire.
+ // TODO: track all callers by callerBinder instead of CaptivePortalImpl, store callerBinder
+ // in a Set. When the death recipient fires, we can remove the callingBinder from the set,
+ // and when the set is empty, we can clear the delegated UID.
+ private IBinder mDelegateUidCaller;
private CaptivePortalImpl(Network network) {
mNetwork = network;
@@ -6361,6 +6422,55 @@
}
}
+ private int handleSetDelegateUid(int uid, @NonNull final IBinder callerBinder) {
+ if (mDelegateUidCaller == null) {
+ mDelegateUidCaller = callerBinder;
+ try {
+ // While technically unnecessary, it is safe to register a DeathRecipient for
+ // a cleanup operation (where uid = INVALID_UID).
+ mDelegateUidCaller.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ // remote has died, return early.
+ return ENOTCONN;
+ }
+ }
+
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(mNetwork);
+ if (nai == null) return ENOENT; // network does not exist anymore.
+ if (nai.isDestroyed()) return ENOENT; // network has already been destroyed.
+
+ // TODO: consider allowing the uid to bypass VPN on all networks before V.
+ if (!mDeps.isAtLeastV()) return EOPNOTSUPP;
+
+ // Check whether there has already been a delegate UID configured, if so, perform
+ // cleanup and disallow bypassing VPN for that UID if no other caller is delegating
+ // this UID.
+ // TODO: consider using exceptions instead of errnos.
+ final int errno = nai.removeCaptivePortalDelegateUid(this);
+ if (errno != 0) return errno;
+
+ // If uid == INVALID_UID, we are done.
+ if (uid == INVALID_UID) return 0;
+ return nai.setCaptivePortalDelegateUid(this, uid);
+ }
+
+ @Override
+ public void setDelegateUid(int uid, @NonNull final IBinder callerBinder,
+ @NonNull final IIntResultListener listener) {
+ Objects.requireNonNull(callerBinder);
+ Objects.requireNonNull(listener);
+ enforceAnyPermissionOf(mContext, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+
+ mHandler.post(() -> {
+ final int errno = handleSetDelegateUid(uid, callerBinder);
+ try {
+ listener.onResult(errno);
+ } catch (RemoteException e) {
+ // remote has died, nothing to do.
+ }
+ });
+ }
+
@Nullable
private NetworkMonitorManager getNetworkMonitorManager(final Network network) {
// getNetworkAgentInfoForNetwork is thread-safe
@@ -6370,6 +6480,13 @@
// nai.networkMonitor() is thread-safe
return nai.networkMonitor();
}
+
+ @Override
+ public void binderDied() {
+ // Cleanup invalid UID and restore the VPN bypass rule. Because mDelegateUidCaller is
+ // never reset, it cannot be null in this context.
+ mHandler.post(() -> handleSetDelegateUid(INVALID_UID, mDelegateUidCaller));
+ }
}
public boolean avoidBadWifi() {
@@ -6671,7 +6788,7 @@
final NetworkOfferInfo offer =
findNetworkOfferInfoByCallback((INetworkOfferCallback) msg.obj);
if (null != offer) {
- handleUnregisterNetworkOffer(offer);
+ handleUnregisterNetworkOffer(offer, true /* releaseReservations */);
}
break;
}
@@ -7588,17 +7705,23 @@
}
}
- private void ensureAllNetworkRequestsHaveType(List<NetworkRequest> requests) {
+ private void ensureAllNetworkRequestsHaveSupportedType(List<NetworkRequest> requests) {
+ final boolean isMultilayerRequest = requests.size() > 1;
for (int i = 0; i < requests.size(); i++) {
- ensureNetworkRequestHasType(requests.get(i));
+ ensureNetworkRequestHasSupportedType(requests.get(i), isMultilayerRequest);
}
}
- private void ensureNetworkRequestHasType(NetworkRequest request) {
+ private void ensureNetworkRequestHasSupportedType(NetworkRequest request,
+ boolean isMultilayerRequest) {
if (request.type == NetworkRequest.Type.NONE) {
throw new IllegalArgumentException(
"All NetworkRequests in ConnectivityService must have a type");
}
+ if (isMultilayerRequest && request.type == NetworkRequest.Type.RESERVATION) {
+ throw new IllegalArgumentException(
+ "Reservation requests are not supported in multilayer request");
+ }
}
/**
@@ -7709,6 +7832,28 @@
}
/**
+ * NetworkCapabilities that were created as part of a NetworkOffer in response to a
+ * RESERVATION request. mReservedCapabilities is null if no current offer matches the
+ * RESERVATION request or if the request is not a RESERVATION. Matching is based on
+ * reservationId.
+ */
+ @Nullable
+ private NetworkCapabilities mReservedCapabilities;
+ @Nullable
+ NetworkCapabilities getReservedCapabilities() {
+ return mReservedCapabilities;
+ }
+
+ void setReservedCapabilities(@NonNull NetworkCapabilities caps) {
+ // This function can only be called once. NetworkCapabilities are never reset as the
+ // reservation is released when the offer disappears.
+ if (mReservedCapabilities != null) {
+ logwtf("ReservedCapabilities can only be set once");
+ }
+ mReservedCapabilities = caps;
+ }
+
+ /**
* Get the list of UIDs this nri applies to.
*/
@NonNull
@@ -7728,7 +7873,7 @@
NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
@NonNull final NetworkRequest requestForCallback, @Nullable final PendingIntent pi,
@Nullable String callingAttributionTag, final int preferenceOrder) {
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = requestForCallback;
mPendingIntent = pi;
@@ -7762,7 +7907,7 @@
@NetworkCallback.Flag int callbackFlags,
@Nullable String callingAttributionTag, int declaredMethodsFlags) {
super();
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = requestForCallback;
mMessenger = m;
@@ -7782,7 +7927,7 @@
NetworkRequestInfo(@NonNull final NetworkRequestInfo nri,
@NonNull final List<NetworkRequest> r) {
super();
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = nri.getNetworkRequestForCallback();
final NetworkAgentInfo satisfier = nri.getSatisfier();
@@ -8068,6 +8213,14 @@
return PREFERENCE_ORDER_NONE;
}
+ public int getReservationId() {
+ // RESERVATIONs cannot be used in multilayer requests.
+ if (isMultilayerRequest()) return RES_ID_UNSET;
+ final NetworkRequest req = mRequests.get(0);
+ // Non-reservation types return RES_ID_UNSET.
+ return req.networkCapabilities.getReservationId();
+ }
+
@Override
public void binderDied() {
// As an immutable collection, mRequests cannot change by the time the
@@ -8119,6 +8272,7 @@
flags = maybeAppendDeclaredMethod(flags, CALLBACK_BLK_CHANGED, "BLK", sb);
flags = maybeAppendDeclaredMethod(flags, CALLBACK_LOCAL_NETWORK_INFO_CHANGED,
"LOCALINF", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_RESERVED, "RES", sb);
if (flags != 0) {
sb.append("|0x").append(Integer.toHexString(flags));
}
@@ -8323,6 +8477,7 @@
enforceNetworkStackOrSettingsPermission();
// Fall-through since other checks are the same with normal requests.
case REQUEST:
+ case RESERVATION:
networkCapabilities = new NetworkCapabilities(networkCapabilities);
enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
callingAttributionTag, callingUid);
@@ -8761,7 +8916,7 @@
@Override
public void releaseNetworkRequest(NetworkRequest networkRequest) {
- ensureNetworkRequestHasType(networkRequest);
+ ensureNetworkRequestHasSupportedType(networkRequest, false /* isMultilayerRequest */);
mHandler.sendMessage(mHandler.obtainMessage(
EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
}
@@ -8804,6 +8959,11 @@
Objects.requireNonNull(score);
Objects.requireNonNull(caps);
Objects.requireNonNull(callback);
+ if (caps.hasTransport(TRANSPORT_TEST)) {
+ enforceAnyPermissionOf(mContext, Manifest.permission.MANAGE_TEST_NETWORKS);
+ } else {
+ enforceNetworkFactoryPermission();
+ }
final boolean yieldToBadWiFi = caps.hasTransport(TRANSPORT_CELLULAR) && !avoidBadWifi();
final NetworkOffer offer = new NetworkOffer(
FullScore.makeProspectiveScore(score, caps, yieldToBadWiFi),
@@ -8842,7 +9002,7 @@
}
}
for (final NetworkOfferInfo noi : toRemove) {
- handleUnregisterNetworkOffer(noi);
+ handleUnregisterNetworkOffer(noi, true /* releaseReservations */);
}
if (DBG) log("unregisterNetworkProvider for " + npi.name);
}
@@ -9275,7 +9435,7 @@
@Override
public void binderDied() {
- mHandler.post(() -> handleUnregisterNetworkOffer(this));
+ mHandler.post(() -> handleUnregisterNetworkOffer(this, true /* releaseReservations */));
}
}
@@ -9286,6 +9446,18 @@
return false;
}
+ @Nullable
+ private NetworkRequestInfo maybeGetNriForReservedOffer(NetworkOfferInfo noi) {
+ final int reservationId = noi.offer.caps.getReservationId();
+ if (reservationId == RES_ID_UNSET) return null; // not a reserved offer.
+
+ for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (reservationId == nri.getReservationId()) return nri;
+ }
+ // The reservation was withdrawn or the reserving process died.
+ return null;
+ }
+
/**
* Register or update a network offer.
* @param newOffer The new offer. If the callback member is the same as an existing
@@ -9302,19 +9474,62 @@
return;
}
final NetworkOfferInfo existingOffer = findNetworkOfferInfoByCallback(newOffer.callback);
+
+ // If a reserved offer is updated, ensure the capabilities are not changed. This ensures
+ // that the reserved offer's capabilities match the ones passed by the onReserved callback,
+ // which is sent only once.
+ //
+ // TODO: consider letting the provider change the capabilities of an offer as long as they
+ // continue to satisfy the capabilities that were passed to onReserved. This is not needed
+ // today, but it shouldn't violate the API contract:
+ // - NetworkOffer capabilities are not promises
+ // - The app making a reservation must never assume that the capabilities of the reserved
+ // network are equal to the ones that were passed to onReserved. There will almost always be
+ // other capabilities, for example, those that change at runtime such as VALIDATED or
+ // NOT_SUSPENDED.
+ if (null != existingOffer
+ && existingOffer.offer.caps.getReservationId() != RES_ID_UNSET
+ && existingOffer.offer.caps.getReservationId() != RES_ID_MATCH_ALL_RESERVATIONS
+ && !newOffer.caps.equals(existingOffer.offer.caps)) {
+ // Reserved offers are not allowed to update their NetworkCapabilities.
+ // Doing so will immediately remove the offer from CS and send onUnavailable to the app.
+ handleUnregisterNetworkOffer(existingOffer, true /* releaseReservations */);
+ existingOffer.offer.notifyUnneeded();
+ logwtf("Reserved offers must never update their reserved NetworkCapabilities");
+ return;
+ }
+
+ final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
if (null != existingOffer) {
- handleUnregisterNetworkOffer(existingOffer);
+ // Do not send onUnavailable for a reserved offer when updating it.
+ handleUnregisterNetworkOffer(existingOffer, false /* releaseReservations */);
newOffer.migrateFrom(existingOffer.offer);
if (DBG) {
// handleUnregisterNetworkOffer has already logged the old offer
log("update offer from providerId " + newOffer.providerId + " new : " + newOffer);
}
} else {
+ final NetworkRequestInfo reservationNri = maybeGetNriForReservedOffer(noi);
+ if (reservationNri != null) {
+ // A NetworkRequest is only allowed to trigger a single reserved offer (and
+ // onReserved() callback). All subsequent offers are ignored. This either indicates
+ // a bug in the provider (e.g., responding twice to the same reservation, or
+ // updating the capabilities of a reserved offer), or multiple providers responding
+ // to the same offer (which could happen, but is not useful to the requesting app).
+ if (reservationNri.getReservedCapabilities() != null) {
+ loge("A reservation can only trigger a single offer; new offer is ignored.");
+ return;
+ }
+ // Always update the reserved offer before calling callCallbackForRequest.
+ reservationNri.setReservedCapabilities(noi.offer.caps);
+ callCallbackForRequest(
+ reservationNri, null /*networkAgent*/, CALLBACK_RESERVED, 0 /*arg1*/);
+ }
if (DBG) {
log("register offer from providerId " + newOffer.providerId + " : " + newOffer);
}
}
- final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
+
try {
noi.offer.callback.asBinder().linkToDeath(noi, 0 /* flags */);
} catch (RemoteException e) {
@@ -9325,7 +9540,8 @@
issueNetworkNeeds(noi);
}
- private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi) {
+ private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi,
+ boolean releaseReservations) {
ensureRunningOnConnectivityServiceThread();
if (DBG) {
log("unregister offer from providerId " + noi.offer.providerId + " : " + noi.offer);
@@ -9335,6 +9551,18 @@
// function may be called twice in a row, but the array will no longer contain
// the offer.
if (!mNetworkOffers.remove(noi)) return;
+
+ // If the offer was brought up as a result of a reservation, inform the RESERVATION request
+ // that it has disappeared. There is no need to reset nri.mReservedCapabilities to null, as
+ // CALLBACK_UNAVAIL will cause the request to be torn down. In addition, leaving
+ // nri.mReservedOffer set prevents an additional onReserved() callback in
+ // handleRegisterNetworkOffer() in the case of a migration (which would be ignored as it
+ // follows an onUnavailable).
+ final NetworkRequestInfo nri = maybeGetNriForReservedOffer(noi);
+ if (releaseReservations && nri != null) {
+ handleRemoveNetworkRequest(nri);
+ callCallbackForRequest(nri, null /* networkAgent */, CALLBACK_UNAVAIL, 0 /* arg1 */);
+ }
noi.offer.callback.asBinder().unlinkToDeath(noi, 0 /* flags */);
}
@@ -9377,6 +9605,8 @@
updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
+ updateLocalNetworkAddresses(newLp, oldLp);
+
updateMtu(newLp, oldLp);
// TODO - figure out what to do for clat
// for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -9556,6 +9786,219 @@
}
/**
+ * Update Local Network Addresses to LocalNetAccess BPF map.
+ * @param newLp new link properties
+ * @param oldLp old link properties
+ */
+ private void updateLocalNetworkAddresses(@Nullable final LinkProperties newLp,
+ @NonNull final LinkProperties oldLp) {
+
+ // The maps are available only after 25Q2 release
+ if (!BpfNetMaps.isAtLeast25Q2()) {
+ return;
+ }
+
+ final CompareResult<String> interfaceDiff = new CompareResult<>(
+ oldLp != null ? oldLp.getAllInterfaceNames() : null,
+ newLp != null ? newLp.getAllInterfaceNames() : null);
+
+ for (final String iface : interfaceDiff.added) {
+ addLocalAddressesToBpfMap(iface, MULTICAST_AND_BROADCAST_PREFIXES, newLp);
+ }
+ for (final String iface : interfaceDiff.removed) {
+ removeLocalAddressesFromBpfMap(iface, MULTICAST_AND_BROADCAST_PREFIXES, oldLp);
+ }
+
+ // The both list contain current link properties + stacked links for new and old LP.
+ List<LinkProperties> newLinkProperties = new ArrayList<>();
+ List<LinkProperties> oldLinkProperties = new ArrayList<>();
+
+ if (newLp != null) {
+ newLinkProperties.add(newLp);
+ newLinkProperties.addAll(newLp.getStackedLinks());
+ }
+ if (oldLp != null) {
+ oldLinkProperties.add(oldLp);
+ oldLinkProperties.addAll(oldLp.getStackedLinks());
+ }
+
+ // map contains interface name to list of local network prefixes added because of change
+ // in link properties
+ Map<String, List<IpPrefix>> prefixesAddedForInterface = new ArrayMap<>();
+
+ final CompareResult<LinkProperties> linkPropertiesDiff = new CompareResult<>(
+ oldLinkProperties, newLinkProperties);
+
+ for (LinkProperties linkProperty : linkPropertiesDiff.added) {
+ List<IpPrefix> unicastLocalPrefixesToBeAdded = new ArrayList<>();
+ for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
+ unicastLocalPrefixesToBeAdded.addAll(
+ getLocalNetworkPrefixesForAddress(linkAddress));
+ }
+ addLocalAddressesToBpfMap(linkProperty.getInterfaceName(),
+ unicastLocalPrefixesToBeAdded, linkProperty);
+
+ // adding iterface name -> ip prefixes that we added to map
+ if (!prefixesAddedForInterface.containsKey(linkProperty.getInterfaceName())) {
+ prefixesAddedForInterface.put(linkProperty.getInterfaceName(), new ArrayList<>());
+ }
+ prefixesAddedForInterface.get(linkProperty.getInterfaceName())
+ .addAll(unicastLocalPrefixesToBeAdded);
+ }
+
+ for (LinkProperties linkProperty : linkPropertiesDiff.removed) {
+ List<IpPrefix> unicastLocalPrefixesToBeRemoved = new ArrayList<>();
+ List<IpPrefix> unicastLocalPrefixesAdded = prefixesAddedForInterface.getOrDefault(
+ linkProperty.getInterfaceName(), new ArrayList<>());
+
+ for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
+ unicastLocalPrefixesToBeRemoved.addAll(
+ getLocalNetworkPrefixesForAddress(linkAddress));
+ }
+
+ // This is to ensure if 10.0.10.0/24 was added and 10.0.11.0/24 was removed both will
+ // still populate the same prefix of 10.0.0.0/8, which mean we should not remove the
+ // prefix because of removal of 10.0.11.0/24
+ unicastLocalPrefixesToBeRemoved.removeAll(unicastLocalPrefixesAdded);
+
+ removeLocalAddressesFromBpfMap(linkProperty.getInterfaceName(),
+ new ArrayList<>(unicastLocalPrefixesToBeRemoved), linkProperty);
+ }
+ }
+
+ /**
+ * Filters IpPrefix that are local prefixes and LinkAddress is part of them.
+ * @param linkAddress link address used for filtering
+ * @return list of IpPrefix that are local addresses.
+ */
+ private List<IpPrefix> getLocalNetworkPrefixesForAddress(LinkAddress linkAddress) {
+ List<IpPrefix> localPrefixes = new ArrayList<>();
+ if (linkAddress.isIpv6()) {
+ // For IPv6, if the prefix length is greater than zero then they are part of local
+ // network
+ if (linkAddress.getPrefixLength() != 0) {
+ localPrefixes.add(
+ new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()));
+ }
+ } else {
+ // For IPv4, if the linkAddress is part of IpPrefix adding prefix to result.
+ for (IpPrefix ipv4LocalPrefix : IPV4_LOCAL_PREFIXES) {
+ if (ipv4LocalPrefix.containsPrefix(
+ new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()))) {
+ localPrefixes.add(ipv4LocalPrefix);
+ }
+ }
+ }
+ return localPrefixes;
+ }
+
+ /**
+ * Adds list of prefixes(addresses) to local network access map.
+ * @param iface interface name
+ * @param prefixes list of prefixes/addresses
+ * @param lp LinkProperties
+ */
+ private void addLocalAddressesToBpfMap(final String iface, final List<IpPrefix> prefixes,
+ @Nullable final LinkProperties lp) {
+ if (!BpfNetMaps.isAtLeast25Q2()) return;
+
+ for (IpPrefix prefix : prefixes) {
+ // Add local dnses allow rule To BpfMap before adding the block rule for prefix
+ addLocalDnsesToBpfMap(iface, prefix, lp);
+ /*
+ Prefix length is used by LPM trie map(local_net_access_map) for performing longest
+ prefix matching, this length represents the maximum number of bits used for matching.
+ The interface index should always be matched which is 32-bit integer. For IPv6, prefix
+ length is calculated by adding the ip address prefix length along with interface index
+ making it (32 + length). IPv4 addresses are stored as ipv4-mapped-ipv6 which implies
+ first 96 bits are common for all ipv4 addresses. Hence, prefix length is calculated as
+ 32(interface index) + 96 (common for ipv4-mapped-ipv6) + length.
+ */
+ final int prefixLengthConstant = (prefix.isIPv4() ? (32 + 96) : 32);
+ mBpfNetMaps.addLocalNetAccess(prefixLengthConstant + prefix.getPrefixLength(),
+ iface, prefix.getAddress(), 0, 0, false);
+
+ }
+
+ }
+
+ /**
+ * Removes list of prefixes(addresses) from local network access map.
+ * @param iface interface name
+ * @param prefixes list of prefixes/addresses
+ * @param lp LinkProperties
+ */
+ private void removeLocalAddressesFromBpfMap(final String iface, final List<IpPrefix> prefixes,
+ @Nullable final LinkProperties lp) {
+ if (!BpfNetMaps.isAtLeast25Q2()) return;
+
+ for (IpPrefix prefix : prefixes) {
+ // The reasoning for prefix length is explained in addLocalAddressesToBpfMap()
+ final int prefixLengthConstant = (prefix.isIPv4() ? (32 + 96) : 32);
+ mBpfNetMaps.removeLocalNetAccess(prefixLengthConstant
+ + prefix.getPrefixLength(), iface, prefix.getAddress(), 0, 0);
+
+ // Also remove the allow rule for dnses included in the prefix after removing the block
+ // rule for prefix.
+ removeLocalDnsesFromBpfMap(iface, prefix, lp);
+ }
+ }
+
+ /**
+ * Adds DNS servers to local network access map, if included in the interface prefix
+ * @param iface interface name
+ * @param prefix IpPrefix
+ * @param lp LinkProperties
+ */
+ private void addLocalDnsesToBpfMap(final String iface, IpPrefix prefix,
+ @Nullable final LinkProperties lp) {
+ if (!BpfNetMaps.isAtLeast25Q2() || lp == null) return;
+
+ for (InetAddress dnsServer : lp.getDnsServers()) {
+ // Adds dns allow rule to LocalNetAccessMap for both TCP and UDP protocol at port 53,
+ // if it is a local dns (ie. it falls in the local prefix range).
+ if (prefix.contains(dnsServer)) {
+ mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+ IPPROTO_UDP, 53, true);
+ mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+ IPPROTO_TCP, 53, true);
+ }
+ }
+ }
+
+ /**
+ * Removes DNS servers from local network access map, if included in the interface prefix
+ * @param iface interface name
+ * @param prefix IpPrefix
+ * @param lp LinkProperties
+ */
+ private void removeLocalDnsesFromBpfMap(final String iface, IpPrefix prefix,
+ @Nullable final LinkProperties lp) {
+ if (!BpfNetMaps.isAtLeast25Q2() || lp == null) return;
+
+ for (InetAddress dnsServer : lp.getDnsServers()) {
+ // Removes dns allow rule from LocalNetAccessMap for both TCP and UDP protocol
+ // at port 53, if it is a local dns (ie. it falls in the prefix range).
+ if (prefix.contains(dnsServer)) {
+ mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+ IPPROTO_UDP, 53);
+ mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+ IPPROTO_TCP, 53);
+ }
+ }
+ }
+
+ /**
+ * Returns total bit length of an Ipv4 mapped address.
+ */
+ private int getIpv4MappedAddressBitLen() {
+ final int ifaceLen = 32; // bit length of interface
+ final int inetAddressLen = 32 + 96; // length of ipv4 mapped addresses
+ final int portProtocolLen = 32; //16 for port + 16 for protocol;
+ return ifaceLen + inetAddressLen + portProtocolLen;
+ }
+
+ /**
* Have netd update routes from oldLp to newLp.
* @return true if routes changed between oldLp and newLp
*/
@@ -10493,10 +10936,42 @@
// else not handled
}
+ /**
+ * A small class to manage releasing a lock exactly once even if releaseLock is called
+ * multiple times. See b/390043283
+ * PendingIntent#send throws CanceledException in various cases. In some of them it will
+ * still call onSendFinished, in others it won't and the client can't know. This class
+ * keeps a ref to the wakelock that it releases exactly once, thanks to Atomics semantics.
+ */
+ private class WakeLockOnFinishedReceiver implements PendingIntent.OnFinished {
+ private final AtomicReference<PowerManager.WakeLock> mLock;
+ WakeLockOnFinishedReceiver(@NonNull final PowerManager.WakeLock lock) {
+ mLock = new AtomicReference<>(lock);
+ lock.acquire();
+ }
+
+ public void releaseLock() {
+ final PowerManager.WakeLock lock = mLock.getAndSet(null);
+ if (null != lock) lock.release();
+ }
+
+ @Override
+ public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
+ String resultData, Bundle resultExtras) {
+ if (DBG) log("Finished sending " + pendingIntent);
+ releaseLock();
+ releasePendingNetworkRequestWithDelay(pendingIntent);
+ }
+ }
+
// TODO(b/193460475): Remove when tooling supports SystemApi to public API.
@SuppressLint("NewApi")
private void sendIntent(PendingIntent pendingIntent, Intent intent) {
- mPendingIntentWakeLock.acquire();
+ // Since the receiver will take the lock exactly once and release it exactly once, it
+ // is safe to pass the same wakelock to all receivers and avoid creating a new lock
+ // every time.
+ final WakeLockOnFinishedReceiver receiver =
+ new WakeLockOnFinishedReceiver(mPendingIntentWakeLock);
try {
if (DBG) log("Sending " + pendingIntent);
final BroadcastOptions options = BroadcastOptions.makeBasic();
@@ -10505,25 +10980,14 @@
// utilizing the PendingIntent as a backdoor to do this.
options.setPendingIntentBackgroundActivityLaunchAllowed(false);
}
- pendingIntent.send(mContext, 0, intent, this /* onFinished */, null /* Handler */,
+ pendingIntent.send(mContext, 0, intent, receiver, null /* Handler */,
null /* requiredPermission */,
mDeps.isAtLeastT() ? options.toBundle() : null);
} catch (PendingIntent.CanceledException e) {
if (DBG) log(pendingIntent + " was not sent, it had been canceled.");
- mPendingIntentWakeLock.release();
+ receiver.releaseLock();
releasePendingNetworkRequest(pendingIntent);
}
- // ...otherwise, mPendingIntentWakeLock.release() gets called by onSendFinished()
- }
-
- @Override
- public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
- String resultData, Bundle resultExtras) {
- if (DBG) log("Finished sending " + pendingIntent);
- mPendingIntentWakeLock.release();
- // Release with a delay so the receiving client has an opportunity to put in its
- // own request.
- releasePendingNetworkRequestWithDelay(pendingIntent);
}
@Nullable
@@ -10553,9 +11017,9 @@
return bundle;
}
- // networkAgent is only allowed to be null if notificationType is
- // CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being
- // available, while all other cases are about some particular network.
+ // networkAgent is only allowed to be null if notificationType is CALLBACK_UNAVAIL or
+ // CALLBACK_RESERVED. This is because, per definition, no network is available for UNAVAIL, and
+ // RESERVED callbacks happen when a NetworkOffer is created in response to a reservation.
private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri,
@Nullable final NetworkAgentInfo networkAgent, final int notificationType,
final int arg1) {
@@ -10567,6 +11031,10 @@
}
// 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.
+ // UNAVAIL and RESERVED callbacks are safe not to be queued, because RESERVED must always be
+ // the first callback. In addition, RESERVED cannot be sent more than once and is only
+ // cancelled by UNVAIL.
+ // TODO: evaluate whether it makes sense to queue RESERVED callbacks.
if (networkAgent != null && nri.maybeQueueCallback(networkAgent, notificationType)) {
return;
}
@@ -10574,14 +11042,24 @@
// No need to send the notification as the recipient method is not overridden
return;
}
- final Network bundleNetwork = notificationType == CALLBACK_UNAVAIL
- ? null
- : networkAgent.network;
+ // networkAgent is only null for UNAVAIL and RESERVED.
+ final Network bundleNetwork = (networkAgent != null) ? networkAgent.network : null;
final Bundle bundle = makeCommonBundleForCallback(nri, bundleNetwork);
final boolean includeLocationSensitiveInfo =
(nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0;
final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
switch (notificationType) {
+ case CALLBACK_RESERVED: {
+ final NetworkCapabilities nc =
+ createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+ networkCapabilitiesRestrictedForCallerPermissions(
+ nri.getReservedCapabilities(), nri.mPid, nri.mUid),
+ includeLocationSensitiveInfo, nri.mPid, nri.mUid,
+ nrForCallback.getRequestorPackageName(),
+ nri.mCallingAttributionTag);
+ putParcelable(bundle, nc);
+ break;
+ }
case CALLBACK_AVAILABLE: {
final NetworkCapabilities nc =
createWithLocationInfoSanitizedIfNecessaryWhenParceled(
@@ -13027,6 +13505,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
@@ -13043,11 +13523,14 @@
}
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() {
@@ -13247,13 +13730,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
@@ -13377,6 +13860,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:");
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
new file mode 100644
index 0000000..0352ad5
--- /dev/null
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE;
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY;
+import static android.net.L2capNetworkSpecifier.ROLE_CLIENT;
+import static android.net.L2capNetworkSpecifier.ROLE_SERVER;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.system.OsConstants.F_GETFL;
+import static android.system.OsConstants.F_SETFL;
+import static android.system.OsConstants.O_NONBLOCK;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.L2capNetworkSpecifier;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkProvider.NetworkOfferCallback;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.ParcelFileDescriptor;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HandlerUtils;
+import com.android.net.module.util.ServiceConnectivityJni;
+import com.android.server.net.L2capNetwork;
+import com.android.server.net.L2capNetwork.L2capIpClient;
+import com.android.server.net.L2capPacketForwarder;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+
+public class L2capNetworkProvider {
+ private static final String TAG = L2capNetworkProvider.class.getSimpleName();
+ private static final NetworkCapabilities COMMON_CAPABILITIES =
+ // TODO: add NET_CAPABILITY_NOT_RESTRICTED and check that getRequestorUid() has
+ // BLUETOOTH_CONNECT permission.
+ NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_METERED)
+ .addCapability(NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .addCapability(NET_CAPABILITY_NOT_VPN)
+ .build();
+ private final Dependencies mDeps;
+ private final Context mContext;
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
+ private final NetworkProvider mProvider;
+ private final BlanketReservationOffer mBlanketOffer;
+ private final Set<ReservedServerOffer> mReservedServerOffers = new ArraySet<>();
+ private final ClientOffer mClientOffer;
+ // mBluetoothManager guaranteed non-null when read on handler thread after start() is called
+ @Nullable
+ private BluetoothManager mBluetoothManager;
+
+ // Note: IFNAMSIZ is 16.
+ private static final String TUN_IFNAME = "l2cap-tun";
+ private static int sTunIndex = 0;
+
+ /**
+ * The blanket reservation offer is used to create an L2CAP server network, i.e. a network
+ * based on a BluetoothServerSocket.
+ *
+ * Note that NetworkCapabilities matching semantics will cause onNetworkNeeded to be called for
+ * requests that do not have a NetworkSpecifier set.
+ */
+ private class BlanketReservationOffer implements NetworkOfferCallback {
+ public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+ // Note the missing NET_CAPABILITY_NOT_RESTRICTED marking the network as restricted.
+ public static final NetworkCapabilities CAPABILITIES;
+ static {
+ // Below capabilities will match any reservation request with an L2capNetworkSpecifier
+ // that specifies ROLE_SERVER or without a NetworkSpecifier.
+ final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .build();
+ NetworkCapabilities caps = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+ .setNetworkSpecifier(l2capNetworkSpecifier)
+ .build();
+ // TODO: add #setReservationId() to NetworkCapabilities.Builder
+ caps.setReservationId(RES_ID_MATCH_ALL_RESERVATIONS);
+ CAPABILITIES = caps;
+ }
+
+ @Override
+ public void onNetworkNeeded(NetworkRequest request) {
+ // The NetworkSpecifier is guaranteed to be either null or an L2capNetworkSpecifier, so
+ // this cast is safe.
+ final L2capNetworkSpecifier specifier =
+ (L2capNetworkSpecifier) request.getNetworkSpecifier();
+ if (specifier == null) return;
+ if (!specifier.isValidServerReservationSpecifier()) {
+ Log.i(TAG, "Ignoring invalid reservation request: " + request);
+ return;
+ }
+
+ final ReservedServerOffer reservedOffer = createReservedServerOffer(request);
+ if (reservedOffer == null) {
+ // Something went wrong when creating the offer. Send onUnavailable() to the app.
+ Log.e(TAG, "Failed to create L2cap server offer");
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ return;
+ }
+
+ final NetworkCapabilities reservedCaps = reservedOffer.getReservedCapabilities();
+ mProvider.registerNetworkOffer(SCORE, reservedCaps, mHandler::post, reservedOffer);
+ mReservedServerOffers.add(reservedOffer);
+ }
+
+ @Nullable
+ private ReservedServerOffer createReservedServerOffer(NetworkRequest reservation) {
+ final BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
+ if (bluetoothAdapter == null) {
+ Log.w(TAG, "Failed to get BluetoothAdapter");
+ return null;
+ }
+ final BluetoothServerSocket serverSocket;
+ try {
+ serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to open BluetoothServerSocket");
+ return null;
+ }
+
+ // Create the reserved capabilities partially from the reservation itself (non-reserved
+ // parts of the L2capNetworkSpecifier), the COMMON_CAPABILITIES, and the reserved data
+ // (BLE L2CAP PSM from the BluetoothServerSocket).
+ final NetworkCapabilities reservationNc = reservation.networkCapabilities;
+ final L2capNetworkSpecifier reservationSpec =
+ (L2capNetworkSpecifier) reservationNc.getNetworkSpecifier();
+ // Note: the RemoteAddress is unspecified for server networks.
+ final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(reservationSpec.getHeaderCompression())
+ .setPsm(serverSocket.getPsm())
+ .build();
+ NetworkCapabilities reservedNc =
+ new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+ .setNetworkSpecifier(reservedSpec)
+ .build();
+ reservedNc.setReservationId(reservationNc.getReservationId());
+ return new ReservedServerOffer(reservedNc, serverSocket);
+ }
+
+ @Nullable
+ private ReservedServerOffer getReservedOfferForRequest(NetworkRequest request) {
+ final int rId = request.networkCapabilities.getReservationId();
+ for (ReservedServerOffer offer : mReservedServerOffers) {
+ // Comparing by reservationId is more explicit then using canBeSatisfiedBy() or the
+ // requestId.
+ if (offer.getReservedCapabilities().getReservationId() != rId) continue;
+ return offer;
+ }
+ return null;
+ }
+
+ @Override
+ public void onNetworkUnneeded(NetworkRequest request) {
+ final ReservedServerOffer reservedOffer = getReservedOfferForRequest(request);
+ if (reservedOffer == null) return;
+
+ // Note that the reserved offer gets torn down when the reservation goes away, even if
+ // there are active (non-reservation) requests for said offer.
+ destroyAndUnregisterReservedOffer(reservedOffer);
+ }
+ }
+
+ private void destroyAndUnregisterReservedOffer(ReservedServerOffer reservedOffer) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ // Ensure the offer still exists if this was posted on the handler.
+ if (!mReservedServerOffers.contains(reservedOffer)) return;
+ mReservedServerOffers.remove(reservedOffer);
+
+ reservedOffer.tearDown();
+ mProvider.unregisterNetworkOffer(reservedOffer);
+ }
+
+ @Nullable
+ private L2capNetwork createL2capNetwork(BluetoothSocket socket, NetworkCapabilities caps,
+ L2capNetwork.ICallback cb) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ final String ifname = TUN_IFNAME + String.valueOf(sTunIndex++);
+ final ParcelFileDescriptor tunFd = mDeps.createTunInterface(ifname);
+ if (tunFd == null) {
+ return null;
+ }
+
+ return L2capNetwork.create(
+ mHandler, mContext, mProvider, ifname, socket, tunFd, caps, mDeps, cb);
+ }
+
+ private static void closeBluetoothSocket(BluetoothSocket socket) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close BluetoothSocket", e);
+ }
+ }
+
+ private class ReservedServerOffer implements NetworkOfferCallback {
+ private final NetworkCapabilities mReservedCapabilities;
+ private final AcceptThread mAcceptThread;
+ // This set should almost always contain at most one network. This is because all L2CAP
+ // server networks created by the same reserved offer are indistinguishable from each other,
+ // so that ConnectivityService will tear down all but the first. However, temporarily, there
+ // can be more than one network.
+ private final Set<L2capNetwork> mL2capNetworks = new ArraySet<>();
+
+ private class AcceptThread extends Thread {
+ private static final int TIMEOUT_MS = 500;
+ private final BluetoothServerSocket mServerSocket;
+
+ public AcceptThread(BluetoothServerSocket serverSocket) {
+ super("L2capNetworkProvider-AcceptThread");
+ mServerSocket = serverSocket;
+ }
+
+ private void postDestroyAndUnregisterReservedOffer() {
+ // Called on AcceptThread
+ mHandler.post(() -> {
+ destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
+ });
+ }
+
+ private void postCreateServerNetwork(BluetoothSocket connectedSocket) {
+ // Called on AcceptThread
+ mHandler.post(() -> {
+ final boolean success = createServerNetwork(connectedSocket);
+ if (!success) closeBluetoothSocket(connectedSocket);
+ });
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ final BluetoothSocket connectedSocket;
+ try {
+ connectedSocket = mServerSocket.accept();
+ } catch (IOException e) {
+ // Note calling BluetoothServerSocket#close() also triggers an IOException
+ // which is indistinguishable from any other exceptional behavior.
+ // postDestroyAndUnregisterReservedOffer() is always safe to call as it
+ // first checks whether the offer still exists; so if the
+ // BluetoothServerSocket was closed (i.e. on tearDown()) this is a noop.
+ Log.w(TAG, "BluetoothServerSocket closed or #accept failed", e);
+ postDestroyAndUnregisterReservedOffer();
+ return; // stop running immediately on error
+ }
+ postCreateServerNetwork(connectedSocket);
+ }
+ }
+
+ public void tearDown() {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ try {
+ // BluetoothServerSocket.close() is thread-safe.
+ mServerSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close BluetoothServerSocket", e);
+ }
+ try {
+ join();
+ } catch (InterruptedException e) {
+ // join() interrupted during tearDown(). Do nothing.
+ }
+ }
+ }
+
+ private boolean createServerNetwork(BluetoothSocket socket) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ // It is possible the offer went away.
+ if (!mReservedServerOffers.contains(this)) return false;
+
+ if (!socket.isConnected()) {
+ Log.wtf(TAG, "BluetoothSocket must be connected");
+ return false;
+ }
+
+ final L2capNetwork network = createL2capNetwork(socket, mReservedCapabilities,
+ new L2capNetwork.ICallback() {
+ @Override
+ public void onError(L2capNetwork network) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
+ }
+ @Override
+ public void onNetworkUnwanted(L2capNetwork network) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ // Leave reservation in place.
+ final boolean networkExists = mL2capNetworks.remove(network);
+ if (!networkExists) return; // already torn down.
+ network.tearDown();
+ }
+ });
+
+ if (network == null) {
+ Log.e(TAG, "Failed to create L2capNetwork");
+ return false;
+ }
+
+ mL2capNetworks.add(network);
+ return true;
+ }
+
+ public ReservedServerOffer(NetworkCapabilities reservedCapabilities,
+ BluetoothServerSocket serverSocket) {
+ mReservedCapabilities = reservedCapabilities;
+ mAcceptThread = new AcceptThread(serverSocket);
+ mAcceptThread.start();
+ }
+
+ public NetworkCapabilities getReservedCapabilities() {
+ return mReservedCapabilities;
+ }
+
+ @Override
+ public void onNetworkNeeded(NetworkRequest request) {
+ // UNUSED: the lifetime of the reserved network is controlled by the blanket offer.
+ }
+
+ @Override
+ public void onNetworkUnneeded(NetworkRequest request) {
+ // UNUSED: the lifetime of the reserved network is controlled by the blanket offer.
+ }
+
+ /** Called when the reservation goes away and the reserved offer must be torn down. */
+ public void tearDown() {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ mAcceptThread.tearDown();
+ for (L2capNetwork network : mL2capNetworks) {
+ network.tearDown();
+ }
+ }
+ }
+
+ private class ClientOffer implements NetworkOfferCallback {
+ public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+ public static final NetworkCapabilities CAPABILITIES;
+ static {
+ // Below capabilities will match any request with an L2capNetworkSpecifier
+ // that specifies ROLE_CLIENT or without a NetworkSpecifier.
+ final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .build();
+ CAPABILITIES = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+ .setNetworkSpecifier(l2capNetworkSpecifier)
+ .build();
+ }
+
+ private final Map<L2capNetworkSpecifier, ClientRequestInfo> mClientNetworkRequests =
+ new ArrayMap<>();
+
+ /**
+ * State object to store information for client NetworkRequests.
+ */
+ private static class ClientRequestInfo {
+ public final L2capNetworkSpecifier specifier;
+ public final List<NetworkRequest> requests = new ArrayList<>();
+ // TODO: add support for retries.
+ public final ConnectThread connectThread;
+ @Nullable
+ public L2capNetwork network;
+
+ public ClientRequestInfo(NetworkRequest request, ConnectThread connectThread) {
+ this.specifier = (L2capNetworkSpecifier) request.getNetworkSpecifier();
+ this.requests.add(request);
+ this.connectThread = connectThread;
+ }
+ }
+
+ // TODO: consider using ExecutorService
+ private class ConnectThread extends Thread {
+ private final L2capNetworkSpecifier mSpecifier;
+ private final BluetoothSocket mSocket;
+
+ public ConnectThread(L2capNetworkSpecifier specifier, BluetoothSocket socket) {
+ super("L2capNetworkProvider-ConnectThread");
+ mSpecifier = specifier;
+ mSocket = socket;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mSocket.connect();
+ mHandler.post(() -> {
+ final boolean success = createClientNetwork(mSpecifier, mSocket);
+ if (!success) closeBluetoothSocket(mSocket);
+ });
+ } catch (IOException e) {
+ Log.w(TAG, "BluetoothSocket was closed or #connect failed", e);
+ // It is safe to call BluetoothSocket#close() multiple times.
+ closeBluetoothSocket(mSocket);
+ mHandler.post(() -> {
+ // Note that if the Socket was closed, this call is a noop as the
+ // ClientNetworkRequest has already been removed.
+ declareAllNetworkRequestsUnfulfillable(mSpecifier);
+ });
+ }
+ }
+
+ public void abort() {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ // Closing the BluetoothSocket is the only way to unblock connect() because it calls
+ // shutdown on the underlying (connected) SOCK_SEQPACKET.
+ // It is safe to call BluetoothSocket#close() multiple times.
+ closeBluetoothSocket(mSocket);
+ try {
+ join();
+ } catch (InterruptedException e) {
+ Log.i(TAG, "Interrupted while joining ConnectThread", e);
+ }
+ }
+ }
+
+ private boolean createClientNetwork(L2capNetworkSpecifier specifier,
+ BluetoothSocket socket) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ // Check whether request still exists
+ final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+ if (cri == null) return false;
+
+ final NetworkCapabilities caps = new NetworkCapabilities.Builder(CAPABILITIES)
+ .setNetworkSpecifier(specifier)
+ .build();
+
+ final L2capNetwork network = createL2capNetwork(socket, caps,
+ new L2capNetwork.ICallback() {
+ // TODO: do not send onUnavailable() after the network has become available. The
+ // right thing to do here is to tearDown the network (if it still exists,
+ // because note that the request might have already been removed in the
+ // meantime, so `network` cannot be used directly.
+ @Override
+ public void onError(L2capNetwork network) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ declareAllNetworkRequestsUnfulfillable(specifier);
+ }
+ @Override
+ public void onNetworkUnwanted(L2capNetwork network) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ declareAllNetworkRequestsUnfulfillable(specifier);
+ }
+ });
+ if (network == null) return false;
+
+ cri.network = network;
+ return true;
+ }
+
+ @Override
+ public void onNetworkNeeded(NetworkRequest request) {
+ // The NetworkSpecifier is guaranteed to be either null or an L2capNetworkSpecifier, so
+ // this cast is safe.
+ final L2capNetworkSpecifier requestSpecifier =
+ (L2capNetworkSpecifier) request.getNetworkSpecifier();
+ if (requestSpecifier == null) return;
+ if (!requestSpecifier.isValidClientRequestSpecifier()) {
+ Log.i(TAG, "Ignoring invalid client request: " + request);
+ return;
+ }
+
+ // Check whether this exact request is already being tracked.
+ final ClientRequestInfo cri = mClientNetworkRequests.get(requestSpecifier);
+ if (cri != null) {
+ Log.d(TAG, "The request is already being tracked. NetworkRequest: " + request);
+ cri.requests.add(request);
+ return;
+ }
+
+ // Check whether a fuzzy match shows a mismatch in header compression by calling
+ // canBeSatisfiedBy().
+ // TODO: Add a copy constructor to L2capNetworkSpecifier.Builder.
+ final L2capNetworkSpecifier matchAnyHeaderCompressionSpecifier =
+ new L2capNetworkSpecifier.Builder()
+ .setRole(requestSpecifier.getRole())
+ .setRemoteAddress(requestSpecifier.getRemoteAddress())
+ .setPsm(requestSpecifier.getPsm())
+ .setHeaderCompression(HEADER_COMPRESSION_ANY)
+ .build();
+ for (L2capNetworkSpecifier existingSpecifier : mClientNetworkRequests.keySet()) {
+ if (existingSpecifier.canBeSatisfiedBy(matchAnyHeaderCompressionSpecifier)) {
+ // This requeset can never be serviced as this network already exists with a
+ // different header compression mechanism.
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ return;
+ }
+ }
+
+ // If the code reaches here, this is a new request.
+ final BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
+ if (bluetoothAdapter == null) {
+ Log.w(TAG, "Failed to get BluetoothAdapter");
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ return;
+ }
+
+ final byte[] macAddress = requestSpecifier.getRemoteAddress().toByteArray();
+ final BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(macAddress);
+ final BluetoothSocket socket;
+ try {
+ socket = bluetoothDevice.createInsecureL2capChannel(requestSpecifier.getPsm());
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to createInsecureL2capChannel", e);
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ return;
+ }
+
+ final ConnectThread connectThread = new ConnectThread(requestSpecifier, socket);
+ connectThread.start();
+ final ClientRequestInfo newRequestInfo = new ClientRequestInfo(request, connectThread);
+ mClientNetworkRequests.put(requestSpecifier, newRequestInfo);
+ }
+
+ @Override
+ public void onNetworkUnneeded(NetworkRequest request) {
+ final L2capNetworkSpecifier specifier =
+ (L2capNetworkSpecifier) request.getNetworkSpecifier();
+
+ // Map#get() is safe to call with null key
+ final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+ if (cri == null) return;
+
+ cri.requests.remove(request);
+ if (cri.requests.size() > 0) return;
+
+ // If the code reaches here, the network needs to be torn down.
+ releaseClientNetworkRequest(cri);
+ }
+
+ /**
+ * Release the client network request and tear down all associated state.
+ *
+ * Only call this when all associated NetworkRequests have been released.
+ */
+ private void releaseClientNetworkRequest(ClientRequestInfo cri) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ mClientNetworkRequests.remove(cri.specifier);
+ if (cri.connectThread.isAlive()) {
+ // Note that if ConnectThread succeeds between calling #isAlive() and #abort(), the
+ // request will already be removed from mClientNetworkRequests by the time the
+ // createClientNetwork() call is processed on the handler, so it is safe to call
+ // #abort().
+ cri.connectThread.abort();
+ }
+
+ if (cri.network != null) {
+ cri.network.tearDown();
+ }
+ }
+
+ private void declareAllNetworkRequestsUnfulfillable(L2capNetworkSpecifier specifier) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+ if (cri == null) return;
+
+ for (NetworkRequest request : cri.requests) {
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ }
+ releaseClientNetworkRequest(cri);
+ }
+ }
+
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Get the HandlerThread for L2capNetworkProvider to run on */
+ public HandlerThread getHandlerThread() {
+ final HandlerThread thread = new HandlerThread("L2capNetworkProviderThread");
+ thread.start();
+ return thread;
+ }
+
+ /** Create a tun interface configured for blocking i/o */
+ @Nullable
+ public ParcelFileDescriptor createTunInterface(String ifname) {
+ final ParcelFileDescriptor fd;
+ try {
+ fd = ParcelFileDescriptor.adoptFd(ServiceConnectivityJni.createTunTap(
+ true /*isTun*/,
+ true /*hasCarrier*/,
+ true /*setIffMulticast*/,
+ ifname));
+ ServiceConnectivityJni.bringUpInterface(ifname);
+ // TODO: consider adding a parameter to createTunTap() (or the Builder that should
+ // be added) to configure i/o blocking.
+ final int flags = Os.fcntlInt(fd.getFileDescriptor(), F_GETFL, 0);
+ Os.fcntlInt(fd.getFileDescriptor(), F_SETFL, flags & ~O_NONBLOCK);
+ } catch (Exception e) {
+ // Note: createTunTap currently throws an IllegalStateException on failure.
+ // TODO: native functions should throw ErrnoException.
+ Log.e(TAG, "Failed to create tun interface", e);
+ return null;
+ }
+ return fd;
+ }
+
+ /** Create an L2capPacketForwarder and start forwarding */
+ public L2capPacketForwarder createL2capPacketForwarder(Handler handler,
+ ParcelFileDescriptor tunFd, BluetoothSocket socket, boolean compressHeaders,
+ L2capPacketForwarder.ICallback cb) {
+ return new L2capPacketForwarder(handler, tunFd, socket, compressHeaders, cb);
+ }
+
+ /** Create an L2capIpClient */
+ public L2capIpClient createL2capIpClient(String logTag, Context context, String ifname) {
+ return new L2capIpClient(logTag, context, ifname);
+ }
+ }
+
+ public L2capNetworkProvider(Context context) {
+ this(new Dependencies(), context);
+ }
+
+ public L2capNetworkProvider(Dependencies deps, Context context) {
+ mDeps = deps;
+ mContext = context;
+ mHandlerThread = mDeps.getHandlerThread();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mProvider = new NetworkProvider(context, mHandlerThread.getLooper(), TAG);
+ mBlanketOffer = new BlanketReservationOffer();
+ mClientOffer = new ClientOffer();
+ }
+
+ /**
+ * Start L2capNetworkProvider.
+ *
+ * Called on CS Handler thread.
+ */
+ public void start() {
+ mHandler.post(() -> {
+ final PackageManager pm = mContext.getPackageManager();
+ if (!pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+ return;
+ }
+ mBluetoothManager = mContext.getSystemService(BluetoothManager.class);
+ if (mBluetoothManager == null) {
+ // Can this ever happen?
+ Log.wtf(TAG, "BluetoothManager not found");
+ return;
+ }
+ mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
+ mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
+ BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
+ mProvider.registerNetworkOffer(ClientOffer.SCORE,
+ ClientOffer.CAPABILITIES, mHandler::post, mClientOffer);
+ });
+ }
+}
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index 4d39d7d..96f4e20 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.ServiceConnectivityJni;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -75,15 +76,6 @@
@NonNull private final ConnectivityManager mCm;
@NonNull private final NetworkProvider mNetworkProvider;
- // Native method stubs
- private static native int nativeCreateTunTap(boolean isTun, boolean hasCarrier,
- boolean setIffMulticast, @NonNull String iface);
-
- private static native void nativeSetTunTapCarrierEnabled(@NonNull String iface, int tunFd,
- boolean enabled);
-
- private static native void nativeBringUpInterface(String iface);
-
@VisibleForTesting
protected TestNetworkService(@NonNull Context context) {
mHandlerThread = new HandlerThread("TestNetworkServiceThread");
@@ -143,7 +135,8 @@
// flags atomically.
final boolean setIffMulticast = bringUp;
ParcelFileDescriptor tunIntf = ParcelFileDescriptor.adoptFd(
- nativeCreateTunTap(isTun, hasCarrier, setIffMulticast, interfaceName));
+ ServiceConnectivityJni.createTunTap(
+ isTun, hasCarrier, setIffMulticast, interfaceName));
// Disable DAD and remove router_solicitation_delay before assigning link addresses.
if (disableIpv6ProvisioningDelay) {
@@ -160,7 +153,7 @@
}
if (bringUp) {
- nativeBringUpInterface(interfaceName);
+ ServiceConnectivityJni.bringUpInterface(interfaceName);
}
return new TestNetworkInterface(tunIntf, interfaceName);
@@ -403,11 +396,11 @@
@Override
public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
enforceTestNetworkPermissions(mContext);
- nativeSetTunTapCarrierEnabled(iface.getInterfaceName(), iface.getFileDescriptor().getFd(),
- enabled);
+ ServiceConnectivityJni.setTunTapCarrierEnabled(iface.getInterfaceName(),
+ iface.getFileDescriptor().getFd(), enabled);
// Explicitly close fd after use to prevent StrictMode from complaining.
// Also, explicitly referencing iface guarantees that the object is not garbage collected
- // before nativeSetTunTapCarrierEnabled() executes.
+ // before setTunTapCarrierEnabled() executes.
try {
iface.getFileDescriptor().close();
} catch (IOException e) {
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index df87316..136ea81 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -26,6 +26,11 @@
*/
public final class ConnectivityFlags {
/**
+ * Boot namespace for this module. Values from this should only be read at boot.
+ */
+ public static final String NAMESPACE_TETHERING_BOOT = "tethering_boot";
+
+ /**
* Minimum module version at which to avoid rematching all requests when a network request is
* registered, and rematch only the registered requests instead.
*/
@@ -44,6 +49,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/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 94b655f..e762a8e 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -25,6 +25,12 @@
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.transportNamesOf;
+import static android.system.OsConstants.EIO;
+import static android.system.OsConstants.EEXIST;
+import static android.system.OsConstants.ENOENT;
+
+import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED;
+import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_DISALLOW_BYPASS_VPN_FOR_DELEGATE_UID_ENOENT;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -57,9 +63,11 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.telephony.data.EpsBearerQosSessionAttributes;
import android.telephony.data.NrQosSessionAttributes;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -68,8 +76,10 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.WakeupMessage;
+import com.android.net.module.util.FrameworkConnectivityStatsLog;
import com.android.net.module.util.HandlerUtils;
import com.android.server.ConnectivityService;
+import com.android.server.ConnectivityService.CaptivePortalImpl;
import java.io.PrintWriter;
import java.net.Inet4Address;
@@ -574,6 +584,10 @@
// For fast lookups. Indexes into mInactivityTimers by request ID.
private final SparseArray<InactivityTimer> mInactivityTimerForRequest = new SparseArray<>();
+ // Map of delegated UIDs used to bypass VPN and its captive portal app caller.
+ private final ArrayMap<CaptivePortalImpl, Integer> mCaptivePortalDelegateUids =
+ new ArrayMap<>();
+
// Inactivity expiry timer. Armed whenever mInactivityTimers is non-empty, regardless of
// whether the network is inactive or not. Always set to the expiry of the mInactivityTimers
// that expires last. When the timer fires, all inactivity state is cleared, and if the network
@@ -626,6 +640,7 @@
private final Context mContext;
private final Handler mHandler;
private final QosCallbackTracker mQosCallbackTracker;
+ private final INetd mNetd;
private final long mCreationTime;
@@ -655,6 +670,7 @@
mConnServiceDeps = deps;
setScore(score); // uses members connService, networkCapabilities and networkAgentConfig
clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
+ mNetd = netd;
mContext = context;
mHandler = handler;
this.factorySerialNumber = factorySerialNumber;
@@ -1112,6 +1128,7 @@
int delta = add ? +1 : -1;
switch (request.type) {
case REQUEST:
+ case RESERVATION:
mNumRequestNetworkRequests += delta;
break;
@@ -1549,6 +1566,58 @@
}
}
+ private int allowBypassVpnOnNetwork(boolean allow, int uid, int netId) {
+ try {
+ mNetd.networkAllowBypassVpnOnNetwork(allow, uid, netId);
+ return 0;
+ } catch (RemoteException e) {
+ // Netd has crashed, and this process is about to crash as well.
+ return EIO;
+ } catch (ServiceSpecificException e) {
+ return e.errorCode;
+ }
+ }
+
+ /**
+ * Set the delegate UID of the app that is allowed to perform network traffic for captive
+ * portal login, and configure the netd bypass rule with this delegated UID.
+ *
+ * @param caller the captive portal app to that delegated UID
+ * @param uid the delegated UID of the captive portal app.
+ * @return Return 0 if set the UID and VPN bypass rule successfully or bypass rule corresponding
+ * to this UID already exists otherwise return errno.
+ */
+ public int setCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller, int uid) {
+ final int errorCode = allowBypassVpnOnNetwork(true /* allow */, uid, network.netId);
+ if (errorCode == 0 || errorCode == EEXIST) {
+ mCaptivePortalDelegateUids.put(caller, uid);
+ }
+ return errorCode == EEXIST ? 0 : errorCode;
+ }
+
+ /**
+ * Remove the delegate UID of the app that is allowed to perform network traffic for captive
+ * portal login, and remove the netd bypass rule if no other caller is delegating this UID.
+ *
+ * @param caller the captive portal app to that delegated UID.
+ * @return Return 0 if remove the UID and VPN bypass rule successfully or bypass rule
+ * corresponding to this UID doesn't exist otherwise return errno.
+ */
+ public int removeCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller) {
+ final Integer maybeDelegateUid = mCaptivePortalDelegateUids.remove(caller);
+ if (maybeDelegateUid == null) return 0;
+ if (mCaptivePortalDelegateUids.values().contains(maybeDelegateUid)) return 0;
+ final int errorCode =
+ allowBypassVpnOnNetwork(false /* allow */, maybeDelegateUid, network.netId);
+ if (errorCode == ENOENT) {
+ FrameworkConnectivityStatsLog.write(
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_DISALLOW_BYPASS_VPN_FOR_DELEGATE_UID_ENOENT
+ );
+ }
+ return errorCode == ENOENT ? 0 : errorCode;
+ }
+
private static boolean areAllowedUidsAcceptableFromNetworkAgent(
@NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
@NonNull final ConnectivityService.Dependencies deps,
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index eea382e..d294046 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -42,6 +42,7 @@
* @hide
*/
public class NetworkOffer implements NetworkRanker.Scoreable {
+ private static final String TAG = NetworkOffer.class.getSimpleName();
@NonNull public final FullScore score;
@NonNull public final NetworkCapabilities caps;
@NonNull public final INetworkOfferCallback callback;
@@ -126,6 +127,23 @@
}
/**
+ * Sends onNetworkUnneeded for any remaining NetworkRequests.
+ *
+ * Used after a NetworkOffer migration failed to let the provider know that its networks should
+ * be torn down (as the offer is no longer registered).
+ */
+ public void notifyUnneeded() {
+ try {
+ for (NetworkRequest request : mCurrentlyNeeded) {
+ callback.onNetworkUnneeded(request);
+ }
+ } catch (RemoteException e) {
+ // The remote is dead; nothing to do.
+ }
+ mCurrentlyNeeded.clear();
+ }
+
+ /**
* Migrate from, and take over, a previous offer.
*
* When an updated offer is sent from a provider, call this method on the new offer, passing
diff --git a/service/src/com/android/server/connectivity/NetworkPermissions.java b/service/src/com/android/server/connectivity/NetworkPermissions.java
new file mode 100644
index 0000000..9543d8f
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkPermissions.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2025 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.connectivity;
+
+import android.net.INetd;
+
+/**
+ * A wrapper class for managing network and traffic permissions.
+ *
+ * This class encapsulates permissions represented as a bitmask, as defined in INetd.aidl
+ * and used within PermissionMonitor.java. It distinguishes between two types of permissions:
+ *
+ * 1. Network Permissions: These permissions, declared in INetd.aidl, are used
+ * by the Android platform's network daemon (system/netd) to control network
+ * management
+ *
+ * 2. Traffic Permissions: These permissions are used internally by PermissionMonitor.java and
+ * BpfNetMaps.java to manage fine-grained network traffic filtering and control.
+ *
+ * This wrapper ensures that no new permission definitions, here or in aidl, conflict with any
+ * existing permissions. This prevents unintended interactions or overrides.
+ *
+ * @hide
+ */
+public class NetworkPermissions {
+
+ /*
+ * Below are network permissions declared in INetd.aidl and used by the platform. Using these is
+ * equivalent to using the values in android.net.INetd.
+ */
+ public static final int PERMISSION_NONE = INetd.PERMISSION_NONE; /* 0 */
+ public static final int PERMISSION_NETWORK = INetd.PERMISSION_NETWORK; /* 1 */
+ public static final int PERMISSION_SYSTEM = INetd.PERMISSION_SYSTEM; /* 2 */
+
+ /*
+ * Below are traffic permissions used by PermissionMonitor and BpfNetMaps.
+ */
+
+ /**
+ * PERMISSION_UNINSTALLED is used when an app is uninstalled from the device. All internet
+ * related permissions need to be cleaned.
+ */
+ public static final int TRAFFIC_PERMISSION_UNINSTALLED = -1;
+
+ /**
+ * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets.
+ */
+ public static final int TRAFFIC_PERMISSION_INTERNET = 4;
+
+ /**
+ * PERMISSION_UPDATE_DEVICE_STATS is used for system UIDs and privileged apps
+ * that have the UPDATE_DEVICE_STATS permission.
+ */
+ public static final int TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS = 8;
+
+ /**
+ * TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST indicates if an SdkSandbox UID will be allowed
+ * to connect to localhost. For non SdkSandbox UIDs this bit is a no-op.
+ */
+ public static final int TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST = 16;
+}
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index beaa174..5de5f61 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -19,25 +19,29 @@
import static android.Manifest.permission.CHANGE_NETWORK_STATE;
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.INTERNET;
+import static android.Manifest.permission.NEARBY_WIFI_DEVICES;
import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
-import static android.net.INetd.PERMISSION_INTERNET;
-import static android.net.INetd.PERMISSION_NETWORK;
-import static android.net.INetd.PERMISSION_NONE;
-import static android.net.INetd.PERMISSION_SYSTEM;
-import static android.net.INetd.PERMISSION_UNINSTALLED;
-import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.connectivity.ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK;
import static android.os.Process.INVALID_UID;
import static android.os.Process.SYSTEM_UID;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_NETWORK;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_NONE;
+import static com.android.server.connectivity.NetworkPermissions.PERMISSION_SYSTEM;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_INTERNET;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED;
+import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
import static com.android.net.module.util.CollectionUtils.toIntArray;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.compat.CompatChanges;
+import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -60,6 +64,7 @@
import android.os.SystemConfigManager;
import android.os.UserHandle;
import android.os.UserManager;
+import android.permission.PermissionManager;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -96,6 +101,8 @@
private final PackageManager mPackageManager;
private final UserManager mUserManager;
private final SystemConfigManager mSystemConfigManager;
+ private final PermissionManager mPermissionManager;
+ private final PermissionChangeListener mPermissionChangeListener;
private final INetd mNetd;
private final Dependencies mDeps;
private final Context mContext;
@@ -227,6 +234,12 @@
context.getContentResolver().registerContentObserver(
uri, notifyForDescendants, observer);
}
+
+ public boolean shouldEnforceLocalNetRestrictions(int uid) {
+ // TODO(b/394567896): Update compat change checks for enforcement
+ return BpfNetMaps.isAtLeast25Q2() &&
+ CompatChanges.isChangeEnabled(RESTRICT_LOCAL_NETWORK, uid);
+ }
}
private static class MultiSet<T> {
@@ -267,18 +280,49 @@
}
@VisibleForTesting
- PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
+ public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
@NonNull final BpfNetMaps bpfNetMaps,
@NonNull final Dependencies deps,
@NonNull final HandlerThread thread) {
mPackageManager = context.getPackageManager();
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
mSystemConfigManager = context.getSystemService(SystemConfigManager.class);
+ mPermissionManager = context.getSystemService(PermissionManager.class);
+ mPermissionChangeListener = new PermissionChangeListener();
mNetd = netd;
mDeps = deps;
mContext = context;
mBpfNetMaps = bpfNetMaps;
mThread = thread;
+ if (BpfNetMaps.isAtLeast25Q2()) {
+ // Local net restrictions is supported as a developer opt-in starting in Android B.
+ // This listener should finish registration by the time the system has completed
+ // boot setup such that any changes to runtime permissions for local network
+ // restrictions can only occur after this registration has completed.
+ mPackageManager.addOnPermissionsChangeListener(mPermissionChangeListener);
+ }
+ }
+
+ @VisibleForTesting
+ void setLocalNetworkPermissions(final int uid, @Nullable final String packageName) {
+ if (!mDeps.shouldEnforceLocalNetRestrictions(uid)) return;
+
+ final AttributionSource attributionSource =
+ new AttributionSource.Builder(uid).setPackageName(packageName).build();
+ final int permissionState = mPermissionManager.checkPermissionForPreflight(
+ NEARBY_WIFI_DEVICES, attributionSource);
+ if (permissionState == PermissionManager.PERMISSION_GRANTED) {
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(attributionSource.getUid());
+ } else {
+ mBpfNetMaps.addUidToLocalNetBlockMap(attributionSource.getUid());
+ }
+ if (hasSdkSandbox(uid)){
+ // SDKs in the SDK RT cannot hold runtime permissions
+ final int sdkSandboxUid = sProcessShim.toSdkSandboxUid(uid);
+ if (!mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(sdkSandboxUid)) {
+ mBpfNetMaps.addUidToLocalNetBlockMap(sdkSandboxUid);
+ }
+ }
}
private void ensureRunningOnHandlerThread() {
@@ -341,6 +385,7 @@
uidsPerm.put(sdkSandboxUid, permission);
}
}
+ setLocalNetworkPermissions(uid, app.packageName);
}
return uidsPerm;
}
@@ -395,7 +440,7 @@
final SparseIntArray appIdsPerm = new SparseIntArray();
for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {
final int appId = UserHandle.getAppId(uid);
- final int permission = appIdsPerm.get(appId) | PERMISSION_INTERNET;
+ final int permission = appIdsPerm.get(appId) | TRAFFIC_PERMISSION_INTERNET;
appIdsPerm.put(appId, permission);
if (hasSdkSandbox(appId)) {
appIdsPerm.put(sProcessShim.toSdkSandboxUid(appId), permission);
@@ -403,7 +448,7 @@
}
for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {
final int appId = UserHandle.getAppId(uid);
- final int permission = appIdsPerm.get(appId) | PERMISSION_UPDATE_DEVICE_STATS;
+ final int permission = appIdsPerm.get(appId) | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
appIdsPerm.put(appId, permission);
if (hasSdkSandbox(appId)) {
appIdsPerm.put(sProcessShim.toSdkSandboxUid(appId), permission);
@@ -588,7 +633,7 @@
final List<PackageInfo> apps = getInstalledPackagesAsUser(user);
- // Save all apps
+ // Save all apps in mAllApps
updateAllApps(apps);
// Uids network permissions
@@ -625,6 +670,11 @@
final int uid = allUids.keyAt(i);
if (user.equals(UserHandle.getUserHandleForUid(uid))) {
mUidToNetworkPerm.delete(uid);
+ if (mDeps.shouldEnforceLocalNetRestrictions(uid)) {
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid);
+ if (hasSdkSandbox(uid)) mBpfNetMaps.removeUidFromLocalNetBlockMap(
+ sProcessShim.toSdkSandboxUid(uid));
+ }
removedUids.put(uid, allUids.valueAt(i));
}
}
@@ -646,7 +696,7 @@
final int appId = removedUserAppIds.keyAt(i);
// Need to clear permission if the removed appId is not found in the array.
if (appIds.indexOfKey(appId) < 0) {
- appIds.put(appId, PERMISSION_UNINSTALLED);
+ appIds.put(appId, TRAFFIC_PERMISSION_UNINSTALLED);
}
}
sendAppIdsTrafficPermission(appIds);
@@ -698,7 +748,7 @@
}
} else {
// The last package of this uid is removed from device. Clean the package up.
- permission = PERMISSION_UNINSTALLED;
+ permission = TRAFFIC_PERMISSION_UNINSTALLED;
}
return permission;
}
@@ -741,13 +791,13 @@
return "NETWORK";
case PERMISSION_SYSTEM:
return "SYSTEM";
- case PERMISSION_INTERNET:
+ case TRAFFIC_PERMISSION_INTERNET:
return "INTERNET";
- case PERMISSION_UPDATE_DEVICE_STATS:
+ case TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS:
return "UPDATE_DEVICE_STATS";
- case (PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS):
+ case (TRAFFIC_PERMISSION_INTERNET | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS):
return "ALL";
- case PERMISSION_UNINSTALLED:
+ case TRAFFIC_PERMISSION_UNINSTALLED:
return "UNINSTALLED";
default:
return "UNKNOWN";
@@ -766,7 +816,7 @@
// (PERMISSION_UNINSTALLED), remove the appId from the array. Otherwise, update the latest
// permission to the appId.
final int appId = UserHandle.getAppId(uid);
- if (uidTrafficPerm == PERMISSION_UNINSTALLED) {
+ if (uidTrafficPerm == TRAFFIC_PERMISSION_UNINSTALLED) {
userTrafficPerms.delete(appId);
} else {
userTrafficPerms.put(appId, uidTrafficPerm);
@@ -784,7 +834,7 @@
installed = true;
}
}
- return installed ? permission : PERMISSION_UNINSTALLED;
+ return installed ? permission : TRAFFIC_PERMISSION_UNINSTALLED;
}
/**
@@ -819,6 +869,7 @@
}
sendUidsNetworkPermission(apps, true /* add */);
}
+ setLocalNetworkPermissions(uid, packageName);
// If the newly-installed package falls within some VPN's uid range, update Netd with it.
// This needs to happen after the mUidToNetworkPerm update above, since
@@ -863,6 +914,11 @@
synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) {
// Update uid permission.
updateAppIdTrafficPermission(uid);
+ if (BpfNetMaps.isAtLeast25Q2()) {
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid);
+ if (hasSdkSandbox(uid)) mBpfNetMaps.removeUidFromLocalNetBlockMap(
+ sProcessShim.toSdkSandboxUid(uid));
+ }
// Get the appId permission from all users then send the latest permission to netd.
final int appId = UserHandle.getAppId(uid);
final int appIdTrafficPerm = getAppIdTrafficPermission(appId);
@@ -921,11 +977,11 @@
for (int i = 0; i < requestedPermissions.length; i++) {
if (requestedPermissions[i].equals(INTERNET)
&& ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) {
- permissions |= PERMISSION_INTERNET;
+ permissions |= TRAFFIC_PERMISSION_INTERNET;
}
if (requestedPermissions[i].equals(UPDATE_DEVICE_STATS)
&& ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) {
- permissions |= PERMISSION_UPDATE_DEVICE_STATS;
+ permissions |= TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
}
}
return permissions;
@@ -1164,19 +1220,19 @@
for (int i = 0; i < netdPermissionsAppIds.size(); i++) {
int permissions = netdPermissionsAppIds.valueAt(i);
switch(permissions) {
- case (PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS):
+ case (TRAFFIC_PERMISSION_INTERNET | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS):
allPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
- case PERMISSION_INTERNET:
+ case TRAFFIC_PERMISSION_INTERNET:
internetPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
- case PERMISSION_UPDATE_DEVICE_STATS:
+ case TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS:
updateStatsPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
case PERMISSION_NONE:
noPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
- case PERMISSION_UNINSTALLED:
+ case TRAFFIC_PERMISSION_UNINSTALLED:
uninstalledAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
default:
@@ -1188,15 +1244,15 @@
// TODO: add a lock inside netd to protect IPC trafficSetNetPermForUids()
if (allPermissionAppIds.size() != 0) {
mBpfNetMaps.setNetPermForUids(
- PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS,
+ TRAFFIC_PERMISSION_INTERNET | TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS,
toIntArray(allPermissionAppIds));
}
if (internetPermissionAppIds.size() != 0) {
- mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET,
+ mBpfNetMaps.setNetPermForUids(TRAFFIC_PERMISSION_INTERNET,
toIntArray(internetPermissionAppIds));
}
if (updateStatsPermissionAppIds.size() != 0) {
- mBpfNetMaps.setNetPermForUids(PERMISSION_UPDATE_DEVICE_STATS,
+ mBpfNetMaps.setNetPermForUids(TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS,
toIntArray(updateStatsPermissionAppIds));
}
if (noPermissionAppIds.size() != 0) {
@@ -1204,7 +1260,7 @@
toIntArray(noPermissionAppIds));
}
if (uninstalledAppIds.size() != 0) {
- mBpfNetMaps.setNetPermForUids(PERMISSION_UNINSTALLED,
+ mBpfNetMaps.setNetPermForUids(TRAFFIC_PERMISSION_UNINSTALLED,
toIntArray(uninstalledAppIds));
}
} catch (RemoteException | ServiceSpecificException e) {
@@ -1311,4 +1367,11 @@
private static void loge(String s, Throwable e) {
Log.e(TAG, s, e);
}
+
+ private class PermissionChangeListener implements PackageManager.OnPermissionsChangedListener {
+ @Override
+ public void onPermissionsChanged(int uid) {
+ setLocalNetworkPermissions(uid, null);
+ }
+ }
}
diff --git a/service/src/com/android/server/net/HeaderCompressionUtils.java b/service/src/com/android/server/net/HeaderCompressionUtils.java
new file mode 100644
index 0000000..5bd3a76
--- /dev/null
+++ b/service/src/com/android/server/net/HeaderCompressionUtils.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public class HeaderCompressionUtils {
+ private static final String TAG = "L2capHeaderCompressionUtils";
+ private static final int IPV6_HEADER_SIZE = 40;
+
+ private static byte[] decodeIpv6Address(ByteBuffer buffer, int mode, boolean isMulticast)
+ throws BufferUnderflowException, IOException {
+ // Mode is equivalent between SAM and DAM; however, isMulticast only applies to DAM.
+ final byte[] address = new byte[16];
+ // If multicast bit is set, mix it in the mode, so that the lower two bits represent the
+ // address mode, and the upper bit represents multicast compression.
+ switch ((isMulticast ? 0b100 : 0) | mode) {
+ case 0b000: // 128 bits. The full address is carried in-line.
+ case 0b100:
+ buffer.get(address);
+ break;
+ case 0b001: // 64 bits. The first 64-bits of the fe80:: address are elided.
+ address[0] = (byte) 0xfe;
+ address[1] = (byte) 0x80;
+ buffer.get(address, 8 /*off*/, 8 /*len*/);
+ break;
+ case 0b010: // 16 bits. fe80::ff:fe00:XXXX, where XXXX are the bits carried in-line
+ address[0] = (byte) 0xfe;
+ address[1] = (byte) 0x80;
+ address[11] = (byte) 0xff;
+ address[12] = (byte) 0xfe;
+ buffer.get(address, 14 /*off*/, 2 /*len*/);
+ break;
+ case 0b011: // 0 bits. The address is fully elided and derived from BLE MAC address
+ // Note that on Android, the BLE MAC addresses are not exposed via the API;
+ // therefore, this compression mode cannot be supported.
+ throw new IOException("Address cannot be fully elided");
+ case 0b101: // 48 bits. The address takes the form ffXX::00XX:XXXX:XXXX.
+ address[0] = (byte) 0xff;
+ address[1] = buffer.get();
+ buffer.get(address, 11 /*off*/, 5 /*len*/);
+ break;
+ case 0b110: // 32 bits. The address takes the form ffXX::00XX:XXXX
+ address[0] = (byte) 0xff;
+ address[1] = buffer.get();
+ buffer.get(address, 13 /*off*/, 3 /*len*/);
+ break;
+ case 0b111: // 8 bits. The address takes the form ff02::00XX.
+ address[0] = (byte) 0xff;
+ address[1] = (byte) 0x02;
+ address[15] = buffer.get();
+ break;
+ }
+ return address;
+ }
+
+ /**
+ * Performs 6lowpan header decompression in place.
+ *
+ * Note that the passed in buffer must have enough capacity for successful decompression.
+ *
+ * @param bytes The buffer containing the packet.
+ * @param len The size of the packet
+ * @return decompressed size or zero
+ * @throws BufferUnderflowException if an illegal packet is encountered.
+ * @throws IOException if an unsupported option is encountered.
+ */
+ public static int decompress6lowpan(byte[] bytes, int len)
+ throws BufferUnderflowException, IOException {
+ // Note that ByteBuffer's default byte order is big endian.
+ final ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
+ inBuffer.limit(len);
+
+ // LOWPAN_IPHC base encoding:
+ // 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15
+ // +---+---+---+---+---+---+---+---|---+---+---+---+---+---+---+---+
+ // | 0 | 1 | 1 | TF |NH | HLIM |CID|SAC| SAM | M |DAC| DAM |
+ // +---+---+---+---+---+---+---+---|---+---+---+---+---+---+---+---+
+ final int iphc1 = inBuffer.get() & 0xff;
+ final int iphc2 = inBuffer.get() & 0xff;
+ // Dispatch must start with 0b011.
+ if ((iphc1 & 0xe0) != 0x60) {
+ throw new IOException("LOWPAN_IPHC does not start with 011");
+ }
+
+ final int tf = (iphc1 >> 3) & 3; // Traffic class
+ final boolean nh = (iphc1 & 4) != 0; // Next header
+ final int hlim = iphc1 & 3; // Hop limit
+ final boolean cid = (iphc2 & 0x80) != 0; // Context identifier extension
+ final boolean sac = (iphc2 & 0x40) != 0; // Source address compression
+ final int sam = (iphc2 >> 4) & 3; // Source address mode
+ final boolean m = (iphc2 & 8) != 0; // Multicast compression
+ final boolean dac = (iphc2 & 4) != 0; // Destination address compression
+ final int dam = iphc2 & 3; // Destination address mode
+
+ final ByteBuffer ipv6Header = ByteBuffer.allocate(IPV6_HEADER_SIZE);
+
+ final int trafficClass;
+ final int flowLabel;
+ switch (tf) {
+ case 0b00: // ECN + DSCP + 4-bit Pad + Flow Label (4 bytes)
+ trafficClass = inBuffer.get() & 0xff;
+ flowLabel = (inBuffer.get() & 0x0f) << 16
+ | (inBuffer.get() & 0xff) << 8
+ | (inBuffer.get() & 0xff);
+ break;
+ case 0b01: // ECN + 2-bit Pad + Flow Label (3 bytes), DSCP is elided.
+ final int firstByte = inBuffer.get() & 0xff;
+ // 0 1 2 3 4 5 6 7
+ // +-----+-----+-----+-----+-----+-----+-----+-----+
+ // | DS FIELD, DSCP | ECN FIELD |
+ // +-----+-----+-----+-----+-----+-----+-----+-----+
+ // rfc6282 does not explicitly state what value to use for DSCP, assuming 0.
+ trafficClass = firstByte >> 6;
+ flowLabel = (firstByte & 0x0f) << 16
+ | (inBuffer.get() & 0xff) << 8
+ | (inBuffer.get() & 0xff);
+ break;
+ case 0b10: // ECN + DSCP (1 byte), Flow Label is elided.
+ trafficClass = inBuffer.get() & 0xff;
+ // rfc6282 does not explicitly state what value to use, assuming 0.
+ flowLabel = 0;
+ break;
+ case 0b11: // Traffic Class and Flow Label are elided.
+ // rfc6282 does not explicitly state what value to use, assuming 0.
+ trafficClass = 0;
+ flowLabel = 0;
+ break;
+ default:
+ // This cannot happen. Crash if it does.
+ throw new IllegalStateException("Illegal TF value");
+ }
+
+ // Write version, traffic class, and flow label
+ final int versionTcFlowLabel = (6 << 28) | (trafficClass << 20) | flowLabel;
+ ipv6Header.putInt(versionTcFlowLabel);
+
+ // Payload length is still unknown. Use 0 for now.
+ ipv6Header.putShort((short) 0);
+
+ // We do not use UDP or extension header compression, therefore the next header
+ // cannot be compressed.
+ if (nh) throw new IOException("Next header cannot be compressed");
+ // Write next header
+ ipv6Header.put(inBuffer.get());
+
+ final byte hopLimit;
+ switch (hlim) {
+ case 0b00: // The Hop Limit field is carried in-line.
+ hopLimit = inBuffer.get();
+ break;
+ case 0b01: // The Hop Limit field is compressed and the hop limit is 1.
+ hopLimit = 1;
+ break;
+ case 0b10: // The Hop Limit field is compressed and the hop limit is 64.
+ hopLimit = 64;
+ break;
+ case 0b11: // The Hop Limit field is compressed and the hop limit is 255.
+ hopLimit = (byte) 255;
+ break;
+ default:
+ // This cannot happen. Crash if it does.
+ throw new IllegalStateException("Illegal HLIM value");
+ }
+ ipv6Header.put(hopLimit);
+
+ if (cid) throw new IOException("Context based compression not supported");
+ if (sac) throw new IOException("Context based compression not supported");
+ if (dac) throw new IOException("Context based compression not supported");
+
+ // Write source address
+ ipv6Header.put(decodeIpv6Address(inBuffer, sam, false /* isMulticast */));
+
+ // Write destination address
+ ipv6Header.put(decodeIpv6Address(inBuffer, dam, m));
+
+ // Go back and fix up payloadLength
+ final short payloadLength = (short) inBuffer.remaining();
+ ipv6Header.putShort(4, payloadLength);
+
+ // Done! Check that 40 bytes were written.
+ if (ipv6Header.position() != IPV6_HEADER_SIZE) {
+ // This indicates a bug in our code -> crash.
+ throw new IllegalStateException("Faulty decompression wrote less than 40 bytes");
+ }
+
+ // Ensure there is enough room in the buffer
+ final int packetLength = payloadLength + IPV6_HEADER_SIZE;
+ if (bytes.length < packetLength) {
+ throw new IOException("Decompressed packet exceeds buffer size");
+ }
+
+ // Move payload bytes back to make room for the header
+ inBuffer.limit(packetLength);
+ System.arraycopy(bytes, inBuffer.position(), bytes, IPV6_HEADER_SIZE, payloadLength);
+ // Copy IPv6 header to the beginning of the buffer.
+ inBuffer.position(0);
+ ipv6Header.flip();
+ inBuffer.put(ipv6Header);
+
+ return packetLength;
+ }
+
+ /**
+ * Performs 6lowpan header compression in place.
+ *
+ * @param bytes The buffer containing the packet.
+ * @param len The size of the packet
+ * @return compressed size or zero
+ * @throws BufferUnderflowException if an illegal packet is encountered.
+ * @throws IOException if an unsupported option is encountered.
+ */
+ public static int compress6lowpan(byte[] bytes, final int len)
+ throws BufferUnderflowException, IOException {
+ // Compression only happens on egress, i.e. the packet is read from the tun fd.
+ // This means that this code can be a bit more lenient.
+ if (len < 40) {
+ Log.wtf(TAG, "Encountered short (<40 byte) packet");
+ return 0;
+ }
+
+ // Note that ByteBuffer's default byte order is big endian.
+ final ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
+ inBuffer.limit(len);
+
+ // Check that the packet is an IPv6 packet
+ final int versionTcFlowLabel = inBuffer.getInt() & 0xffffffff;
+ if ((versionTcFlowLabel >> 28) != 6) {
+ return 0;
+ }
+
+ // Check that the payload length matches the packet length - 40.
+ int payloadLength = inBuffer.getShort();
+ if (payloadLength != len - IPV6_HEADER_SIZE) {
+ throw new IOException("Encountered packet with payload length mismatch");
+ }
+
+ // Implements rfc 6282 6lowpan header compression using iphc 0110 0000 0000 0000 (all
+ // fields are carried inline).
+ inBuffer.position(0);
+ inBuffer.put((byte) 0x60);
+ inBuffer.put((byte) 0x00);
+ final byte trafficClass = (byte) ((versionTcFlowLabel >> 20) & 0xff);
+ inBuffer.put(trafficClass);
+ final byte flowLabelMsb = (byte) ((versionTcFlowLabel >> 16) & 0x0f);
+ final short flowLabelLsb = (short) (versionTcFlowLabel & 0xffff);
+ inBuffer.put(flowLabelMsb);
+ // Note: the next putShort overrides the payload length. This is WAI as the payload length
+ // is reconstructed via L2CAP packet length.
+ inBuffer.putShort(flowLabelLsb);
+
+ // Since the iphc (2 bytes) matches the payload length that was elided (2 bytes), the length
+ // of the packet did not change.
+ return len;
+ }
+}
diff --git a/service/src/com/android/server/net/L2capNetwork.java b/service/src/com/android/server/net/L2capNetwork.java
new file mode 100644
index 0000000..ca155db
--- /dev/null
+++ b/service/src/com/android/server/net/L2capNetwork.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.net.L2capNetworkSpecifier;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.net.ip.IpClientUtil;
+import android.net.shared.ProvisioningConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.android.server.L2capNetworkProvider;
+
+public class L2capNetwork {
+ private static final NetworkScore NETWORK_SCORE = new NetworkScore.Builder().build();
+ private final String mLogTag;
+ private final Handler mHandler;
+ private final L2capPacketForwarder mForwarder;
+ private final NetworkCapabilities mNetworkCapabilities;
+ private final NetworkAgent mNetworkAgent;
+
+ /** IpClient wrapper to handle IPv6 link-local provisioning for L2CAP tun.
+ *
+ * Note that the IpClient does not need to be stopped.
+ */
+ public static class L2capIpClient extends IpClientCallbacks {
+ private final String mLogTag;
+ private final ConditionVariable mOnIpClientCreatedCv = new ConditionVariable(false);
+ private final ConditionVariable mOnProvisioningSuccessCv = new ConditionVariable(false);
+ @Nullable
+ private IpClientManager mIpClient;
+ @Nullable
+ private volatile LinkProperties mLinkProperties;
+
+ public L2capIpClient(String logTag, Context context, String ifname) {
+ mLogTag = logTag;
+ IpClientUtil.makeIpClient(context, ifname, this);
+ }
+
+ @Override
+ public void onIpClientCreated(IIpClient ipClient) {
+ mIpClient = new IpClientManager(ipClient, mLogTag);
+ mOnIpClientCreatedCv.open();
+ }
+
+ @Override
+ public void onProvisioningSuccess(LinkProperties lp) {
+ Log.d(mLogTag, "Successfully provisioned l2cap tun: " + lp);
+ mLinkProperties = lp;
+ mOnProvisioningSuccessCv.open();
+ }
+
+ @Override
+ public void onProvisioningFailure(LinkProperties lp) {
+ Log.i(mLogTag, "Failed to provision l2cap tun: " + lp);
+ mLinkProperties = null;
+ mOnProvisioningSuccessCv.open();
+ }
+
+ /**
+ * Starts IPv6 link-local provisioning.
+ *
+ * @return LinkProperties on success, null on failure.
+ */
+ @Nullable
+ public LinkProperties start() {
+ mOnIpClientCreatedCv.block();
+ // mIpClient guaranteed non-null.
+ final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+ .withoutIPv4()
+ .withIpv6LinkLocalOnly()
+ .withRandomMacAddress() // addr_gen_mode EUI64 -> random on tun.
+ .build();
+ mIpClient.startProvisioning(config);
+ // "Provisioning" is guaranteed to succeed as link-local only mode does not actually
+ // require any provisioning.
+ mOnProvisioningSuccessCv.block();
+ return mLinkProperties;
+ }
+ }
+
+ public interface ICallback {
+ /** Called when an error is encountered */
+ void onError(L2capNetwork network);
+ /** Called when CS triggers NetworkAgent#onNetworkUnwanted */
+ void onNetworkUnwanted(L2capNetwork network);
+ }
+
+ public L2capNetwork(String logTag, Handler handler, Context context, NetworkProvider provider,
+ BluetoothSocket socket, ParcelFileDescriptor tunFd, NetworkCapabilities nc,
+ LinkProperties lp, L2capNetworkProvider.Dependencies deps, ICallback cb) {
+ mLogTag = logTag;
+ mHandler = handler;
+ mNetworkCapabilities = nc;
+
+ final L2capNetworkSpecifier spec = (L2capNetworkSpecifier) nc.getNetworkSpecifier();
+ final boolean compressHeaders = spec.getHeaderCompression() == HEADER_COMPRESSION_6LOWPAN;
+
+ mForwarder = deps.createL2capPacketForwarder(handler, tunFd, socket, compressHeaders,
+ () -> {
+ // TODO: add a check that this callback is invoked on the handler thread.
+ cb.onError(L2capNetwork.this);
+ });
+
+ final NetworkAgentConfig config = new NetworkAgentConfig.Builder().build();
+ mNetworkAgent = new NetworkAgent(context, mHandler.getLooper(), mLogTag,
+ nc, lp, NETWORK_SCORE, config, provider) {
+ @Override
+ public void onNetworkUnwanted() {
+ Log.i(mLogTag, "Network is unwanted");
+ // TODO: add a check that this callback is invoked on the handler thread.
+ cb.onNetworkUnwanted(L2capNetwork.this);
+ }
+ };
+ mNetworkAgent.register();
+ mNetworkAgent.markConnected();
+ }
+
+ /** Create an L2capNetwork or return null on failure. */
+ @Nullable
+ public static L2capNetwork create(Handler handler, Context context, NetworkProvider provider,
+ String ifname, BluetoothSocket socket, ParcelFileDescriptor tunFd,
+ NetworkCapabilities nc, L2capNetworkProvider.Dependencies deps, ICallback cb) {
+ // TODO: add a check that this function is invoked on the handler thread.
+ final String logTag = String.format("L2capNetwork[%s]", ifname);
+
+ // L2capIpClient#start() blocks until provisioning either succeeds (and returns
+ // LinkProperties) or fails (and returns null).
+ // Note that since L2capNetwork is using IPv6 link-local provisioning the most likely
+ // (only?) failure mode is due to the interface disappearing.
+ final LinkProperties lp = deps.createL2capIpClient(logTag, context, ifname).start();
+ if (lp == null) return null;
+
+ return new L2capNetwork(
+ logTag, handler, context, provider, socket, tunFd, nc, lp, deps, cb);
+ }
+
+ /** Get the NetworkCapabilities used for this Network */
+ public NetworkCapabilities getNetworkCapabilities() {
+ return mNetworkCapabilities;
+ }
+
+ /** Tear down the network and associated resources */
+ public void tearDown() {
+ mNetworkAgent.unregister();
+ mForwarder.tearDown();
+ }
+}
diff --git a/service/src/com/android/server/net/L2capPacketForwarder.java b/service/src/com/android/server/net/L2capPacketForwarder.java
new file mode 100644
index 0000000..8420d60
--- /dev/null
+++ b/service/src/com/android/server/net/L2capPacketForwarder.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static com.android.server.net.HeaderCompressionUtils.compress6lowpan;
+import static com.android.server.net.HeaderCompressionUtils.decompress6lowpan;
+
+import android.bluetooth.BluetoothSocket;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.BufferUnderflowException;
+
+/**
+ * Forwards packets from a BluetoothSocket of type L2CAP to a tun fd and vice versa.
+ *
+ * The forwarding logic operates on raw IP packets and there are no ethernet headers.
+ * Therefore, L3 MTU = L2 MTU.
+ */
+public class L2capPacketForwarder {
+ private static final String TAG = "L2capPacketForwarder";
+
+ // DCT specifies an MTU of 1500.
+ // TODO: Set /proc/sys/net/ipv6/conf/${iface}/mtu to 1280 and the link MTU to 1528 to accept
+ // slightly larger packets on ingress (i.e. packets passing through a NAT64 gateway).
+ // MTU determines the value of the read buffers, so use the larger of the two.
+ @VisibleForTesting
+ public static final int MTU = 1528;
+ private final Handler mHandler;
+ private final IReadWriteFd mTunFd;
+ private final IReadWriteFd mL2capFd;
+ private final L2capThread mIngressThread;
+ private final L2capThread mEgressThread;
+ private final ICallback mCallback;
+
+ public interface ICallback {
+ /** Called when an error is encountered; should tear down forwarding. */
+ void onError();
+ }
+
+ private interface IReadWriteFd {
+ /**
+ * Read up to len bytes into bytes[off] and return bytes actually read.
+ *
+ * bytes[] must be of size >= off + len.
+ */
+ int read(byte[] bytes, int off, int len) throws IOException;
+ /**
+ * Write len bytes starting from bytes[off]
+ *
+ * bytes[] must be of size >= off + len.
+ */
+ void write(byte[] bytes, int off, int len) throws IOException;
+ /** Disallow further receptions, shutdown(fd, SHUT_RD) */
+ void shutdownRead();
+ /** Disallow further transmissions, shutdown(fd, SHUT_WR) */
+ void shutdownWrite();
+ /** Close the fd */
+ void close();
+ }
+
+ @VisibleForTesting
+ public static class BluetoothSocketWrapper implements IReadWriteFd {
+ private final BluetoothSocket mSocket;
+ private final InputStream mInputStream;
+ private final OutputStream mOutputStream;
+
+ public BluetoothSocketWrapper(BluetoothSocket socket) {
+ // TODO: assert that MTU can fit within Bluetooth L2CAP SDU (maximum size of an L2CAP
+ // packet). The L2CAP SDU is 65535 by default, but can be less when using hardware
+ // offload.
+ mSocket = socket;
+ try {
+ mInputStream = socket.getInputStream();
+ mOutputStream = socket.getOutputStream();
+ } catch (IOException e) {
+ // Per the API docs, this should not actually be possible.
+ Log.wtf(TAG, "Failed to get Input/OutputStream", e);
+ // Fail hard.
+ throw new IllegalStateException("Failed to get Input/OutputStream");
+ }
+ }
+
+ /** Read from the BluetoothSocket. */
+ @Override
+ public int read(byte[] bytes, int off, int len) throws IOException {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ int bytesRead = mInputStream.read(bytes, off, len);
+ if (bytesRead < 0 || bytesRead > MTU) {
+ // Don't try to recover, just trigger network teardown. This might indicate a bug in
+ // the Bluetooth stack.
+ throw new IOException("Packet exceeds MTU or reached EOF. Read: " + bytesRead);
+ }
+ return bytesRead;
+ }
+
+ /** Write to the BluetoothSocket. */
+ @Override
+ public void write(byte[] bytes, int off, int len) throws IOException {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ mOutputStream.write(bytes, off, len);
+ }
+
+ @Override
+ public void shutdownRead() {
+ // BluetoothSocket does not expose methods to shutdown read / write individually;
+ // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+ // called multiple times from any thread.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "shutdownRead: Failed to close BluetoothSocket", e);
+ }
+ }
+
+ @Override
+ public void shutdownWrite() {
+ // BluetoothSocket does not expose methods to shutdown read / write individually;
+ // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+ // called multiple times from any thread.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "shutdownWrite: Failed to close BluetoothSocket", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ // BluetoothSocket#close() is safe to be called multiple times.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "close: Failed to close BluetoothSocket", e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public static class FdWrapper implements IReadWriteFd {
+ private final ParcelFileDescriptor mFd;
+
+ public FdWrapper(ParcelFileDescriptor fd) {
+ mFd = fd;
+ }
+
+ @Override
+ public int read(byte[] bytes, int off, int len) throws IOException {
+ try {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ return Os.read(mFd.getFileDescriptor(), bytes, off, len);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Write to BluetoothSocket.
+ */
+ @Override
+ public void write(byte[] bytes, int off, int len) throws IOException {
+ try {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ Os.write(mFd.getFileDescriptor(), bytes, off, len);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public void shutdownRead() {
+ try {
+ Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_RD);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "shutdownRead: Failed to shutdown(fd, SHUT_RD)", e);
+ }
+ }
+
+ @Override
+ public void shutdownWrite() {
+ try {
+ Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_WR);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "shutdownWrite: Failed to shutdown(fd, SHUT_WR)", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ // Safe to call multiple times. Both Os.close(FileDescriptor) and
+ // ParcelFileDescriptor#close() offer protection against double-closing an fd.
+ mFd.close();
+ } catch (IOException e) {
+ Log.w(TAG, "close: Failed to close fd", e);
+ }
+ }
+ }
+
+ private class L2capThread extends Thread {
+ // Set mBuffer length to MTU + 1 to catch read() overflows.
+ private final byte[] mBuffer = new byte[MTU + 1];
+ private volatile boolean mIsRunning = true;
+
+ private final String mLogTag;
+ private final IReadWriteFd mReadFd;
+ private final IReadWriteFd mWriteFd;
+ private final boolean mIsIngress;
+ private final boolean mCompressHeaders;
+
+ L2capThread(IReadWriteFd readFd, IReadWriteFd writeFd, boolean isIngress,
+ boolean compressHeaders) {
+ super("L2capNetworkProvider-ForwarderThread");
+ mLogTag = isIngress ? "L2capForwarderThread-Ingress" : "L2capForwarderThread-Egress";
+ mReadFd = readFd;
+ mWriteFd = writeFd;
+ mIsIngress = isIngress;
+ mCompressHeaders = compressHeaders;
+ }
+
+ private void postOnError() {
+ mHandler.post(() -> {
+ // All callbacks must be called on handler thread.
+ mCallback.onError();
+ });
+ }
+
+ @Override
+ public void run() {
+ while (mIsRunning) {
+ try {
+ int readBytes = mReadFd.read(mBuffer, 0 /*off*/, mBuffer.length);
+ // No bytes to write, continue.
+ if (readBytes <= 0) {
+ Log.w(mLogTag, "Zero-byte read encountered: " + readBytes);
+ continue;
+ }
+
+ if (mCompressHeaders) {
+ if (mIsIngress) {
+ readBytes = decompress6lowpan(mBuffer, readBytes);
+ } else {
+ readBytes = compress6lowpan(mBuffer, readBytes);
+ }
+ }
+
+ // If the packet is 0-length post de/compression or exceeds MTU, drop it.
+ // Note that a large read on BluetoothSocket throws an IOException to tear down
+ // the network.
+ if (readBytes <= 0 || readBytes > MTU) continue;
+
+ mWriteFd.write(mBuffer, 0 /*off*/, readBytes);
+ } catch (IOException|BufferUnderflowException e) {
+ Log.e(mLogTag, "L2capThread exception", e);
+ // Tear down the network on any error.
+ mIsRunning = false;
+ // Notify provider that forwarding has stopped.
+ postOnError();
+ }
+ }
+ }
+
+ public void tearDown() {
+ mIsRunning = false;
+ mReadFd.shutdownRead();
+ mWriteFd.shutdownWrite();
+ }
+ }
+
+ public L2capPacketForwarder(Handler handler, ParcelFileDescriptor tunFd, BluetoothSocket socket,
+ boolean compressHdrs, ICallback cb) {
+ this(handler, new FdWrapper(tunFd), new BluetoothSocketWrapper(socket), compressHdrs, cb);
+ }
+
+ @VisibleForTesting
+ L2capPacketForwarder(Handler handler, IReadWriteFd tunFd, IReadWriteFd l2capFd,
+ boolean compressHeaders, ICallback cb) {
+ mHandler = handler;
+ mTunFd = tunFd;
+ mL2capFd = l2capFd;
+ mCallback = cb;
+
+ mIngressThread = new L2capThread(l2capFd, tunFd, true /*isIngress*/, compressHeaders);
+ mEgressThread = new L2capThread(tunFd, l2capFd, false /*isIngress*/, compressHeaders);
+
+ mIngressThread.start();
+ mEgressThread.start();
+ }
+
+ /**
+ * Tear down the L2capPacketForwarder.
+ *
+ * This operation closes both the passed tun fd and BluetoothSocket.
+ **/
+ public void tearDown() {
+ // Destroying both threads first guarantees that both read and write side of FD have been
+ // shutdown.
+ mIngressThread.tearDown();
+ mEgressThread.tearDown();
+
+ // In order to interrupt a blocking read on the BluetoothSocket, the BluetoothSocket must be
+ // closed (which triggers shutdown()). This means, the BluetoothSocket must be closed inside
+ // L2capPacketForwarder. Tear down the tun fd alongside it for consistency.
+ mTunFd.close();
+ mL2capFd.close();
+
+ try {
+ mIngressThread.join();
+ } catch (InterruptedException e) {
+ // join() interrupted in tearDown path, do nothing.
+ }
+ try {
+ mEgressThread.join();
+ } catch (InterruptedException e) {
+ // join() interrupted in tearDown path, do nothing.
+ }
+ }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index a825b87..0eab6e7 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: [
@@ -348,7 +350,7 @@
// TODO: remove "apex_available:platform".
apex_available: [
"//apex_available:platform",
- "com.android.btservices",
+ "com.android.bt",
"com.android.tethering",
"com.android.wifi",
],
@@ -671,11 +673,12 @@
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: [
"device/**/*.java",
- ":framework-connectivity-shared-srcs",
":net-utils-framework-common-srcs",
],
visibility: ["//visibility:private"],
@@ -724,3 +727,34 @@
],
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/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index d99eedc..8b2fe58 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -149,21 +149,21 @@
}
/** Setup interface for tethering. */
- public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest)
- throws RemoteException, ServiceSpecificException {
- tetherInterface(netd, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
+ public static void tetherInterface(final INetd netd, int netId, final String iface,
+ final IpPrefix dest) throws RemoteException, ServiceSpecificException {
+ tetherInterface(netd, netId, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
}
/** Setup interface with configurable retries for tethering. */
- public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest,
- int maxAttempts, int pollingIntervalMs)
+ public static void tetherInterface(final INetd netd, int netId, final String iface,
+ final IpPrefix dest, int maxAttempts, int pollingIntervalMs)
throws RemoteException, ServiceSpecificException {
netd.tetherInterfaceAdd(iface);
- networkAddInterface(netd, iface, maxAttempts, pollingIntervalMs);
+ networkAddInterface(netd, netId, iface, maxAttempts, pollingIntervalMs);
// Activate a route to dest and IPv6 link local.
- modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ modifyRoute(netd, ModifyOperation.ADD, netId,
new RouteInfo(dest, null, iface, RTN_UNICAST));
- modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ modifyRoute(netd, ModifyOperation.ADD, netId,
new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
}
@@ -174,12 +174,12 @@
* in use in netd because the ConnectivityService thread hasn't processed the disconnect yet.
* See b/158269544 for detail.
*/
- private static void networkAddInterface(final INetd netd, final String iface,
+ private static void networkAddInterface(final INetd netd, int netId, final String iface,
int maxAttempts, int pollingIntervalMs)
throws ServiceSpecificException, RemoteException {
for (int i = 1; i <= maxAttempts; i++) {
try {
- netd.networkAddInterface(INetd.LOCAL_NET_ID, iface);
+ netd.networkAddInterface(netId, iface);
return;
} catch (ServiceSpecificException e) {
if (e.errorCode == EBUSY && i < maxAttempts) {
@@ -194,37 +194,38 @@
}
/** Reset interface for tethering. */
- public static void untetherInterface(final INetd netd, String iface)
+ public static void untetherInterface(final INetd netd, int netId, String iface)
throws RemoteException, ServiceSpecificException {
try {
netd.tetherInterfaceRemove(iface);
} finally {
- netd.networkRemoveInterface(INetd.LOCAL_NET_ID, iface);
+ netd.networkRemoveInterface(netId, iface);
}
}
- /** Add |routes| to local network. */
- public static void addRoutesToLocalNetwork(final INetd netd, final String iface,
+ /** Add |routes| to the given network. */
+ public static void addRoutesToNetwork(final INetd netd, int netId, final String iface,
final List<RouteInfo> routes) {
for (RouteInfo route : routes) {
if (!route.isDefaultRoute()) {
- modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID, route);
+ modifyRoute(netd, ModifyOperation.ADD, netId, route);
}
}
// IPv6 link local should be activated always.
- modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ modifyRoute(netd, ModifyOperation.ADD, netId,
new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
}
- /** Remove routes from local network. */
- public static int removeRoutesFromLocalNetwork(final INetd netd, final List<RouteInfo> routes) {
+ /** Remove routes from the given network. */
+ public static int removeRoutesFromNetwork(final INetd netd, int netId,
+ final List<RouteInfo> routes) {
int failures = 0;
for (RouteInfo route : routes) {
try {
- modifyRoute(netd, ModifyOperation.REMOVE, INetd.LOCAL_NET_ID, route);
+ modifyRoute(netd, ModifyOperation.REMOVE, netId, route);
} catch (IllegalStateException e) {
failures++;
}
diff --git a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
index 5069672..c2fbb56 100644
--- a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
+++ b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
@@ -16,7 +16,6 @@
package com.android.net.module.util;
-import static android.net.INetd.LOCAL_NET_ID;
import static android.system.OsConstants.EBUSY;
import static com.android.testutils.MiscAsserts.assertThrows;
@@ -63,6 +62,7 @@
private static final String IFACE = "TEST_IFACE";
private static final IpPrefix TEST_IPPREFIX = new IpPrefix("192.168.42.1/24");
+ private static final int TEST_NET_ID = 123;
@Before
public void setUp() throws Exception {
@@ -134,7 +134,7 @@
}
throw new ServiceSpecificException(EBUSY);
- }).when(mNetd).networkAddInterface(LOCAL_NET_ID, IFACE);
+ }).when(mNetd).networkAddInterface(TEST_NET_ID, IFACE);
}
class Counter {
@@ -163,7 +163,7 @@
setNetworkAddInterfaceOutcome(new ServiceSpecificException(expectedCode), expectedTries);
try {
- NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+ NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
fail("Expect throw ServiceSpecificException");
} catch (ServiceSpecificException e) {
assertEquals(e.errorCode, expectedCode);
@@ -177,7 +177,7 @@
setNetworkAddInterfaceOutcome(new RemoteException(), expectedTries);
try {
- NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+ NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
fail("Expect throw RemoteException");
} catch (RemoteException e) { }
@@ -187,18 +187,19 @@
private void verifyNetworkAddInterfaceFails(int expectedTries) throws Exception {
verify(mNetd).tetherInterfaceAdd(IFACE);
- verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
+ verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+
verifyNoMoreInteractions(mNetd);
}
private void verifyTetherInterfaceSucceeds(int expectedTries) throws Exception {
setNetworkAddInterfaceOutcome(null, expectedTries);
- NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX);
+ NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX);
verify(mNetd).tetherInterfaceAdd(IFACE);
- verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
- verify(mNetd, times(2)).networkAddRoute(eq(LOCAL_NET_ID), eq(IFACE), any(), any());
+ verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
+ verify(mNetd, times(2)).networkAddRoute(eq(TEST_NET_ID), eq(IFACE), any(), any());
verifyNoMoreInteractions(mNetd);
reset(mNetd);
}
diff --git a/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java b/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
index 7fcbd4e..bb95585 100644
--- a/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
+++ b/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
@@ -35,12 +35,12 @@
import android.net.NetworkCapabilities;
import android.os.RemoteException;
import android.util.ArrayMap;
-import android.util.IndentingPrintWriter;
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;
@@ -459,34 +459,27 @@
}
}
- // TODO: dump PrivateAddressCoordinator when dumping RoutingCoordinatorService.
- void dump(final IndentingPrintWriter pw) {
+ // TODO: dump PrivateAddressCoordinator when dumping RoutingCoordinatorService and apply
+ // indentation.
+ void dump(final PrintWriter 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 (LinkAddress downstream : mDownstreams.values()) {
pw.println(downstream);
}
- 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/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
new file mode 100644
index 0000000..c8fdf72
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
@@ -0,0 +1,345 @@
+/*
+ * 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 static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+import java.util.PriorityQueue;
+
+/**
+ * Represents a realtime scheduler object used for scheduling tasks with precise delays.
+ * Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed
+ * callbacks by accounting for periods when the device is in deep sleep.
+ *
+ * <p> This class is designed for use exclusively from the handler thread.
+ *
+ * **Usage Examples:**
+ *
+ * ** Scheduling recurring tasks with the same RealtimeScheduler **
+ *
+ * ```java
+ * // Create a RealtimeScheduler
+ * final RealtimeScheduler scheduler = new RealtimeScheduler(handler);
+ *
+ * // Schedule a new task with a delay.
+ * scheduler.postDelayed(() -> taskToExecute(), delayTime);
+ *
+ * // Once the delay has elapsed, and the task is running, schedule another task.
+ * scheduler.postDelayed(() -> anotherTaskToExecute(), anotherDelayTime);
+ *
+ * // Remember to close the RealtimeScheduler after all tasks have finished running.
+ * scheduler.close();
+ * ```
+ */
+public class RealtimeScheduler {
+ private static final String TAG = RealtimeScheduler.class.getSimpleName();
+ // EVENT_ERROR may be generated even if not specified, as per its javadoc.
+ private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+ private final CloseGuard mGuard = new CloseGuard();
+ @NonNull
+ private final Handler mHandler;
+ @NonNull
+ private final MessageQueue mQueue;
+ @NonNull
+ private final ParcelFileDescriptor mParcelFileDescriptor;
+ private final int mFdInt;
+
+ private final PriorityQueue<Task> mTaskQueue;
+
+ /**
+ * An abstract class for defining tasks that can be executed using a {@link Handler}.
+ */
+ private abstract static class Task implements Comparable<Task> {
+ private final long mRunTimeMs;
+ private final long mCreatedTimeNs = SystemClock.elapsedRealtimeNanos();
+
+ /**
+ * create a task with a run time
+ */
+ Task(long runTimeMs) {
+ mRunTimeMs = runTimeMs;
+ }
+
+ /**
+ * Executes the task using the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for executing the task.
+ */
+ abstract void post(Handler handler);
+
+ @Override
+ public int compareTo(@NonNull Task o) {
+ if (mRunTimeMs != o.mRunTimeMs) {
+ return Long.compare(mRunTimeMs, o.mRunTimeMs);
+ }
+ return Long.compare(mCreatedTimeNs, o.mCreatedTimeNs);
+ }
+
+ /**
+ * Returns the run time of the task.
+ */
+ public long getRunTimeMs() {
+ return mRunTimeMs;
+ }
+ }
+
+ /**
+ * A task that sends a {@link Message} using a {@link Handler}.
+ */
+ private static class MessageTask extends Task {
+ private final Message mMessage;
+
+ MessageTask(Message message, long runTimeMs) {
+ super(runTimeMs);
+ mMessage = message;
+ }
+
+ /**
+ * Sends the {@link Message} using the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for sending the message.
+ */
+ @Override
+ public void post(Handler handler) {
+ handler.sendMessage(mMessage);
+ }
+ }
+
+ /**
+ * A task that posts a {@link Runnable} to a {@link Handler}.
+ */
+ private static class RunnableTask extends Task {
+ private final Runnable mRunnable;
+
+ RunnableTask(Runnable runnable, long runTimeMs) {
+ super(runTimeMs);
+ mRunnable = runnable;
+ }
+
+ /**
+ * Posts the {@link Runnable} to the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for posting the runnable.
+ */
+ @Override
+ public void post(Handler handler) {
+ handler.post(mRunnable);
+ }
+ }
+
+ /**
+ * The RealtimeScheduler constructor
+ *
+ * Note: The constructor is currently safe to call on another thread because it only sets final
+ * members and registers the event to be called on the handler.
+ */
+ public RealtimeScheduler(@NonNull Handler handler) {
+ mFdInt = TimerFdUtils.createTimerFileDescriptor();
+ mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
+ mHandler = handler;
+ mQueue = handler.getLooper().getQueue();
+ mTaskQueue = new PriorityQueue<>();
+ registerFdEventListener();
+
+ mGuard.open("close");
+ }
+
+ private boolean enqueueTask(@NonNull Task task, long delayMs) {
+ ensureRunningOnCorrectThread();
+ if (delayMs <= 0L) {
+ task.post(mHandler);
+ return true;
+ }
+ if (mTaskQueue.isEmpty() || task.compareTo(mTaskQueue.peek()) < 0) {
+ if (!TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
+ return false;
+ }
+ }
+ mTaskQueue.add(task);
+ return true;
+ }
+
+ /**
+ * Set a runnable to be executed after a specified delay.
+ *
+ * If delayMs is less than or equal to 0, the runnable will be executed immediately.
+ *
+ * @param runnable the runnable to be executed
+ * @param delayMs the delay time in milliseconds
+ * @return true if the task is scheduled successfully, false otherwise.
+ */
+ public boolean postDelayed(@NonNull Runnable runnable, long delayMs) {
+ return enqueueTask(new RunnableTask(runnable, SystemClock.elapsedRealtime() + delayMs),
+ delayMs);
+ }
+
+ /**
+ * Remove a scheduled runnable.
+ *
+ * @param runnable the runnable to be removed
+ */
+ public void removeDelayedRunnable(@NonNull Runnable runnable) {
+ ensureRunningOnCorrectThread();
+ mTaskQueue.removeIf(task -> task instanceof RunnableTask
+ && ((RunnableTask) task).mRunnable == runnable);
+ }
+
+ /**
+ * Set a message to be sent after a specified delay.
+ *
+ * If delayMs is less than or equal to 0, the message will be sent immediately.
+ *
+ * @param msg the message to be sent
+ * @param delayMs the delay time in milliseconds
+ * @return true if the message is scheduled successfully, false otherwise.
+ */
+ public boolean sendDelayedMessage(Message msg, long delayMs) {
+
+ return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs);
+ }
+
+ /**
+ * Remove a scheduled message.
+ *
+ * @param what the message to be removed
+ */
+ public void removeDelayedMessage(int what) {
+ ensureRunningOnCorrectThread();
+ mTaskQueue.removeIf(task -> task instanceof MessageTask
+ && ((MessageTask) task).mMessage.what == what);
+ }
+
+ /**
+ * Close the RealtimeScheduler. This implementation closes the underlying
+ * OS resources allocated to represent this stream.
+ */
+ public void close() {
+ ensureRunningOnCorrectThread();
+ unregisterAndDestroyFd();
+ }
+
+ private void registerFdEventListener() {
+ mQueue.addOnFileDescriptorEventListener(
+ mParcelFileDescriptor.getFileDescriptor(),
+ FD_EVENTS,
+ (fd, events) -> {
+ if (!isRunning()) {
+ return 0;
+ }
+ if ((events & EVENT_ERROR) != 0) {
+ Log.wtf(TAG, "Got EVENT_ERROR from FileDescriptorEventListener.");
+ return 0;
+ }
+ if ((events & EVENT_INPUT) != 0) {
+ handleExpiration();
+ }
+ return FD_EVENTS;
+ });
+ }
+
+ private boolean isRunning() {
+ return mParcelFileDescriptor.getFileDescriptor().valid();
+ }
+
+ private void handleExpiration() {
+ // The data from the FileDescriptor must be read after the timer expires. Otherwise,
+ // expiration callbacks will continue to be sent, notifying of unread data. The content(the
+ // number of expirations) can be ignored, as the callback is the only item of interest.
+ // Refer to https://man7.org/linux/man-pages/man2/timerfd_create.2.html
+ // read(2)
+ // If the timer has already expired one or more times since
+ // its settings were last modified using timerfd_settime(),
+ // or since the last successful read(2), then the buffer
+ // given to read(2) returns an unsigned 8-byte integer
+ // (uint64_t) containing the number of expirations that have
+ // occurred. (The returned value is in host byte order—that
+ // is, the native byte order for integers on the host
+ // machine.)
+ final byte[] readBuffer = new byte[8];
+ try {
+ Os.read(mParcelFileDescriptor.getFileDescriptor(), readBuffer, 0, readBuffer.length);
+ } catch (IOException | ErrnoException exception) {
+ Log.wtf(TAG, "Read FileDescriptor failed. ", exception);
+ }
+
+ long currentTimeMs = SystemClock.elapsedRealtime();
+ while (!mTaskQueue.isEmpty()) {
+ final Task task = mTaskQueue.peek();
+ currentTimeMs = SystemClock.elapsedRealtime();
+ if (currentTimeMs < task.getRunTimeMs()) {
+ break;
+ }
+ task.post(mHandler);
+ mTaskQueue.poll();
+ }
+
+ if (!mTaskQueue.isEmpty()) {
+ // Using currentTimeMs ensures that the calculated expiration time
+ // is always positive.
+ if (!TimerFdUtils.setExpirationTime(mFdInt,
+ mTaskQueue.peek().getRunTimeMs() - currentTimeMs)) {
+ // If setting the expiration time fails, clear the task queue.
+ Log.wtf(TAG, "Failed to set expiration time");
+ mTaskQueue.clear();
+ }
+ }
+ }
+
+ private void unregisterAndDestroyFd() {
+ if (mGuard != null) {
+ mGuard.close();
+ }
+
+ mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor());
+ try {
+ mParcelFileDescriptor.close();
+ } catch (IOException exception) {
+ Log.e(TAG, "close ParcelFileDescriptor failed. ", exception);
+ }
+ }
+
+ private void ensureRunningOnCorrectThread() {
+ if (mHandler.getLooper() != Looper.myLooper()) {
+ throw new IllegalStateException(
+ "Not running on Handler thread: " + Thread.currentThread().getName());
+ }
+ }
+
+ @SuppressWarnings("Finalize")
+ @Override
+ protected void finalize() throws Throwable {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+ super.finalize();
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
new file mode 100644
index 0000000..1d3561a
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2025 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.system.ErrnoException;
+
+/**
+ * Contains JNI functions for use in service-connectivity
+ */
+public class ServiceConnectivityJni {
+ static {
+ final String libName = JniUtil.getJniLibraryName(ServiceConnectivityJni.class.getPackage());
+ if (libName.equals("android_net_connectivity_com_android_net_module_util_jni")) {
+ // 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(libName);
+ }
+ }
+
+ /**
+ * Create a timerfd.
+ *
+ * @throws ErrnoException if the timerfd creation is failed.
+ */
+ public static native int createTimerFd() throws ErrnoException;
+
+ /**
+ * Set given time to the timerfd.
+ *
+ * @param timeMs target time
+ * @throws ErrnoException if setting expiration time is failed.
+ */
+ public static native void setTimerFdTime(int fd, long timeMs) throws ErrnoException;
+
+ /** Create tun/tap interface */
+ public static native int createTunTap(boolean isTun, boolean hasCarrier,
+ boolean setIffMulticast, @NonNull String iface);
+
+ /** Enable carrier on tun/tap interface */
+ public static native void setTunTapCarrierEnabled(@NonNull String iface, int tunFd,
+ boolean enabled);
+
+ /** Bring up tun/tap interface */
+ public static native void bringUpInterface(String iface);
+}
diff --git a/staticlibs/device/com/android/net/module/util/SkDestroyListener.java b/staticlibs/device/com/android/net/module/util/SkDestroyListener.java
new file mode 100644
index 0000000..c7c2829
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SkDestroyListener.java
@@ -0,0 +1,113 @@
+/*
+ * 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.net.module.util;
+
+import static android.system.OsConstants.NETLINK_INET_DIAG;
+
+import android.os.Handler;
+
+import com.android.net.module.util.ip.NetlinkMonitor;
+import com.android.net.module.util.netlink.InetDiagMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * Monitor socket destroy and delete entry from cookie tag bpf map.
+ */
+public class SkDestroyListener extends NetlinkMonitor {
+ private static final int SKNLGRP_INET_TCP_DESTROY = 1;
+ private static final int SKNLGRP_INET_UDP_DESTROY = 2;
+ private static final int SKNLGRP_INET6_TCP_DESTROY = 3;
+ private static final int SKNLGRP_INET6_UDP_DESTROY = 4;
+
+ // TODO: if too many sockets are closed too quickly, this can overflow the socket buffer, and
+ // some entries in mCookieTagMap will not be freed. In order to fix this it would be needed to
+ // periodically dump all sockets and remove the tag entries for sockets that have been closed.
+ // For now, set a large-enough buffer that hundreds of sockets can be closed without getting
+ // ENOBUFS and leaking mCookieTagMap entries.
+ private static final int SOCK_RCV_BUF_SIZE = 512 * 1024;
+
+ private final Consumer<InetDiagMessage> mSkDestroyCallback;
+
+ /**
+ * Return SkDestroyListener that monitor both TCP and UDP socket destroy
+ *
+ * @param consumer The consumer that processes InetDiagMessage
+ * @param handler The Handler on which to poll for messages
+ * @param log A SharedLog to log to.
+ * @return SkDestroyListener
+ */
+ public static SkDestroyListener makeSkDestroyListener(final Consumer<InetDiagMessage> consumer,
+ final Handler handler, final SharedLog log) {
+ return makeSkDestroyListener(consumer, true /* monitorTcpSocket */,
+ true /* monitorUdpSocket */, handler, log);
+ }
+
+ /**
+ * Return SkDestroyListener that monitor socket destroy
+ *
+ * @param consumer The consumer that processes InetDiagMessage
+ * @param monitorTcpSocket {@code true} to monitor TCP socket destroy
+ * @param monitorUdpSocket {@code true} to monitor UDP socket destroy
+ * @param handler The Handler on which to poll for messages
+ * @param log A SharedLog to log to.
+ * @return SkDestroyListener
+ */
+ public static SkDestroyListener makeSkDestroyListener(final Consumer<InetDiagMessage> consumer,
+ final boolean monitorTcpSocket, final boolean monitorUdpSocket,
+ final Handler handler, final SharedLog log) {
+ if (!monitorTcpSocket && !monitorUdpSocket) {
+ throw new IllegalArgumentException(
+ "Both monitorTcpSocket and monitorUdpSocket can not be false");
+ }
+ int bindGroups = 0;
+ if (monitorTcpSocket) {
+ bindGroups |= 1 << (SKNLGRP_INET_TCP_DESTROY - 1)
+ | 1 << (SKNLGRP_INET6_TCP_DESTROY - 1);
+ }
+ if (monitorUdpSocket) {
+ bindGroups |= 1 << (SKNLGRP_INET_UDP_DESTROY - 1)
+ | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1);
+ }
+ return new SkDestroyListener(consumer, bindGroups, handler, log);
+ }
+
+ private SkDestroyListener(final Consumer<InetDiagMessage> consumer, final int bindGroups,
+ final Handler handler, final SharedLog log) {
+ super(handler, log, "SkDestroyListener", NETLINK_INET_DIAG,
+ bindGroups, SOCK_RCV_BUF_SIZE);
+ mSkDestroyCallback = consumer;
+ }
+
+ @Override
+ public void processNetlinkMessage(final NetlinkMessage nlMsg, final long whenMs) {
+ if (!(nlMsg instanceof InetDiagMessage)) {
+ mLog.e("Received non InetDiagMessage");
+ return;
+ }
+ mSkDestroyCallback.accept((InetDiagMessage) nlMsg);
+ }
+
+ /**
+ * Dump the contents of SkDestroyListener log.
+ */
+ public void dump(PrintWriter pw) {
+ mLog.reverseDump(pw);
+ }
+}
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..cce7efd
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -0,0 +1,52 @@
+/*
+ * 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.system.ErrnoException;
+import android.util.Log;
+
+/**
+ * Contains mostly timerfd functionality.
+ */
+public class TimerFdUtils {
+ private static final String TAG = TimerFdUtils.class.getSimpleName();
+
+ /**
+ * Create a timerfd
+ */
+ static int createTimerFileDescriptor() {
+ try {
+ return ServiceConnectivityJni.createTimerFd();
+ } catch (ErrnoException e) {
+ Log.e(TAG, "createTimerFd failed", e);
+ return -1;
+ }
+ }
+
+ /**
+ * Set expiration time to timerfd
+ */
+ static boolean setExpirationTime(int fd, long expirationTimeMs) {
+ try {
+ ServiceConnectivityJni.setTimerFdTime(fd, expirationTimeMs);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "setExpirationTime failed", e);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index fecaa09..c9a89ec 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -309,16 +309,18 @@
}
private static void sendNetlinkDestroyRequest(FileDescriptor fd, int proto,
- InetDiagMessage diagMsg) throws InterruptedIOException, ErrnoException {
+ StructInetDiagSockId id, short family, int state)
+ throws InterruptedIOException, ErrnoException {
+ // TODO: Investigate if it's fine to always set 0 to state and remove state from the arg
final byte[] destroyMsg = InetDiagMessage.inetDiagReqV2(
proto,
- diagMsg.inetDiagMsg.id,
- diagMsg.inetDiagMsg.idiag_family,
+ id,
+ family,
SOCK_DESTROY,
(short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_ACK),
0 /* pad */,
0 /* idiagExt */,
- 1 << diagMsg.inetDiagMsg.idiag_state
+ state
);
NetlinkUtils.sendMessage(fd, destroyMsg, 0, destroyMsg.length, IO_TIMEOUT_MS);
NetlinkUtils.receiveNetlinkAck(fd);
@@ -343,7 +345,8 @@
Consumer<InetDiagMessage> handleNlDumpMsg = (diagMsg) -> {
if (filter.test(diagMsg)) {
try {
- sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
+ sendNetlinkDestroyRequest(destroyFd, proto, diagMsg.inetDiagMsg.id,
+ diagMsg.inetDiagMsg.idiag_family, 1 << diagMsg.inetDiagMsg.idiag_state);
destroyedSockets.getAndIncrement();
} catch (InterruptedIOException | ErrnoException e) {
if (!(e instanceof ErrnoException
@@ -484,6 +487,30 @@
Log.d(TAG, "Destroyed live tcp sockets for uids=" + ownerUids + " in " + durationMs + "ms");
}
+ /**
+ * Close the udp socket which can be uniquely identified with the cookie and other information.
+ */
+ public static void destroyUdpSocket(final InetSocketAddress src, final InetSocketAddress dst,
+ final int ifIndex, final long cookie)
+ throws ErrnoException, SocketException, InterruptedIOException {
+ FileDescriptor fd = null;
+
+ try {
+ fd = NetlinkUtils.createNetLinkInetDiagSocket();
+ connectToKernel(fd);
+ final int family = (src.getAddress() instanceof Inet6Address) ? AF_INET6 : AF_INET;
+ final StructInetDiagSockId id = new StructInetDiagSockId(
+ src,
+ dst,
+ ifIndex,
+ cookie
+ );
+ sendNetlinkDestroyRequest(fd, IPPROTO_UDP, id, (short) family, 0 /* state */);
+ } finally {
+ closeSocketQuietly(fd);
+ }
+ }
+
@Override
public String toString() {
return "InetDiagMessage{ "
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index 781a04e..dfb2053 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -150,6 +150,8 @@
return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
case NetlinkConstants.RTM_NEWNDUSEROPT:
return (NetlinkMessage) NduseroptMessage.parse(nlmsghdr, byteBuffer);
+ case NetlinkConstants.RTM_NEWPREFIX:
+ return (NetlinkMessage) RtNetlinkPrefixMessage.parse(nlmsghdr, byteBuffer);
default: return null;
}
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index 541a375..2420e7a 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -55,6 +55,7 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
@@ -82,7 +83,7 @@
public static final int INET_DIAG_INFO = 2;
public static final int INET_DIAG_MARK = 15;
- public static final long IO_TIMEOUT_MS = 300L;
+ public static final long IO_TIMEOUT_MS = 3000L;
public static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
public static final int SOCKET_RECV_BUFSIZE = 64 * 1024;
@@ -469,4 +470,60 @@
// Nothing we can do here
}
}
+
+ /**
+ * Sends a netlink request to set flags for given interface
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param flags power-of-two integer flags to set or unset. A flag to set should be passed as
+ * is as a power-of-two value, and a flag to remove should be passed inversed as -1 with
+ * a single bit down. For example: IFF_UP, ~IFF_BROADCAST...
+ * @return true if the request finished successfully, otherwise false.
+ */
+ public static boolean setInterfaceFlags(@NonNull String interfaceName, int... flags) {
+ final RtNetlinkLinkMessage ntMsg =
+ RtNetlinkLinkMessage.createSetFlagsMessage(interfaceName, /*seqNo*/ 0, flags);
+ if (ntMsg == null) {
+ Log.e(TAG, "Failed to create message to set interface flags for interface "
+ + interfaceName + ", input flags are: " + Arrays.toString(flags));
+ return false;
+ }
+ final byte[] msg = ntMsg.pack(ByteOrder.nativeOrder());
+ try {
+ NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+ return true;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to set flags for: " + interfaceName, e);
+ return false;
+ }
+ }
+
+ /**
+ * Sends a netlink request to set MTU for given interface
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param mtu MTU value to set for the interface.
+ * @return true if the request finished successfully, otherwise false.
+ */
+ public static boolean setInterfaceMtu(@NonNull String interfaceName, int mtu) {
+ if (mtu < 68) {
+ Log.e(TAG, "Invalid mtu: " + mtu + ", mtu should be greater than 68 referring RFC791");
+ return false;
+ }
+ final RtNetlinkLinkMessage ntMsg =
+ RtNetlinkLinkMessage.createSetMtuMessage(interfaceName, /*seqNo*/ 0, mtu);
+ if (ntMsg == null) {
+ Log.e(TAG, "Failed to create message to set MTU to " + mtu
+ + "for interface " + interfaceName);
+ return false;
+ }
+ final byte[] msg = ntMsg.pack(ByteOrder.nativeOrder());
+ try {
+ NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+ return true;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to set MTU to " + mtu + " for: " + interfaceName, e);
+ return false;
+ }
+ }
}
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 037d95f..c19a124 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -312,6 +312,89 @@
DEFAULT_MTU, null, null);
}
+ /**
+ * Creates an {@link RtNetlinkLinkMessage} instance that can be used to set the flags of a
+ * network interface.
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param sequenceNumber The sequence number for the Netlink message.
+ * @param flags power-of-two integer flags to set or unset. A flag to set should be passed as
+ * is as a power-of-two value, and a flag to remove should be passed inversed as -1 with
+ * a single bit down. For example: IFF_UP, ~IFF_BROADCAST...
+ * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createSetFlagsMessage(@NonNull String interfaceName,
+ int sequenceNumber, int... flags) {
+ return createSetFlagsMessage(
+ interfaceName, sequenceNumber, new OsAccess(), flags);
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createSetFlagsMessage(
+ @NonNull String interfaceName, int sequenceNumber, @NonNull OsAccess osAccess,
+ int... flags) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+
+ int flagsBits = 0;
+ int changeBits = 0;
+ for (int f : flags) {
+ if (Integer.bitCount(f) == 1) {
+ flagsBits |= f;
+ changeBits |= f;
+ } else if (Integer.bitCount(~f) == 1) {
+ flagsBits &= f;
+ changeBits |= ~f;
+ } else {
+ return null;
+ }
+ }
+ // RTM_NEWLINK is used here for create, modify, or notify changes about a internet
+ // interface, including change in administrative state. While RTM_SETLINK is used to
+ // modify an existing link rather than creating a new one.
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(
+ /*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST_ACK, sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
+ flagsBits, changeBits),
+ DEFAULT_MTU, /*hardwareAddress*/ null, /*interfaceName*/ null);
+ }
+
+ /**
+ * Creates an {@link RtNetlinkLinkMessage} instance that can be used to set the MTU of a
+ * network interface.
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param sequenceNumber The sequence number for the Netlink message.
+ * @param mtu MTU value to set for the interface.
+ * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createSetMtuMessage(@NonNull String interfaceName,
+ int sequenceNumber, int mtu) {
+ return createSetMtuMessage(
+ interfaceName, sequenceNumber, mtu, new OsAccess());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createSetMtuMessage(@NonNull String interfaceName,
+ int sequenceNumber, int mtu, @NonNull OsAccess osAccess) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(/*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST_ACK , sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
+ /*flags*/ 0, /*change*/ 0),
+ mtu, /*hardwareAddress*/ null, /*interfaceName*/ null);
+ }
+
@Override
public String toString() {
return "RtNetlinkLinkMessage{ "
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java
new file mode 100644
index 0000000..30c63fb
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java
@@ -0,0 +1,160 @@
+/*
+ * 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.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink address messages.
+ *
+ * RtNetlinkPrefixMessage.parse() must be called with a ByteBuffer that contains exactly one
+ * netlink message.
+ *
+ * see also:
+ *
+ * include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class RtNetlinkPrefixMessage extends NetlinkMessage {
+ public static final short PREFIX_ADDRESS = 1;
+ public static final short PREFIX_CACHEINFO = 2;
+
+ @NonNull
+ private StructPrefixMsg mPrefixmsg;
+ @NonNull
+ private IpPrefix mPrefix;
+ private long mPreferredLifetime;
+ private long mValidLifetime;
+
+ @VisibleForTesting
+ public RtNetlinkPrefixMessage(@NonNull final StructNlMsgHdr header,
+ @NonNull final StructPrefixMsg prefixmsg,
+ @NonNull final IpPrefix prefix,
+ long preferred, long valid) {
+ super(header);
+ mPrefixmsg = prefixmsg;
+ mPrefix = prefix;
+ mPreferredLifetime = preferred;
+ mValidLifetime = valid;
+ }
+
+ private RtNetlinkPrefixMessage(@NonNull StructNlMsgHdr header) {
+ this(header, null, null, 0 /* preferredLifetime */, 0 /* validLifetime */);
+ }
+
+ @NonNull
+ public StructPrefixMsg getPrefixMsg() {
+ return mPrefixmsg;
+ }
+
+ @NonNull
+ public IpPrefix getPrefix() {
+ return mPrefix;
+ }
+
+ public long getPreferredLifetime() {
+ return mPreferredLifetime;
+ }
+
+ public long getValidLifetime() {
+ return mValidLifetime;
+ }
+
+ /**
+ * Parse rtnetlink prefix message from {@link ByteBuffer}. This method must be called with a
+ * ByteBuffer that contains exactly one netlink message.
+ *
+ * RTM_NEWPREFIX Message Format:
+ * +----------+- - -+-------------+- - -+---------------------+-----------------------+
+ * | nlmsghdr | Pad | prefixmsg | Pad | PREFIX_ADDRESS attr | PREFIX_CACHEINFO attr |
+ * +----------+- - -+-------------+- - -+---------------------+-----------------------+
+ *
+ * @param header netlink message header.
+ * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+ */
+ @Nullable
+ public static RtNetlinkPrefixMessage parse(@NonNull final StructNlMsgHdr header,
+ @NonNull final ByteBuffer byteBuffer) {
+ try {
+ final RtNetlinkPrefixMessage msg = new RtNetlinkPrefixMessage(header);
+ msg.mPrefixmsg = StructPrefixMsg.parse(byteBuffer);
+
+ // PREFIX_ADDRESS
+ final int baseOffset = byteBuffer.position();
+ StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(PREFIX_ADDRESS, byteBuffer);
+ if (nlAttr == null) return null;
+ final Inet6Address addr = (Inet6Address) nlAttr.getValueAsInetAddress();
+ if (addr == null) return null;
+ msg.mPrefix = new IpPrefix(addr, msg.mPrefixmsg.prefix_len);
+
+ // PREFIX_CACHEINFO
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(PREFIX_CACHEINFO, byteBuffer);
+ if (nlAttr == null) return null;
+ final ByteBuffer buffer = nlAttr.getValueAsByteBuffer();
+ if (buffer == null) return null;
+ final StructPrefixCacheInfo cacheinfo = StructPrefixCacheInfo.parse(buffer);
+ msg.mPreferredLifetime = cacheinfo.preferred_time;
+ msg.mValidLifetime = cacheinfo.valid_time;
+
+ return msg;
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Write a rtnetlink prefix message to {@link ByteBuffer}.
+ */
+ @VisibleForTesting
+ protected void pack(ByteBuffer byteBuffer) {
+ getHeader().pack(byteBuffer);
+ mPrefixmsg.pack(byteBuffer);
+
+ // PREFIX_ADDRESS attribute
+ final StructNlAttr prefixAddress =
+ new StructNlAttr(PREFIX_ADDRESS, mPrefix.getRawAddress());
+ prefixAddress.pack(byteBuffer);
+
+ // PREFIX_CACHEINFO attribute
+ final StructPrefixCacheInfo cacheinfo =
+ new StructPrefixCacheInfo(mPreferredLifetime, mValidLifetime);
+ final StructNlAttr prefixCacheinfo =
+ new StructNlAttr(PREFIX_CACHEINFO, cacheinfo.writeToBytes());
+ prefixCacheinfo.pack(byteBuffer);
+ }
+
+ @Override
+ public String toString() {
+ return "RtNetlinkPrefixMessage{ "
+ + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+ + "prefixmsg{" + mPrefixmsg.toString() + "}, "
+ + "IP Prefix{" + mPrefix + "}, "
+ + "preferred lifetime{" + mPreferredLifetime + "}, "
+ + "valid lifetime{" + mValidLifetime + "} "
+ + "}";
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
index f3d8c4a..760d849 100644
--- a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
@@ -22,6 +22,7 @@
import android.util.Pair;
import android.util.SparseArray;
+import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -413,4 +414,68 @@
}
return -1;
}
+
+ /**
+ * Concatenates multiple arrays of the same type into a single new array.
+ */
+ public static byte[] concatArrays(@NonNull byte[]... arr) {
+ int size = 0;
+ for (byte[] a : arr) {
+ size += a.length;
+ }
+ final byte[] result = new byte[size];
+ int offset = 0;
+ for (byte[] a : arr) {
+ System.arraycopy(a, 0, result, offset, a.length);
+ offset += a.length;
+ }
+ return result;
+ }
+
+ /**
+ * Concatenates multiple arrays of the same type into a single new array.
+ */
+ public static <T> T[] concatArrays(@NonNull Class<T> clazz, @NonNull T[]... arr) {
+ int size = 0;
+ for (T[] a : arr) {
+ size += a.length;
+ }
+ final T[] result = (T[]) Array.newInstance(clazz, size);
+ int offset = 0;
+ for (T[] a : arr) {
+ System.arraycopy(a, 0, result, offset, a.length);
+ offset += a.length;
+ }
+ return result;
+ }
+
+ /**
+ * Prepends the elements of a variable number of prefixes to an existing array (suffix).
+ */
+ public static byte[] prependArray(@NonNull byte[] suffix, @NonNull byte... prefixes) {
+ return concatArrays(prefixes, suffix);
+ }
+
+ /**
+ * Prepends the elements of a variable number of prefixes to an existing array (suffix).
+ */
+ public static <T> T[] prependArray(@NonNull Class<T> clazz, @NonNull T[] suffix,
+ @NonNull T... prefixes) {
+ return concatArrays(clazz, prefixes, suffix);
+ }
+
+ /**
+ * Appends the elements of a variable number of suffixes to an existing array (prefix).
+ */
+ public static byte[] appendArray(@NonNull byte[] prefix, @NonNull byte... suffixes) {
+ return concatArrays(prefix, suffixes);
+ }
+
+ /**
+ * Appends the elements of a variable number of suffixes to an existing array (prefix).
+ */
+ public static <T> T[] appendArray(@NonNull Class<T> clazz, @NonNull T[] prefix,
+ @NonNull T... suffixes) {
+ return concatArrays(clazz, prefix, suffixes);
+ }
}
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/HexDump.java b/staticlibs/framework/com/android/net/module/util/HexDump.java
index a22c258..409f611 100644
--- a/staticlibs/framework/com/android/net/module/util/HexDump.java
+++ b/staticlibs/framework/com/android/net/module/util/HexDump.java
@@ -202,7 +202,7 @@
if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
- throw new RuntimeException("Invalid hex char '" + c + "'");
+ throw new IllegalArgumentException("Invalid hex char '" + c + "'");
}
/**
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..96d995a
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
@@ -0,0 +1,169 @@
+/*
+ * 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.util.Objects;
+import java.util.function.LongSupplier;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * A thread-safe 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 LongSupplier mTimeSupplier;
+ 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 timeSupplier The {@link java.util.function.LongSupplier} 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 LongSupplier timeSupplier, long expiryDurationMs,
+ int maxSize, Predicate<V> shouldCacheValue) {
+ mTimeSupplier = timeSupplier;
+ 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<>(mTimeSupplier.getAsLong(), value));
+ }
+ }
+
+ /**
+ * Stores a value in the cache if absent, associated with the given key.
+ *
+ * @param key The key to associate with the value.
+ * @param value The value to store in the cache.
+ * @return The existing value associated with the key, if present; otherwise, null.
+ */
+ @Nullable
+ public V putIfAbsent(@NonNull K key, @NonNull V value) {
+ Objects.requireNonNull(value);
+ synchronized (mMap) {
+ final V existingValue = get(key);
+ if (existingValue == null) {
+ put(key, value);
+ }
+ return existingValue;
+ }
+ }
+
+ /**
+ * Clear the cache.
+ */
+ public void clear() {
+ synchronized (mMap) {
+ mMap.evictAll();
+ }
+ }
+
+ private boolean isExpired(long timestamp) {
+ return mTimeSupplier.getAsLong() > 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..8d7ae5c 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -23,6 +23,7 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.util.List;
/**
* Network constants used by the network stack.
@@ -100,6 +101,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(
@@ -111,6 +121,8 @@
(byte) 0, (byte) 0, (byte) 0, (byte) 0,
(byte) 0, (byte) 0, (byte) 0, (byte) 0,
(byte) 0, (byte) 0, (byte) 0, (byte) 0 });
+ public static final Inet4Address IPV4_ADDR_ALL_HOST_MULTICAST =
+ (Inet4Address) InetAddresses.parseNumericAddress("224.0.0.1");
/**
* CLAT constants
@@ -142,7 +154,8 @@
(Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
public static final Inet6Address IPV6_ADDR_ALL_HOSTS_MULTICAST =
(Inet6Address) InetAddresses.parseNumericAddress("ff02::3");
-
+ public static final Inet6Address IPV6_ADDR_NODE_LOCAL_ALL_NODES_MULTICAST =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff01::1");
public static final int IPPROTO_FRAGMENT = 44;
/**
@@ -329,6 +342,26 @@
*/
public static final String TEST_URL_EXPIRATION_TIME = "test_url_expiration_time";
+ /**
+ * List of IpPrefix that are local network prefixes.
+ */
+ public static final List<IpPrefix> IPV4_LOCAL_PREFIXES = List.of(
+ new IpPrefix("169.254.0.0/16"), // Link Local
+ new IpPrefix("100.64.0.0/10"), // CGNAT
+ new IpPrefix("10.0.0.0/8"), // RFC1918
+ new IpPrefix("172.16.0.0/12"), // RFC1918
+ new IpPrefix("192.168.0.0/16") // RFC1918
+ );
+
+ /**
+ * List of IpPrefix that are multicast and broadcast prefixes.
+ */
+ public static final List<IpPrefix> MULTICAST_AND_BROADCAST_PREFIXES = List.of(
+ new IpPrefix("224.0.0.0/4"), // Multicast
+ new IpPrefix("ff00::/8"), // Multicast
+ new IpPrefix("255.255.255.255/32") // Broadcast
+ );
+
// TODO: Move to Inet4AddressUtils
// See aosp/1455936: NetworkStackConstants can't depend on it as it causes jarjar-related issues
// for users of both the net-utils-device-common and net-utils-framework-common libraries.
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/framework/com/android/net/module/util/TerribleErrorLog.java b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
new file mode 100644
index 0000000..b4f7642
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
@@ -0,0 +1,41 @@
+/*
+ * 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.util.Log;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Utility class for logging terrible errors and reporting them for tracking.
+ *
+ * @hide
+ */
+public class TerribleErrorLog {
+
+ private static final String TAG = TerribleErrorLog.class.getSimpleName();
+
+ /**
+ * Logs a terrible error and reports metrics through a provided statsLog.
+ */
+ public static void logTerribleError(@NonNull BiConsumer<Integer, Integer> statsLog,
+ @NonNull String message, int protoType, int errorType) {
+ statsLog.accept(protoType, errorType);
+ Log.wtf(TAG, message);
+ }
+}
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/serviceconnectivityjni/Android.bp
similarity index 86%
rename from staticlibs/native/timerfdutils/Android.bp
rename to staticlibs/native/serviceconnectivityjni/Android.bp
index 939a2d2..18246dd 100644
--- a/staticlibs/native/timerfdutils/Android.bp
+++ b/staticlibs/native/serviceconnectivityjni/Android.bp
@@ -18,17 +18,20 @@
}
cc_library_static {
- name: "libnet_utils_device_common_timerfdjni",
+ name: "libserviceconnectivityjni",
srcs: [
- "com_android_net_module_util_TimerFdUtils.cpp",
+ "com_android_net_module_util_ServiceConnectivityJni.cpp",
],
header_libs: [
+ "bpf_headers",
"jni_headers",
+ "libbase_headers",
],
shared_libs: [
"liblog",
"libnativehelper_compat_libc++",
],
+ stl: "libc++_static",
cflags: [
"-Wall",
"-Werror",
diff --git a/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp b/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp
new file mode 100644
index 0000000..8767589
--- /dev/null
+++ b/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp
@@ -0,0 +1,209 @@
+/*
+ * 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 <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <jni.h>
+#include <linux/if.h>
+#include <linux/if_tun.h>
+#include <linux/ipv6_route.h>
+#include <linux/route.h>
+#include <netinet/in.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <string>
+#include <sys/epoll.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/timerfd.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <android-base/unique_fd.h>
+#include <bpf/KernelUtils.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/scoped_utf_chars.h>
+
+#define MSEC_PER_SEC 1000
+#define NSEC_PER_MSEC 1000000
+
+#ifndef IFF_NO_CARRIER
+#define IFF_NO_CARRIER 0x0040
+#endif
+
+namespace android {
+
+static jint createTimerFd(JNIEnv *env, jclass clazz) {
+ int tfd;
+ // For safety, the file descriptor should have O_NONBLOCK(TFD_NONBLOCK) set
+ // using fcntl during creation. This ensures that, in the worst-case scenario,
+ // an EAGAIN error is returned when reading.
+ tfd = timerfd_create(CLOCK_BOOTTIME, TFD_NONBLOCK);
+ if (tfd == -1) {
+ jniThrowErrnoException(env, "createTimerFd", tfd);
+ }
+ return tfd;
+}
+
+static void setTimerFdTime(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, "setTimerFdTime", ret);
+ }
+}
+
+static void throwException(JNIEnv *env, int error, const char *action,
+ const char *iface) {
+ const std::string &msg = "Error: " + std::string(action) + " " +
+ std::string(iface) + ": " +
+ std::string(strerror(error));
+ jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
+}
+
+// enable or disable carrier on tun / tap interface.
+static void setTunTapCarrierEnabledImpl(JNIEnv *env, const char *iface,
+ int tunFd, bool enabled) {
+ uint32_t carrierOn = enabled;
+ if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
+ throwException(env, errno, "set carrier", iface);
+ }
+}
+
+static int createTunTapImpl(JNIEnv *env, bool isTun, bool hasCarrier,
+ bool setIffMulticast, const char *iface) {
+ base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
+ ifreq ifr{};
+
+ // Allocate interface.
+ ifr.ifr_flags = (isTun ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
+ if (!hasCarrier) {
+ // Using IFF_NO_CARRIER is supported starting in kernel version >= 6.0
+ // Up until then, unsupported flags are ignored.
+ if (!bpf::isAtLeastKernelVersion(6, 0, 0)) {
+ throwException(env, EOPNOTSUPP, "IFF_NO_CARRIER not supported",
+ ifr.ifr_name);
+ return -1;
+ }
+ ifr.ifr_flags |= IFF_NO_CARRIER;
+ }
+ strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
+ if (ioctl(tun.get(), TUNSETIFF, &ifr)) {
+ throwException(env, errno, "allocating", ifr.ifr_name);
+ return -1;
+ }
+
+ // Mark some TAP interfaces as supporting multicast
+ if (setIffMulticast && !isTun) {
+ base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
+ ifr.ifr_flags = IFF_MULTICAST;
+
+ if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
+ throwException(env, errno, "set IFF_MULTICAST", ifr.ifr_name);
+ return -1;
+ }
+ }
+
+ return tun.release();
+}
+
+static void bringUpInterfaceImpl(JNIEnv *env, const char *iface) {
+ // Activate interface using an unconnected datagram socket.
+ base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
+
+ ifreq ifr{};
+ strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
+ if (ioctl(inet6CtrlSock.get(), SIOCGIFFLAGS, &ifr)) {
+ throwException(env, errno, "read flags", iface);
+ return;
+ }
+ ifr.ifr_flags |= IFF_UP;
+ if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
+ throwException(env, errno, "set IFF_UP", iface);
+ return;
+ }
+}
+
+//------------------------------------------------------------------------------
+
+static void setTunTapCarrierEnabled(JNIEnv *env, jclass /* clazz */,
+ jstring jIface, jint tunFd,
+ jboolean enabled) {
+ ScopedUtfChars iface(env, jIface);
+ if (!iface.c_str()) {
+ jniThrowNullPointerException(env, "iface");
+ return;
+ }
+ setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
+}
+
+static jint createTunTap(JNIEnv *env, jclass /* clazz */, jboolean isTun,
+ jboolean hasCarrier, jboolean setIffMulticast,
+ jstring jIface) {
+ ScopedUtfChars iface(env, jIface);
+ if (!iface.c_str()) {
+ jniThrowNullPointerException(env, "iface");
+ return -1;
+ }
+
+ return createTunTapImpl(env, isTun, hasCarrier, setIffMulticast,
+ iface.c_str());
+}
+
+static void bringUpInterface(JNIEnv *env, jclass /* clazz */, jstring jIface) {
+ ScopedUtfChars iface(env, jIface);
+ if (!iface.c_str()) {
+ jniThrowNullPointerException(env, "iface");
+ return;
+ }
+ bringUpInterfaceImpl(env, iface.c_str());
+}
+
+//------------------------------------------------------------------------------
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"createTimerFd", "()I", (void *)createTimerFd},
+ {"setTimerFdTime", "(IJ)V", (void *)setTimerFdTime},
+ {"setTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V",
+ (void *)setTunTapCarrierEnabled},
+ {"createTunTap", "(ZZZLjava/lang/String;)I", (void *)createTunTap},
+ {"bringUpInterface", "(Ljava/lang/String;)V", (void *)bringUpInterface},
+};
+
+int register_com_android_net_module_util_ServiceConnectivityJni(
+ JNIEnv *env, char const *class_name) {
+ return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/native/tcutils/tcutils.cpp b/staticlibs/native/tcutils/tcutils.cpp
index 21e781c..5425d0e 100644
--- a/staticlibs/native/tcutils/tcutils.cpp
+++ b/staticlibs/native/tcutils/tcutils.cpp
@@ -361,7 +361,7 @@
const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
-int sendAndProcessNetlinkResponse(const void *req, int len) {
+int sendAndProcessNetlinkResponse(const void *req, int len, bool enoent_ok) {
// TODO: use unique_fd instead of ScopeGuard
unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
if (!fd.ok()) {
@@ -445,7 +445,9 @@
return -ENOMSG;
}
- if (resp.e.error) {
+ if (resp.e.error == -ENOENT) {
+ if (!enoent_ok) ALOGE("NLMSG_ERROR message returned ENOENT");
+ } else if (resp.e.error) {
ALOGE("NLMSG_ERROR message return error: %d", resp.e.error);
}
return resp.e.error; // returns 0 on success
@@ -560,7 +562,8 @@
};
#undef CLSACT
- return sendAndProcessNetlinkResponse(&req, sizeof(req));
+ const bool enoent_ok = (nlMsgType == RTM_DELQDISC);
+ return sendAndProcessNetlinkResponse(&req, sizeof(req), enoent_ok);
}
// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
@@ -666,7 +669,7 @@
snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]",
basename(bpfProgPath));
- int error = sendAndProcessNetlinkResponse(&req, sizeof(req));
+ int error = sendAndProcessNetlinkResponse(&req, sizeof(req), false);
return error;
}
@@ -698,7 +701,8 @@
return error;
}
return sendAndProcessNetlinkResponse(filter.getRequest(),
- filter.getRequestSize());
+ filter.getRequestSize(),
+ false);
}
// tc filter del dev .. in/egress prio .. protocol ..
@@ -726,7 +730,7 @@
},
};
- return sendAndProcessNetlinkResponse(&req, sizeof(req));
+ return sendAndProcessNetlinkResponse(&req, sizeof(req), true);
}
} // namespace android
diff --git a/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp b/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
deleted file mode 100644
index c4c960d..0000000
--- a/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
+++ /dev/null
@@ -1,79 +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.
- */
-
-#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/netd/Android.bp b/staticlibs/netd/Android.bp
index 44abba2..03f5f06 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -22,7 +22,7 @@
sdk_version: "system_current",
min_sdk_version: "30",
static_libs: [
- "netd_aidl_interface-V15-java",
+ "netd_aidl_interface-V16-java",
],
apex_available: [
"//apex_available:platform", // used from services.net
@@ -45,7 +45,7 @@
cc_library_static {
name: "netd_aidl_interface-lateststable-ndk",
whole_static_libs: [
- "netd_aidl_interface-V15-ndk",
+ "netd_aidl_interface-V16-ndk",
],
apex_available: [
"com.android.resolv",
@@ -56,12 +56,12 @@
cc_defaults {
name: "netd_aidl_interface_lateststable_cpp_static",
- static_libs: ["netd_aidl_interface-V15-cpp"],
+ static_libs: ["netd_aidl_interface-V16-cpp"],
}
cc_defaults {
name: "netd_aidl_interface_lateststable_cpp_shared",
- shared_libs: ["netd_aidl_interface-V15-cpp"],
+ shared_libs: ["netd_aidl_interface-V16-cpp"],
}
aidl_interface {
@@ -171,6 +171,10 @@
version: "15",
imports: [],
},
+ {
+ version: "16",
+ imports: [],
+ },
],
frozen: true,
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash
new file mode 100644
index 0000000..08cd338
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/.hash
@@ -0,0 +1 @@
+28e20632b92e146787d32437a53aaa5ad39125b7
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl
new file mode 100644
index 0000000..8351b56
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetd.aidl
@@ -0,0 +1,272 @@
+/**
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetd {
+ boolean isAlive();
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
+ boolean bandwidthEnableDataSaver(boolean enable);
+ /**
+ * @deprecated use networkCreate() instead.
+ */
+ void networkCreatePhysical(int netId, int permission);
+ /**
+ * @deprecated use networkCreate() instead.
+ */
+ void networkCreateVpn(int netId, boolean secure);
+ void networkDestroy(int netId);
+ void networkAddInterface(int netId, in @utf8InCpp String iface);
+ void networkRemoveInterface(int netId, in @utf8InCpp String iface);
+ void networkAddUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+ void networkRemoveUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+ void networkRejectNonSecureVpn(boolean add, in android.net.UidRangeParcel[] uidRanges);
+ void socketDestroy(in android.net.UidRangeParcel[] uidRanges, in int[] exemptUids);
+ boolean tetherApplyDnsInterfaces();
+ android.net.TetherStatsParcel[] tetherGetStats();
+ void interfaceAddAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+ void interfaceDelAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+ @utf8InCpp String getProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter);
+ void setProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter, in @utf8InCpp String value);
+ void ipSecSetEncapSocketOwner(in ParcelFileDescriptor socket, int newUid);
+ int ipSecAllocateSpi(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+ void ipSecAddSecurityAssociation(int transformId, int mode, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int underlyingNetId, int spi, int markValue, int markMask, in @utf8InCpp String authAlgo, in byte[] authKey, in int authTruncBits, in @utf8InCpp String cryptAlgo, in byte[] cryptKey, in int cryptTruncBits, in @utf8InCpp String aeadAlgo, in byte[] aeadKey, in int aeadIcvBits, int encapType, int encapLocalPort, int encapRemotePort, int interfaceId);
+ void ipSecDeleteSecurityAssociation(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecApplyTransportModeTransform(in ParcelFileDescriptor socket, int transformId, int direction, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+ void ipSecRemoveTransportModeTransform(in ParcelFileDescriptor socket);
+ void ipSecAddSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecUpdateSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecDeleteSecurityPolicy(int transformId, int selAddrFamily, int direction, int markValue, int markMask, int interfaceId);
+ void ipSecAddTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+ void ipSecUpdateTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+ void ipSecRemoveTunnelInterface(in @utf8InCpp String deviceName);
+ void wakeupAddInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+ void wakeupDelInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+ void setIPv6AddrGenMode(in @utf8InCpp String ifName, int mode);
+ void idletimerAddInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+ void idletimerRemoveInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+ void strictUidCleartextPenalty(int uid, int policyPenalty);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+ */
+ @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+ */
+ void clatdStop(in @utf8InCpp String ifName);
+ boolean ipfwdEnabled();
+ @utf8InCpp String[] ipfwdGetRequesterList();
+ void ipfwdEnableForwarding(in @utf8InCpp String requester);
+ void ipfwdDisableForwarding(in @utf8InCpp String requester);
+ void ipfwdAddInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+ void ipfwdRemoveInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+ void bandwidthSetInterfaceQuota(in @utf8InCpp String ifName, long bytes);
+ void bandwidthRemoveInterfaceQuota(in @utf8InCpp String ifName);
+ void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
+ void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
+ void bandwidthSetGlobalAlert(long bytes);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthAddNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthRemoveNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthAddNiceApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthRemoveNiceApp(int uid);
+ void tetherStart(in @utf8InCpp String[] dhcpRanges);
+ void tetherStop();
+ boolean tetherIsEnabled();
+ void tetherInterfaceAdd(in @utf8InCpp String ifName);
+ void tetherInterfaceRemove(in @utf8InCpp String ifName);
+ @utf8InCpp String[] tetherInterfaceList();
+ void tetherDnsSet(int netId, in @utf8InCpp String[] dnsAddrs);
+ @utf8InCpp String[] tetherDnsList();
+ void networkAddRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+ void networkRemoveRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+ void networkAddLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+ void networkRemoveLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+ int networkGetDefault();
+ void networkSetDefault(int netId);
+ void networkClearDefault();
+ void networkSetPermissionForNetwork(int netId, int permission);
+ void networkSetPermissionForUser(int permission, in int[] uids);
+ void networkClearPermissionForUser(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void trafficSetNetPermForUids(int permission, in int[] uids);
+ void networkSetProtectAllow(int uid);
+ void networkSetProtectDeny(int uid);
+ boolean networkCanProtect(int uid);
+ void firewallSetFirewallType(int firewalltype);
+ void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallSetUidRule(int childChain, int uid, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallEnableChildChain(int childChain, boolean enable);
+ @utf8InCpp String[] interfaceGetList();
+ android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
+ void interfaceSetCfg(in android.net.InterfaceConfigurationParcel cfg);
+ void interfaceSetIPv6PrivacyExtensions(in @utf8InCpp String ifName, boolean enable);
+ void interfaceClearAddrs(in @utf8InCpp String ifName);
+ void interfaceSetEnableIPv6(in @utf8InCpp String ifName, boolean enable);
+ void interfaceSetMtu(in @utf8InCpp String ifName, int mtu);
+ void tetherAddForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+ void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+ void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
+ void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallRemoveUidInterfaceRules(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void trafficSwapActiveStatsMap();
+ IBinder getOemNetd();
+ void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
+ android.net.MarkMaskParcel getFwmarkForNetwork(int netId);
+ void networkAddRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ void networkUpdateRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ void networkRemoveRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ android.net.TetherStatsParcel[] tetherOffloadGetStats();
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+ void networkCreate(in android.net.NativeNetworkConfig config);
+ void networkAddUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+ void networkRemoveUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+ void ipSecMigrate(in android.net.IpSecMigrateInfoParcel migrateInfo);
+ void setNetworkAllowlist(in android.net.netd.aidl.NativeUidRangeConfig[] allowedNetworks);
+ void networkAllowBypassVpnOnNetwork(boolean allow, int uid, int netId);
+ const int IPV4 = 4;
+ const int IPV6 = 6;
+ const int CONF = 1;
+ const int NEIGH = 2;
+ const String IPSEC_INTERFACE_PREFIX = "ipsec";
+ const int IPV6_ADDR_GEN_MODE_EUI64 = 0;
+ const int IPV6_ADDR_GEN_MODE_NONE = 1;
+ const int IPV6_ADDR_GEN_MODE_STABLE_PRIVACY = 2;
+ const int IPV6_ADDR_GEN_MODE_RANDOM = 3;
+ const int IPV6_ADDR_GEN_MODE_DEFAULT = 0;
+ const int PENALTY_POLICY_ACCEPT = 1;
+ const int PENALTY_POLICY_LOG = 2;
+ const int PENALTY_POLICY_REJECT = 3;
+ const int CLAT_MARK = 0xdeadc1a7;
+ const int LOCAL_NET_ID = 99;
+ const int DUMMY_NET_ID = 51;
+ const int UNREACHABLE_NET_ID = 52;
+ const String NEXTHOP_NONE = "";
+ const String NEXTHOP_UNREACHABLE = "unreachable";
+ const String NEXTHOP_THROW = "throw";
+ const int PERMISSION_NONE = 0;
+ const int PERMISSION_NETWORK = 1;
+ const int PERMISSION_SYSTEM = 2;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int NO_PERMISSIONS = 0;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int PERMISSION_INTERNET = 4;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+ /**
+ * @deprecated usage is internal to module.
+ */
+ const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
+ /**
+ * @deprecated use FIREWALL_ALLOWLIST.
+ */
+ const int FIREWALL_WHITELIST = 0;
+ const int FIREWALL_ALLOWLIST = 0;
+ /**
+ * @deprecated use FIREWALL_DENYLIST.
+ */
+ const int FIREWALL_BLACKLIST = 1;
+ const int FIREWALL_DENYLIST = 1;
+ const int FIREWALL_RULE_ALLOW = 1;
+ const int FIREWALL_RULE_DENY = 2;
+ const int FIREWALL_CHAIN_NONE = 0;
+ const int FIREWALL_CHAIN_DOZABLE = 1;
+ const int FIREWALL_CHAIN_STANDBY = 2;
+ const int FIREWALL_CHAIN_POWERSAVE = 3;
+ const int FIREWALL_CHAIN_RESTRICTED = 4;
+ const String IF_STATE_UP = "up";
+ const String IF_STATE_DOWN = "down";
+ const String IF_FLAG_BROADCAST = "broadcast";
+ const String IF_FLAG_LOOPBACK = "loopback";
+ const String IF_FLAG_POINTOPOINT = "point-to-point";
+ const String IF_FLAG_RUNNING = "running";
+ const String IF_FLAG_MULTICAST = "multicast";
+ const int IPSEC_DIRECTION_IN = 0;
+ const int IPSEC_DIRECTION_OUT = 1;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2018, 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetdUnsolicitedEventListener {
+ oneway void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs, int uid);
+ oneway void onQuotaLimitReached(@utf8InCpp String alertName, @utf8InCpp String ifName);
+ oneway void onInterfaceDnsServerInfo(@utf8InCpp String ifName, long lifetimeS, in @utf8InCpp String[] servers);
+ oneway void onInterfaceAddressUpdated(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+ oneway void onInterfaceAddressRemoved(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+ oneway void onInterfaceAdded(@utf8InCpp String ifName);
+ oneway void onInterfaceRemoved(@utf8InCpp String ifName);
+ oneway void onInterfaceChanged(@utf8InCpp String ifName, boolean up);
+ oneway void onInterfaceLinkStateChanged(@utf8InCpp String ifName, boolean up);
+ oneway void onRouteChanged(boolean updated, @utf8InCpp String route, @utf8InCpp String gateway, @utf8InCpp String ifName);
+ oneway void onStrictCleartextDetected(int uid, @utf8InCpp String hex);
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable InterfaceConfigurationParcel {
+ @utf8InCpp String ifName;
+ @utf8InCpp String hwAddr;
+ @utf8InCpp String ipv4Addr;
+ int prefixLength;
+ @utf8InCpp String[] flags;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/IpSecMigrateInfoParcel.aidl
@@ -0,0 +1,45 @@
+/**
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaOnlyImmutable
+parcelable IpSecMigrateInfoParcel {
+ int requestId;
+ int selAddrFamily;
+ int direction;
+ @utf8InCpp String oldSourceAddress;
+ @utf8InCpp String oldDestinationAddress;
+ @utf8InCpp String newSourceAddress;
+ @utf8InCpp String newDestinationAddress;
+ int interfaceId;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable MarkMaskParcel {
+ int mark;
+ int mask;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeNetworkConfig {
+ int netId;
+ android.net.NativeNetworkType networkType = android.net.NativeNetworkType.PHYSICAL;
+ int permission;
+ boolean secure;
+ android.net.NativeVpnType vpnType = android.net.NativeVpnType.PLATFORM;
+ boolean excludeLocalRoutes = false;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeNetworkType.aidl
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeNetworkType {
+ PHYSICAL = 0,
+ VIRTUAL = 1,
+ PHYSICAL_LOCAL = 2,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/NativeVpnType.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeVpnType {
+ SERVICE = 1,
+ PLATFORM = 2,
+ LEGACY = 3,
+ OEM = 4,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,40 @@
+/**
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable RouteInfoParcel {
+ @utf8InCpp String destination;
+ @utf8InCpp String ifName;
+ @utf8InCpp String nextHop;
+ int mtu;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherConfigParcel {
+ boolean usingLegacyDnsProxy;
+ @utf8InCpp String[] dhcpRanges;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherOffloadRuleParcel {
+ int inputInterfaceIndex;
+ int outputInterfaceIndex;
+ byte[] destination;
+ int prefixLength;
+ byte[] srcL2Address;
+ byte[] dstL2Address;
+ int pmtu = 1500;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherStatsParcel {
+ @utf8InCpp String iface;
+ long rxBytes;
+ long rxPackets;
+ long txBytes;
+ long txPackets;
+ int ifIndex = 0;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/UidRangeParcel.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable UidRangeParcel {
+ int start;
+ int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/16/android/net/netd/aidl/NativeUidRangeConfig.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.netd.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeUidRangeConfig {
+ int netId;
+ android.net.UidRangeParcel[] uidRanges;
+ int subPriority;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
index 80b3b62..8351b56 100644
--- a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
@@ -227,9 +227,21 @@
const int PERMISSION_NONE = 0;
const int PERMISSION_NETWORK = 1;
const int PERMISSION_SYSTEM = 2;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int NO_PERMISSIONS = 0;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int PERMISSION_INTERNET = 4;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+ /**
+ * @deprecated usage is internal to module.
+ */
const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
/**
* @deprecated use FIREWALL_ALLOWLIST.
diff --git a/staticlibs/netd/binder/android/net/INetd.aidl b/staticlibs/netd/binder/android/net/INetd.aidl
index e4c63b9..be8f538 100644
--- a/staticlibs/netd/binder/android/net/INetd.aidl
+++ b/staticlibs/netd/binder/android/net/INetd.aidl
@@ -933,24 +933,27 @@
/**
* NO_PERMISSIONS indicates that this app is installed and doesn't have either
* PERMISSION_INTERNET or PERMISSION_UPDATE_DEVICE_STATS.
- * TODO: use PERMISSION_NONE to represent this case
+ * @deprecated usage is internal to module.
*/
const int NO_PERMISSIONS = 0;
/**
- * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets
+ * PERMISSION_INTERNET indicates that the app can create AF_INET and AF_INET6 sockets.
+ * @deprecated usage is internal to module.
*/
const int PERMISSION_INTERNET = 4;
/**
* PERMISSION_UPDATE_DEVICE_STATS is used for system UIDs and privileged apps
- * that have the UPDATE_DEVICE_STATS permission
+ * that have the UPDATE_DEVICE_STATS permission.
+ * @deprecated usage is internal to module.
*/
const int PERMISSION_UPDATE_DEVICE_STATS = 8;
/**
* PERMISSION_UNINSTALLED is used when an app is uninstalled from the device. All internet
- * related permissions need to be cleaned
+ * related permissions need to be cleaned.
+ * @deprecated usage is internal to module.
*/
const int PERMISSION_UNINSTALLED = -1;
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 8c54e6a..f4f1ea9 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -28,6 +28,7 @@
"net-utils-device-common-struct-base",
"net-utils-device-common-wear",
"net-utils-service-connectivity",
+ "truth",
],
libs: [
"android.test.runner.stubs",
@@ -55,6 +56,7 @@
// For mockito extended
"libdexmakerjvmtiagent",
"libstaticjvmtiagent",
+ "libcom_android_net_moduletests_util_jni",
],
jarjar_rules: "jarjar-rules.txt",
test_suites: ["device-tests"],
diff --git a/staticlibs/native/timerfdutils/Android.bp b/staticlibs/tests/unit/jni/Android.bp
similarity index 66%
copy from staticlibs/native/timerfdutils/Android.bp
copy to staticlibs/tests/unit/jni/Android.bp
index 939a2d2..c444159 100644
--- a/staticlibs/native/timerfdutils/Android.bp
+++ b/staticlibs/tests/unit/jni/Android.bp
@@ -14,33 +14,26 @@
package {
default_team: "trendy_team_fwk_core_networking",
+ // See: http://go/android-license-faq
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++",
- ],
+cc_library_shared {
+ name: "libcom_android_net_moduletests_util_jni",
cflags: [
"-Wall",
"-Werror",
"-Wno-unused-parameter",
+ "-Wthread-safety",
],
- sdk_version: "current",
- min_sdk_version: "30",
- apex_available: [
- "com.android.tethering",
- "//apex_available:platform",
+ srcs: [
+ "com_android_net_moduletests_util/onload.cpp",
],
- visibility: [
- "//packages/modules/Connectivity:__subpackages__",
+ static_libs: [
+ "libserviceconnectivityjni",
+ ],
+ shared_libs: [
+ "liblog",
+ "libnativehelper",
],
}
diff --git a/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
new file mode 100644
index 0000000..af4810f
--- /dev/null
+++ b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
@@ -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.
+ */
+
+#include "jni.h"
+#include <nativehelper/JNIHelp.h>
+
+#define LOG_TAG "NetworkStaticLibTestsJni"
+#include <android/log.h>
+
+namespace android {
+
+int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env,
+ char const *class_name);
+
+extern "C" jint JNI_OnLoad(JavaVM *vm, void *) {
+ JNIEnv *env;
+ if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed");
+ return JNI_ERR;
+ }
+
+ if (register_com_android_net_module_util_ServiceConnectivityJni(
+ env, "com/android/net/moduletests/util/ServiceConnectivityJni") < 0)
+ return JNI_ERR;
+
+ return JNI_VERSION_1_6;
+}
+
+}; // namespace android
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..1aa943e 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
@@ -19,7 +19,7 @@
import android.util.SparseArray
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.assertThrows
+import kotlin.test.assertContentEquals
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
@@ -183,17 +183,101 @@
@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))
+ }
+
+ @Test
+ fun testConcatEmptyByteArrays() {
+ assertContentEquals(
+ byteArrayOf(),
+ CollectionUtils.concatArrays(byteArrayOf(), byteArrayOf())
+ )
+ }
+
+ @Test
+ fun testConcatEmptyStringArrays() {
+ assertContentEquals(
+ arrayOf<String>(),
+ CollectionUtils.concatArrays(
+ String::class.java,
+ arrayOf<String>(),
+ arrayOf<String>()
+ )
+ )
+ }
+
+ @Test
+ fun testConcatByteArrays() {
+ val byteArr1 = byteArrayOf(1, 2, 3)
+ val byteArr2 = byteArrayOf(4, 5, 6)
+ val byteArr3 = byteArrayOf()
+ val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+ assertContentEquals(
+ byteArrExpected,
+ CollectionUtils.concatArrays(byteArr1, byteArr2, byteArr3)
+ )
+ }
+
+ @Test
+ fun testConcatStringArrays() {
+ val stringArr1 = arrayOf("1", "2", "3")
+ val stringArr2 = arrayOf("4", "5", "6")
+ val strinvArr3 = arrayOf<String>()
+ val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+ assertContentEquals(
+ stringArrExpected,
+ CollectionUtils.concatArrays(String::class.java, stringArr1, stringArr2, strinvArr3)
+ )
+ }
+
+ @Test
+ fun testPrependByteArrays() {
+ val byteArr2 = byteArrayOf(4, 5, 6)
+ val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+ assertContentEquals(
+ byteArrExpected,
+ CollectionUtils.prependArray(byteArr2, 1, 2, 3)
+ )
+ }
+
+ @Test
+ fun testPrependStringArrays() {
+ val stringArr2 = arrayOf("4", "5", "6")
+ val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+ assertContentEquals(
+ stringArrExpected,
+ CollectionUtils.prependArray(String::class.java, stringArr2, "1", "2", "3")
+ )
+ }
+
+ @Test
+ fun testAppendByteArrays() {
+ val byteArr1 = byteArrayOf(1, 2, 3)
+ val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+ assertContentEquals(
+ byteArrExpected,
+ CollectionUtils.appendArray(byteArr1, 4, 5, 6)
+ )
+ }
+
+ @Test
+ fun testAppendStringArrays() {
+ val stringArr1 = arrayOf("1", "2", "3")
+ val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+ assertContentEquals(
+ stringArrExpected,
+ CollectionUtils.appendArray(String::class.java, stringArr1, "4", "5", "6")
+ )
}
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
index 5a15585..f81978a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -50,6 +51,11 @@
}
@Test
+ public void testInvalidHexStringToByteArray() {
+ assertThrows(IllegalArgumentException.class, () -> HexDump.hexStringToByteArray("abxX"));
+ }
+
+ @Test
public void testIntegerToByteArray() {
assertArrayEquals(new byte[]{(byte) 0xff, (byte) 0x00, (byte) 0x00, (byte) 0x04},
HexDump.toByteArray((int) 0xff000004));
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt
new file mode 100644
index 0000000..b6af892
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.function.LongSupplier
+
+@RunWith(DevSdkIgnoreRunner::class)
+class LruCacheWithExpiryTest {
+
+ companion object {
+ private const val CACHE_SIZE = 2
+ private const val EXPIRY_DURATION_MS = 1000L
+ }
+
+ private val timeSupplier = object : LongSupplier {
+ private var currentTimeMillis = 0L
+ override fun getAsLong(): Long = currentTimeMillis
+ fun advanceTime(millis: Long) { currentTimeMillis += millis }
+ }
+
+ private val cache = LruCacheWithExpiry<Int, String>(
+ timeSupplier, EXPIRY_DURATION_MS, CACHE_SIZE) { true }
+
+ @Test
+ fun testPutIfAbsent_keyNotPresent() {
+ val value = cache.putIfAbsent(1, "value1")
+ assertNull(value)
+ assertEquals("value1", cache.get(1))
+ }
+
+ @Test
+ fun testPutIfAbsent_keyPresent() {
+ cache.put(1, "value1")
+ val value = cache.putIfAbsent(1, "value2")
+ assertEquals("value1", value)
+ assertEquals("value1", cache.get(1))
+ }
+
+ @Test
+ fun testPutIfAbsent_keyPresentButExpired() {
+ cache.put(1, "value1")
+ // Advance time to expire the entry
+ timeSupplier.advanceTime(EXPIRY_DURATION_MS + 1)
+ val value = cache.putIfAbsent(1, "value2")
+ assertNull(value)
+ assertEquals("value2", cache.get(1))
+ }
+
+ @Test
+ fun testPutIfAbsent_maxSizeReached() {
+ cache.put(1, "value1")
+ cache.put(2, "value2")
+ cache.putIfAbsent(3, "value3") // This should evict the least recently used entry (1)
+ assertNull(cache.get(1))
+ assertEquals("value2", cache.get(2))
+ assertEquals("value3", cache.get(3))
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt
new file mode 100644
index 0000000..30b530f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.Build
+import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+import android.os.SystemClock
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.android.testutils.visibleOnHandlerThread
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class RealtimeSchedulerTest {
+
+ private val TIMEOUT_MS = 1000L
+ private val TOLERANCE_MS = 50L
+ private class TestHandler(looper: Looper) : Handler(looper) {
+ override fun handleMessage(msg: Message) {
+ val pair = msg.obj as Pair<ConditionVariable, MutableList<Long>>
+ val cv = pair.first
+ cv.open()
+ val executionTimes = pair.second
+ executionTimes.add(SystemClock.elapsedRealtime())
+ }
+ }
+ private val thread = HandlerThread(RealtimeSchedulerTest::class.simpleName).apply { start() }
+ private val handler by lazy { TestHandler(thread.looper) }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ @Test
+ fun testMultiplePostDelayedTasks() {
+ val scheduler = RealtimeScheduler(handler)
+ tryTest {
+ val initialTimeMs = SystemClock.elapsedRealtime()
+ val executionTimes = mutableListOf<Long>()
+ val cv = ConditionVariable()
+ handler.post {
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 0)
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 200)
+ val toBeRemoved = Runnable {
+ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
+ }
+ scheduler.postDelayed(toBeRemoved, 250)
+ scheduler.removeDelayedRunnable(toBeRemoved)
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 100)
+ scheduler.postDelayed({
+ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
+ cv.open() }, 300)
+ }
+ cv.block(TIMEOUT_MS)
+ assertEquals(4, executionTimes.size)
+ assertThat(executionTimes[0]).isIn(Range.closed(0L, TOLERANCE_MS))
+ assertThat(executionTimes[1]).isIn(Range.closed(100L, 100 + TOLERANCE_MS))
+ assertThat(executionTimes[2]).isIn(Range.closed(200L, 200 + TOLERANCE_MS))
+ assertThat(executionTimes[3]).isIn(Range.closed(300L, 300 + TOLERANCE_MS))
+ } cleanup {
+ visibleOnHandlerThread(handler) { scheduler.close() }
+ }
+ }
+
+ @Test
+ fun testMultipleSendDelayedMessages() {
+ val scheduler = RealtimeScheduler(handler)
+ tryTest {
+ val MSG_ID_0 = 0
+ val MSG_ID_1 = 1
+ val MSG_ID_2 = 2
+ val MSG_ID_3 = 3
+ val MSG_ID_4 = 4
+ val initialTimeMs = SystemClock.elapsedRealtime()
+ val executionTimes = mutableListOf<Long>()
+ val cv = ConditionVariable()
+ handler.post {
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_0, Pair(ConditionVariable(), executionTimes)), 0)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_1, Pair(ConditionVariable(), executionTimes)),
+ 200)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_4, Pair(ConditionVariable(), executionTimes)),
+ 250)
+ scheduler.removeDelayedMessage(MSG_ID_4)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_2, Pair(ConditionVariable(), executionTimes)),
+ 100)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_3, Pair(cv, executionTimes)),
+ 300)
+ }
+ cv.block(TIMEOUT_MS)
+ assertEquals(4, executionTimes.size)
+ assertThat(executionTimes[0] - initialTimeMs).isIn(Range.closed(0L, TOLERANCE_MS))
+ assertThat(executionTimes[1] - initialTimeMs)
+ .isIn(Range.closed(100L, 100 + TOLERANCE_MS))
+ assertThat(executionTimes[2] - initialTimeMs)
+ .isIn(Range.closed(200L, 200 + TOLERANCE_MS))
+ assertThat(executionTimes[3] - initialTimeMs)
+ .isIn(Range.closed(300L, 300 + TOLERANCE_MS))
+ } cleanup {
+ visibleOnHandlerThread(handler) { scheduler.close() }
+ }
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/SkDestroyListenerTest.kt
similarity index 89%
rename from tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/SkDestroyListenerTest.kt
index 18785e5..e4b47fe 100644
--- a/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SkDestroyListenerTest.kt
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.server.net
+package com.android.net.module.util
import android.os.Handler
import android.os.HandlerThread
-import com.android.net.module.util.SharedLog
+import com.android.net.module.util.SkDestroyListener.makeSkDestroyListener
import com.android.testutils.DevSdkIgnoreRunner
import java.io.PrintWriter
import org.junit.After
@@ -54,7 +54,7 @@
doReturn(sharedLog).`when`(sharedLog).forSubComponent(any())
val handler = Handler(handlerThread.looper)
- val skDestroylistener = SkDestroyListener(null /* cookieTagMap */, handler, sharedLog)
+ val skDestroylistener = makeSkDestroyListener({} /* consumer */, handler, sharedLog)
val pw = PrintWriter(System.out)
skDestroylistener.dump(pw)
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
new file mode 100644
index 0000000..5fd634e
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
@@ -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 com.android.net.module.util
+
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.testutils.tryTest
+import kotlin.test.assertContentEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TerribleErrorLogTest {
+ @Test
+ fun testLogTerribleError() {
+ val wtfCaptures = mutableListOf<String>()
+ val prevHandler = Log.setWtfHandler { tag, what, system ->
+ wtfCaptures.add("$tag,${what.message}")
+ }
+ val statsLogCapture = mutableListOf<Pair<Int, Int>>()
+ val testStatsLog = object {
+ fun write(protoType: Int, errorType: Int) {
+ statsLogCapture.add(protoType to errorType)
+ }
+ }
+ tryTest {
+ TerribleErrorLog.logTerribleError(testStatsLog::write, "error", 1, 2)
+ assertContentEquals(listOf(1 to 2), statsLogCapture)
+ assertContentEquals(listOf("TerribleErrorLog,error"), wtfCaptures)
+ } cleanup {
+ Log.setWtfHandler(prevHandler)
+ }
+ }
+}
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 bd0e31d..13710b1 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
@@ -306,6 +306,50 @@
}
@Test
+ public void testCreateSetInterfaceFlagsMessage() {
+ final String expectedHexBytes =
+ "20000000100005006824000000000000" // struct nlmsghdr
+ + "00000000080000000100000001000100"; // 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.createSetFlagsMessage(
+ interfaceName,
+ sequenceNumber,
+ mOsAccess,
+ NetlinkConstants.IFF_UP,
+ ~NetlinkConstants.IFF_LOWER_UP);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
+ public void testCreateSetInterfaceMtuMessage() {
+ final String expectedHexBytes =
+ "280000001000050068240000000000000000000008000000" // struct nlmsghdr
+ + "000000000000000008000400DC050000"; // struct ifinfomsg
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+ final int mtu = 1500;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetMtuMessage(
+ interfaceName,
+ sequenceNumber,
+ mtu,
+ 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/net/module/util/netlink/RtNetlinkPrefixMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java
new file mode 100644
index 0000000..b1779cb
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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 static android.system.OsConstants.NETLINK_ROUTE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.IpPrefix;
+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 java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkPrefixMessageTest {
+ private static final IpPrefix TEST_PREFIX = new IpPrefix("2001:db8:1:1::/64");
+
+ // An example of the full RTM_NEWPREFIX message.
+ private static final String RTM_NEWPREFIX_HEX =
+ "3C000000340000000000000000000000" // struct nlmsghr
+ + "0A0000002F00000003400300" // struct prefixmsg
+ + "1400010020010DB8000100010000000000000000" // PREFIX_ADDRESS
+ + "0C000200803A0900008D2700"; // PREFIX_CACHEINFO
+
+ private ByteBuffer toByteBuffer(final String hexString) {
+ return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+ }
+
+ @Test
+ public void testParseRtmNewPrefix() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkPrefixMessage);
+ final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+
+ final StructNlMsgHdr hdr = prefixmsg.getHeader();
+ assertNotNull(hdr);
+ assertEquals(60, hdr.nlmsg_len);
+ assertEquals(NetlinkConstants.RTM_NEWPREFIX, hdr.nlmsg_type);
+ assertEquals(0, hdr.nlmsg_flags);
+ assertEquals(0, hdr.nlmsg_seq);
+ assertEquals(0, hdr.nlmsg_pid);
+
+ final StructPrefixMsg prefixmsgHdr = prefixmsg.getPrefixMsg();
+ assertNotNull(prefixmsgHdr);
+ assertEquals((byte) OsConstants.AF_INET6, prefixmsgHdr.prefix_family);
+ assertEquals(3, prefixmsgHdr.prefix_type);
+ assertEquals(64, prefixmsgHdr.prefix_len);
+ assertEquals(0x03, prefixmsgHdr.prefix_flags);
+ assertEquals(0x2F, prefixmsgHdr.prefix_ifindex);
+
+ assertEquals(prefixmsg.getPrefix(), TEST_PREFIX);
+ assertEquals(604800L, prefixmsg.getPreferredLifetime());
+ assertEquals(2592000L, prefixmsg.getValidLifetime());
+ }
+
+ @Test
+ public void testPackRtmNewPrefix() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkPrefixMessage);
+ final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+
+ final ByteBuffer packBuffer = ByteBuffer.allocate(60);
+ packBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ prefixmsg.pack(packBuffer);
+ assertEquals(RTM_NEWPREFIX_HEX, HexDump.toHexString(packBuffer.array()));
+ }
+
+ private static final String RTM_NEWPREFIX_WITHOUT_PREFIX_ADDRESS_HEX =
+ "24000000340000000000000000000000" // struct nlmsghr
+ + "0A0000002F00000003400300" // struct prefixmsg
+ + "0C000200803A0900008D2700"; // PREFIX_CACHEINFO
+
+ @Test
+ public void testParseRtmNewPrefix_withoutPrefixAddressAttribute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_WITHOUT_PREFIX_ADDRESS_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNull(msg);
+ }
+
+ private static final String RTM_NEWPREFIX_WITHOUT_PREFIX_CACHEINFO_HEX =
+ "30000000340000000000000000000000" // struct nlmsghr
+ + "0A0000002F00000003400300" // struct prefixmsg
+ + "140001002A0079E10ABCF6050000000000000000"; // PREFIX_ADDRESS
+
+ @Test
+ public void testParseRtmNewPrefix_withoutPrefixCacheinfoAttribute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_WITHOUT_PREFIX_CACHEINFO_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNull(msg);
+ }
+
+ private static final String RTM_NEWPREFIX_TRUNCATED_PREFIX_ADDRESS_HEX =
+ "3C000000340000000000000000000000" // struct nlmsghr
+ + "0A0000002F00000003400300" // struct prefixmsg
+ + "140001002A0079E10ABCF605000000000000" // PREFIX_ADDRESS (truncated)
+ + "0C000200803A0900008D2700"; // PREFIX_CACHEINFO
+
+ @Test
+ public void testParseRtmNewPrefix_truncatedPrefixAddressAttribute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_TRUNCATED_PREFIX_ADDRESS_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNull(msg);
+ }
+
+ private static final String RTM_NEWPREFIX_TRUNCATED_PREFIX_CACHEINFO_HEX =
+ "3C000000340000000000000000000000" // struct nlmsghr
+ + "0A0000002F00000003400300" // struct prefixmsg
+ + "140001002A0079E10ABCF6050000000000000000" // PREFIX_ADDRESS
+ + "0C000200803A0900008D"; // PREFIX_CACHEINFO (truncated)
+
+ @Test
+ public void testParseRtmNewPrefix_truncatedPrefixCacheinfoAttribute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_TRUNCATED_PREFIX_CACHEINFO_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNull(msg);
+ }
+
+ @Test
+ public void testToString() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkPrefixMessage);
+ final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+ final String expected = "RtNetlinkPrefixMessage{ "
+ + "nlmsghdr{StructNlMsgHdr{ nlmsg_len{60}, nlmsg_type{52(RTM_NEWPREFIX)}, "
+ + "nlmsg_flags{0()}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+ + "prefixmsg{prefix_family: 10, prefix_ifindex: 47, prefix_type: 3, "
+ + "prefix_len: 64, prefix_flags: 3}, "
+ + "IP Prefix{2001:db8:1:1::/64}, "
+ + "preferred lifetime{604800}, valid lifetime{2592000} }";
+ assertEquals(expected, prefixmsg.toString());
+ }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index f4ed9e4..ec486fb 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -93,15 +93,13 @@
libs: ["tradefed"],
test_suites: [
"ats",
+ "automotive-general-tests",
"device-tests",
"general-tests",
"cts",
"mts-networking",
- "mcts-networking",
"mts-tethering",
"mcts-tethering",
- "mcts-wifi",
- "mcts-dnsresolver",
],
device_common_data: [":ConnectivityTestPreparer"],
}
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..c42d9e5 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,180 @@
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.InetAddresses.parseNumericAddress
+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
+ ) {
+ if (it !is LinkPropertiesChanged || it.network != network) {
+ false
+ } else {
+ // Same check as used by DnsResolver for AI_ADDRCONFIG (have_ipv4)
+ val ipv4Reachable = it.lp.isReachable(parseNumericAddress("8.8.8.8"))
+ // Same check as used by DnsResolver for AI_ADDRCONFIG (have_ipv6)
+ val ipv6Reachable = it.lp.isReachable(parseNumericAddress("2000::"))
+ ipv4Reachable && (ipv6Unsupported(ssid) || ipv6Reachable)
+ }
+ }
+ assertNotNull(
+ lpChange,
+ "Wifi network $network needs an IPv4 address and default route" +
+ if (ipv6Unsupported(ssid)) {
+ ""
+ } else {
+ " and a global IPv6 address and default route"
+ }
+ )
+
+ 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 +206,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/com/android/testutils/CarrierConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
new file mode 100644
index 0000000..ae0de79
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/CarrierConfigRule.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2025 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.com.android.testutils
+
+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.os.ConditionVariable
+import android.os.PersistableBundle
+import android.telephony.CarrierConfigManager
+import android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
+import android.telephony.SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel.isAtLeastU
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+private val TAG = CarrierConfigRule::class.simpleName
+private const val CARRIER_CONFIG_CHANGE_TIMEOUT_MS = 10_000L
+
+/**
+ * A [TestRule] that helps set [CarrierConfigManager] overrides for tests and clean up the test
+ * configuration automatically on teardown.
+ */
+class CarrierConfigRule : TestRule {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val ccm by lazy { context.getSystemService(CarrierConfigManager::class.java) }
+
+ // Map of (subId) -> (original values of overridden settings)
+ private val originalConfigs = mutableMapOf<Int, PersistableBundle>()
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return CarrierConfigStatement(base, description)
+ }
+
+ private inner class CarrierConfigStatement(
+ private val base: Statement,
+ private val description: Description
+ ) : Statement() {
+ override fun evaluate() {
+ tryTest {
+ base.evaluate()
+ } cleanup {
+ cleanUpNow()
+ }
+ }
+ }
+
+ private class ConfigChangeReceiver(private val subId: Int) : BroadcastReceiver() {
+ val cv = ConditionVariable()
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != ACTION_CARRIER_CONFIG_CHANGED ||
+ intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, -1) != subId) {
+ return
+ }
+ // This may race with other config changes for the same subId, but there is no way to
+ // know which update is being reported, and querying the override would return the
+ // latest values even before the config is applied. Config changes should be rare, so it
+ // is unlikely they would happen exactly after the override applied here and cause
+ // flakes.
+ cv.open()
+ }
+ }
+
+ private fun overrideConfigAndWait(subId: Int, config: PersistableBundle) {
+ val changeReceiver = ConfigChangeReceiver(subId)
+ context.registerReceiver(changeReceiver, IntentFilter(ACTION_CARRIER_CONFIG_CHANGED))
+ ccm.overrideConfig(subId, config)
+ assertTrue(
+ changeReceiver.cv.block(CARRIER_CONFIG_CHANGE_TIMEOUT_MS),
+ "Timed out waiting for config change for subId $subId"
+ )
+ context.unregisterReceiver(changeReceiver)
+ }
+
+ /**
+ * Add carrier config overrides with the specified configuration.
+ *
+ * The overrides will automatically be cleaned up when the test case finishes.
+ */
+ fun addConfigOverrides(subId: Int, config: PersistableBundle) {
+ val originalConfig = originalConfigs.computeIfAbsent(subId) { PersistableBundle() }
+ val overrideKeys = config.keySet()
+ val previousValues = runAsShell(READ_PHONE_STATE) {
+ ccm.getConfigForSubIdCompat(subId, overrideKeys)
+ }
+ // If a key is already in the originalConfig, keep the oldest original overrides
+ originalConfig.keySet().forEach {
+ previousValues.remove(it)
+ }
+ originalConfig.putAll(previousValues)
+
+ runAsShell(MODIFY_PHONE_STATE) {
+ overrideConfigAndWait(subId, config)
+ }
+ }
+
+ /**
+ * Cleanup overrides that were added by the test case.
+ *
+ * This will be called automatically on test teardown, so it does not need to be called by the
+ * test case unless cleaning up earlier is required.
+ */
+ fun cleanUpNow() {
+ runAsShell(MODIFY_PHONE_STATE) {
+ originalConfigs.forEach { (subId, config) ->
+ try {
+ // Do not use null as the config to reset, as it would reset configs that may
+ // have been set by target preparers such as
+ // ConnectivityTestTargetPreparer / CarrierConfigSetupTest.
+ overrideConfigAndWait(subId, config)
+ } catch (e: Throwable) {
+ Log.e(TAG, "Error resetting carrier config for subId $subId")
+ }
+ }
+ originalConfigs.clear()
+ }
+ }
+}
+
+private fun CarrierConfigManager.getConfigForSubIdCompat(
+ subId: Int,
+ keys: Set<String>
+): PersistableBundle {
+ return if (isAtLeastU()) {
+ // This method is U+
+ getConfigForSubId(subId, *keys.toTypedArray())
+ } else {
+ @Suppress("DEPRECATION")
+ val config = assertNotNull(getConfigForSubId(subId))
+ val allKeys = config.keySet().toList()
+ allKeys.forEach {
+ if (!keys.contains(it)) {
+ config.remove(it)
+ }
+ }
+ config
+ }
+}
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
index ea86281..c7d6850 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -43,9 +43,14 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.modules.utils.build.SdkLevel.isAtLeastS
import java.io.ByteArrayOutputStream
+import java.io.CharArrayWriter
import java.io.File
import java.io.FileOutputStream
+import java.io.FileReader
+import java.io.OutputStream
+import java.io.OutputStreamWriter
import java.io.PrintWriter
+import java.io.Reader
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.CompletableFuture
@@ -76,11 +81,44 @@
private const val MAX_DUMPS = 20
private val TAG = ConnectivityDiagnosticsCollector::class.simpleName
+ @JvmStatic
var instance: ConnectivityDiagnosticsCollector? = null
}
+ /**
+ * Indicates tcpdump should be started and written to the diagnostics file on test case failure.
+ */
+ annotation class CollectTcpdumpOnFailure
+
+ private class DumpThread(
+ // Keep a reference to the ParcelFileDescriptor otherwise GC would close it
+ private val fd: ParcelFileDescriptor,
+ private val reader: Reader
+ ) : Thread() {
+ private val writer = CharArrayWriter()
+ override fun run() {
+ reader.copyTo(writer)
+ }
+
+ fun closeAndWriteTo(output: OutputStream?) {
+ join()
+ fd.close()
+ if (output != null) {
+ val outputWriter = OutputStreamWriter(output)
+ outputWriter.write("--- tcpdump stopped at ${ZonedDateTime.now()} ---\n")
+ writer.writeTo(outputWriter)
+ }
+ }
+ }
+
+ private data class TcpdumpRun(val pid: Int, val reader: DumpThread)
+
private var failureHeader: String? = null
+
+ // Accessed from the test listener methods which are synchronized by junit (see TestListener)
+ private var tcpdumpRun: TcpdumpRun? = null
private val buffer = ByteArrayOutputStream()
+ private val failureHeaderExtras = mutableMapOf<String, Any>()
private val collectorDir: File by lazy {
createAndEmptyDirectory(COLLECTOR_DIR)
}
@@ -112,7 +150,7 @@
override fun onSetUp() {
assertNull(instance, "ConnectivityDiagnosticsCollectors were set up multiple times")
instance = this
- TryTestConfig.setDiagnosticsCollector { throwable ->
+ TryTestConfig.swapDiagnosticsCollector { throwable ->
if (runOnFailure(throwable)) {
collectTestFailureDiagnostics(throwable)
}
@@ -130,7 +168,8 @@
.addCapability(NET_CAPABILITY_INTERNET)
.addTransportType(TRANSPORT_WIFI)
.addTransportType(TRANSPORT_CELLULAR)
- .build(), networkCallback
+ .build(),
+ networkCallback
)
}
}
@@ -146,16 +185,69 @@
// 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: " +
+ 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")
+ "--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 onTestStart(testData: DataRecord, description: Description) {
+ val tcpdumpAnn = description.annotations.firstOrNull { it is CollectTcpdumpOnFailure }
+ as? CollectTcpdumpOnFailure
+ if (tcpdumpAnn != null) {
+ startTcpdumpForTestcaseIfSupported()
+ }
+ }
+
+ private fun startTcpdumpForTestcaseIfSupported() {
+ if (!DeviceInfoUtils.isDebuggable()) {
+ Log.d(TAG, "Cannot start tcpdump, build is not debuggable")
+ return
+ }
+ if (tcpdumpRun != null) {
+ Log.e(TAG, "Cannot start tcpdump: it is already running")
+ return
+ }
+ // executeShellCommand won't tokenize quoted arguments containing spaces (like pcap filters)
+ // properly, so pass in the command in stdin instead of using sh -c 'command'
+ val fds = instrumentation.uiAutomation.executeShellCommandRw("sh")
+
+ val stdout = fds[0]
+ val stdin = fds[1]
+ ParcelFileDescriptor.AutoCloseOutputStream(stdin).use { writer ->
+ // Echo the current pid, and replace it (with exec) with the tcpdump process, so the
+ // tcpdump pid is known.
+ writer.write(
+ "echo $$; exec su 0 tcpdump -n -i any -U -xx".encodeToByteArray()
+ )
+ }
+ val reader = FileReader(stdout.fileDescriptor).buffered()
+ val tcpdumpPid = Integer.parseInt(reader.readLine())
+ val dumpThread = DumpThread(stdout, reader)
+ dumpThread.start()
+ tcpdumpRun = TcpdumpRun(tcpdumpPid, dumpThread)
+ }
+
+ private fun stopTcpdumpIfRunning(output: OutputStream?) {
+ val run = tcpdumpRun ?: return
+ // Send SIGTERM for graceful shutdown of tcpdump so that it can flush its output
+ executeCommandBlocking("su 0 kill ${run.pid}")
+ run.reader.closeAndWriteTo(output)
+ tcpdumpRun = null
+ }
+
override fun onTestEnd(testData: DataRecord, description: Description) {
+ // onTestFail is called before onTestEnd, so if the test failed tcpdump would already have
+ // been stopped and output dumped. Here this stops tcpdump if the test succeeded, throwing
+ // away its output.
+ stopTcpdumpIfRunning(output = null)
+
// 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.
@@ -194,6 +286,7 @@
fos.write("\n".toByteArray())
}
fos.write(buffer.toByteArray())
+ stopTcpdumpIfRunning(fos)
}
failureHeader = null
buffer.reset()
@@ -218,6 +311,8 @@
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 {
@@ -235,8 +330,11 @@
}
}
} else {
- Log.w(TAG, "The test is still holding shell permissions, cannot collect privileged " +
- "device info")
+ Log.w(
+ TAG,
+ "The test is still holding shell permissions, cannot collect privileged " +
+ "device info"
+ )
headerObj.put("shellPermissionsUnavailable", true)
}
failureHeader = headerObj.apply {
@@ -288,7 +386,9 @@
cbHelper.registerNetworkCallback(
NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
- .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
+ .addCapability(NET_CAPABILITY_INTERNET).build(),
+ cb
+ )
return try {
cb.wifiInfoFuture.get(1L, TimeUnit.SECONDS)
} catch (e: TimeoutException) {
@@ -319,22 +419,45 @@
* @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")
+ collectDumpsys("connectivity --dump-priority HIGH", exceptionContext)
+ }
+
+ /**
+ * Add a dumpsys to the test data dump.
+ *
+ * <p>The dump will be collected immediately, and exported to a test artifact file when the
+ * test ends.
+ * @param dumpsysCmd The dumpsys command to run (for example "connectivity").
+ * @param exceptionContext An exception to write a stacktrace to the dump for context.
+ */
+ fun collectDumpsys(dumpsysCmd: String, exceptionContext: Throwable? = null) {
+ Log.i(TAG, "Collecting dumpsys $dumpsysCmd for test artifacts")
PrintWriter(buffer).let {
- it.println("--- Dumpsys connectivity at ${ZonedDateTime.now()} ---")
+ it.println("--- Dumpsys $dumpsysCmd at ${ZonedDateTime.now()} ---")
maybeWriteExceptionContext(it, exceptionContext)
it.flush()
}
ParcelFileDescriptor.AutoCloseInputStream(
InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(
- "dumpsys connectivity --dump-priority HIGH")).use {
+ "dumpsys $dumpsysCmd"
+ )
+ ).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/DeviceConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
index 785e55a..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,7 +88,7 @@
}
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(
@@ -116,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/PollingUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/PollingUtils.kt
new file mode 100644
index 0000000..a6e7ead
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PollingUtils.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2025 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
+
+private const val POLLING_INTERVAL_MS: Int = 100
+
+/** Calls condition() until it returns true or timeout occurs. */
+fun pollingCheck(timeout_ms: Long, condition: () -> Boolean): Boolean {
+ var polling_time = 0
+ do {
+ Thread.sleep(POLLING_INTERVAL_MS.toLong())
+ polling_time += POLLING_INTERVAL_MS
+ if (condition()) return true
+ } while (polling_time < timeout_ms)
+ return false
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
index d5e91c2..0b239b4 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)
@@ -67,10 +68,15 @@
* If any `@FeatureFlag` annotation is found, it passes every feature flag's name
* and enabled state into the user-specified lambda to apply custom actions.
*/
+ private val parameterizedRegexp = Regex("\\[\\d+\\]$")
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
- val testMethod = description.testClass.getMethod(description.methodName)
+ // If the same class also uses Parameterized, depending on evaluation order the
+ // method names here may be synthetic method names, where [0] [1] or so are added
+ // at the end of the method name. Find the original method name.
+ val methodName = description.methodName.replace(parameterizedRegexp, "")
+ val testMethod = description.testClass.getMethod(methodName)
val featureFlagAnnotations = testMethod.getAnnotationsByType(
FeatureFlag::class.java
)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
index 8dc1bc4..bfbbc34 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -14,19 +14,34 @@
* limitations under the License.
*/
-package com.android.testutils;
+package com.android.testutils
import android.content.Context
+import android.net.InetAddresses.parseNumericAddress
import android.net.KeepalivePacketData
+import android.net.LinkAddress
import android.net.LinkProperties
import android.net.NetworkAgent
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkProvider
+import android.net.NetworkRequest
import android.net.QosFilter
import android.net.Uri
import android.os.Looper
+import android.system.ErrnoException
+import android.system.Os
+import android.system.OsConstants
+import android.system.OsConstants.EADDRNOTAVAIL
+import android.system.OsConstants.ENETUNREACH
+import android.system.OsConstants.ENONET
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.CompatUtil.makeTestNetworkSpecifier
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
@@ -42,6 +57,8 @@
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import java.net.NetworkInterface
+import java.net.SocketException
import java.time.Duration
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@@ -65,6 +82,92 @@
conf: NetworkAgentConfig
) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+ companion object {
+
+ /**
+ * Convenience method to create a [NetworkRequest] matching [TestableNetworkAgent]s from
+ * [createOnInterface].
+ */
+ fun makeNetworkRequestForInterface(ifaceName: String) = NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+ .build()
+
+ /**
+ * Convenience method to initialize a [TestableNetworkAgent] on a given interface.
+ *
+ * This waits for link-local addresses to be setup and ensures LinkProperties are updated
+ * with the addresses.
+ */
+ fun createOnInterface(
+ context: Context,
+ looper: Looper,
+ ifaceName: String,
+ timeoutMs: Long
+ ): TestableNetworkAgent {
+ val lp = LinkProperties().apply {
+ interfaceName = ifaceName
+ }
+ val agent = TestableNetworkAgent(
+ context,
+ looper,
+ NetworkCapabilities().apply {
+ removeCapability(NET_CAPABILITY_TRUSTED)
+ addTransportType(TRANSPORT_TEST)
+ setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+ },
+ lp,
+ NetworkAgentConfig.Builder().build()
+ )
+ val network = agent.register()
+ agent.markConnected()
+ if (isAtLeastS()) {
+ // OnNetworkCreated was added in S
+ agent.eventuallyExpect<OnNetworkCreated>()
+ }
+
+ // Wait until the link-local address can be used. Address flags are not available
+ // without elevated permissions, so check that bindSocket works.
+ assertEventuallyTrue("No usable v6 address after $timeoutMs ms", timeoutMs) {
+ // To avoid race condition between socket connection succeeding and interface
+ // returning a non-empty address list. Verify that interface returns a non-empty
+ // list, before trying the socket connection.
+ if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
+ return@assertEventuallyTrue false
+ }
+
+ val sock = Os.socket(OsConstants.AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
+ tryTest {
+ network.bindSocket(sock)
+ Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
+ true
+ }.catch<ErrnoException> {
+ if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
+ throw it
+ }
+ false
+ }.catch<SocketException> {
+ // OnNetworkCreated does not exist on R, so a SocketException caused by ENONET
+ // may be seen before the network is created
+ if (isAtLeastS()) throw it
+ val cause = it.cause as? ErrnoException ?: throw it
+ if (cause.errno != ENONET) {
+ throw it
+ }
+ false
+ } cleanup {
+ Os.close(sock)
+ }
+ }
+
+ agent.lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
+ LinkAddress(it.address, it.networkPrefixLength.toInt())
+ })
+ agent.sendLinkProperties(agent.lp)
+ return agent
+ }
+ }
val DEFAULT_TIMEOUT_MS = 5000L
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index ae43c15..d9c51e5 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -32,6 +32,7 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
import com.android.testutils.RecorderCallback.CallbackEntry.Losing
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
@@ -66,6 +67,12 @@
// constructor by specifying override.
abstract val network: Network
+ data class Reserved private constructor(
+ override val network: Network,
+ val caps: NetworkCapabilities
+ ): CallbackEntry() {
+ constructor(caps: NetworkCapabilities) : this(NULL_NETWORK, caps)
+ }
data class Available(override val network: Network) : CallbackEntry()
data class CapabilitiesChanged(
override val network: Network,
@@ -100,6 +107,8 @@
// Convenience constants for expecting a type
companion object {
@JvmField
+ val RESERVED = Reserved::class
+ @JvmField
val AVAILABLE = Available::class
@JvmField
val NETWORK_CAPS_UPDATED = CapabilitiesChanged::class
@@ -127,6 +136,11 @@
val history = backingRecord.newReadHead()
val mark get() = history.mark
+ override fun onReserved(caps: NetworkCapabilities) {
+ Log.d(logTag, "onReserved $caps")
+ history.add(Reserved(caps))
+ }
+
override fun onAvailable(network: Network) {
Log.d(logTag, "onAvailable $network")
history.add(Available(network))
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
index 21bd60c..a0078d2 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
@@ -52,10 +52,11 @@
inline fun <reified T : CallbackEntry> expectCallbackThat(
crossinline predicate: (T) -> Boolean
- ) {
+ ): T {
val event = history.poll(timeoutMs)
?: fail("Did not receive callback after ${timeoutMs}ms")
if (event !is T || !predicate(event)) fail("Received unexpected callback $event")
+ return event
}
fun expectOnNetworkNeeded(capabilities: NetworkCapabilities) =
diff --git a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
index 435fdd8..a99359b 100644
--- a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -18,16 +18,20 @@
import com.android.ddmlib.testrunner.TestResult
import com.android.tradefed.config.Option
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey
import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.log.LogUtil.CLog
import com.android.tradefed.result.CollectingTestListener
import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner
import com.android.tradefed.targetprep.BaseTargetPreparer
import com.android.tradefed.targetprep.TargetSetupError
import com.android.tradefed.targetprep.suite.SuiteApkInstaller
+import java.io.File
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"
@@ -47,7 +51,41 @@
* --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-wifi-check:true".
*/
open class ConnectivityTestTargetPreparer : BaseTargetPreparer() {
- private val installer = SuiteApkInstaller()
+ private val installer = ApkInstaller()
+
+ private class ApkInstaller : SuiteApkInstaller() {
+ override fun getLocalPathForFilename(
+ testInfo: TestInformation,
+ apkFileName: String
+ ): File {
+ if (apkFileName == CONNECTIVITY_CHECKER_APK) {
+ // For the connectivity checker APK, explicitly look for it in the directory of the
+ // host-side preparer.
+ // This preparer is part of the net-tests-utils-host-common library, which includes
+ // the checker APK via device_common_data in its build rule. Both need to be at the
+ // same version so that the preparer calls the right test methods in the checker
+ // APK.
+ // The default strategy for finding test files is to do a recursive search in test
+ // directories, which may find wrong files in wrong directories. In particular,
+ // if some MTS test includes the checker APK, and that test is linked to a module
+ // that boards the train at a version different from this target preparer, there
+ // could be a version difference between the APK and the host-side preparer.
+ // Explicitly looking for the APK in the host-side preparer directory ensures that
+ // it uses the version that was packaged together with the host-side preparer.
+ val testsDir = testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY)
+ val f = File(testsDir, "net-tests-utils-host-common/$CONNECTIVITY_CHECKER_APK")
+ if (f.isFile) {
+ return f
+ }
+ // When running locally via atest, device_common_data does cause the APK to be put
+ // into the test temp directory, so recursive search is still used to find it in the
+ // directory of the test module that is being run. This is fine because atest runs
+ // are on local trees that do not have versioning problems.
+ CLog.i("APK not found at $f, falling back to recursive search")
+ }
+ return super.getLocalPathForFilename(testInfo, apkFileName)
+ }
+ }
@Option(
name = IGNORE_WIFI_CHECK,
@@ -84,27 +122,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)) {
@@ -177,7 +216,7 @@
.contains(":deny")
}
- private fun refreshTime(testInfo: TestInformation,) {
+ private fun refreshTime(testInfo: TestInformation) {
// Forces a synchronous time refresh using the network. Time is fetched synchronously but
// this does not guarantee that system time is updated when it returns.
// This avoids flakes where the system clock rolls back, for example when using test
@@ -187,6 +226,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/tether_utils.py b/staticlibs/testutils/host/python/tether_utils.py
index 702b596..710f8a8 100644
--- a/staticlibs/testutils/host/python/tether_utils.py
+++ b/staticlibs/testutils/host/python/tether_utils.py
@@ -95,7 +95,9 @@
hotspot_interface = server.startHotspot(test_ssid, test_passphrase)
# Make the client connects to the hotspot.
- client_network = client.connectToWifi(test_ssid, test_passphrase)
+ client_network = client.connectToWifi(
+ test_ssid, test_passphrase, upstream_type != UpstreamType.NONE
+ )
return hotspot_interface, client_network
@@ -108,3 +110,6 @@
server.unregisterAll()
# Teardown the hotspot.
server.stopAllTethering()
+ # Some test cases would disable wifi, e.g. cellular upstream tests.
+ # Reconnect to it if feasible.
+ server.reconnectWifiIfSupported()
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
index dcd422c..45c69c9 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
@@ -75,13 +75,21 @@
*/
object TryTestConfig {
- internal var diagnosticsCollector: Consumer<Throwable>? = null
+ private var diagnosticsCollector: Consumer<Throwable>? = null
/**
* Set the diagnostics collector to be used in case of failure in [tryTest].
+ *
+ * @return The previous collector.
*/
- fun setDiagnosticsCollector(collector: Consumer<Throwable>) {
+ fun swapDiagnosticsCollector(collector: Consumer<Throwable>?): Consumer<Throwable>? {
+ val oldCollector = diagnosticsCollector
diagnosticsCollector = collector
+ return oldCollector
+ }
+
+ fun reportError(e: Throwable) {
+ diagnosticsCollector?.accept(e)
}
}
@@ -90,14 +98,10 @@
try {
Result.success(block())
} catch (e: Throwable) {
- TryTestConfig.diagnosticsCollector?.accept(e)
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
@@ -105,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 d1d5649..176546a 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
@@ -20,6 +20,7 @@
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
@@ -134,7 +135,7 @@
// 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() + timeoutMs * 1000
+ val limit = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs)
while (!fn.asBoolean) {
if (System.nanoTime() > limit) {
fail(descr)
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index bb1009b..60a02fb 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -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/CaptivePortalTest.java b/tests/common/java/android/net/CaptivePortalTest.java
index 15d3398..6655827 100644
--- a/tests/common/java/android/net/CaptivePortalTest.java
+++ b/tests/common/java/android/net/CaptivePortalTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.assertEquals;
import android.os.Build;
+import android.os.IBinder;
import android.os.RemoteException;
import androidx.test.filters.SmallTest;
@@ -55,6 +56,10 @@
mCode = request;
}
+ @Override
+ public void setDelegateUid(int uid, IBinder binder, IIntResultListener listener) {
+ }
+
// This is only @Override on R-
public void logEvent(int eventId, String packageName) throws RemoteException {
mCode = eventId;
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 0f0e2f1..d694637 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -382,6 +382,7 @@
netCap.setAllowedUids(allowedUids);
netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2));
netCap.setUids(uids);
+ netCap.setReservationId(42);
}
netCap.setOwnerUid(123);
@@ -1493,4 +1494,42 @@
// nc1 and nc2 are the same since invalid capability is ignored
assertEquals(nc1, nc2);
}
+
+ @Test
+ public void testReservationIdMatching() {
+ final NetworkCapabilities requestNc = new NetworkCapabilities();
+ final NetworkCapabilities reservationNc = new NetworkCapabilities();
+ reservationNc.setReservationId(42);
+
+ final NetworkCapabilities reservedNetworkNc = new NetworkCapabilities(reservationNc);
+ final NetworkCapabilities otherNetworkNc = new NetworkCapabilities();
+ final NetworkCapabilities otherReservedNetworkNc = new NetworkCapabilities();
+ otherReservedNetworkNc.setReservationId(99);
+ final NetworkCapabilities offerNc = new NetworkCapabilities();
+ offerNc.setReservationId(NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS);
+
+ // A regular request can match any network or offer except one with MATCH_ALL_RESERVATIONS
+ assertTrue(requestNc.satisfiedByNetworkCapabilities(reservedNetworkNc));
+ assertTrue(requestNc.satisfiedByNetworkCapabilities(otherNetworkNc));
+ assertTrue(requestNc.satisfiedByNetworkCapabilities(otherReservedNetworkNc));
+ assertFalse(requestNc.satisfiedByNetworkCapabilities(offerNc));
+
+ // A reservation request can only match the reservedNetwork and the blanket offer.
+ assertTrue(reservationNc.satisfiedByNetworkCapabilities(reservedNetworkNc));
+ assertFalse(reservationNc.satisfiedByNetworkCapabilities(otherNetworkNc));
+ assertFalse(reservationNc.satisfiedByNetworkCapabilities(otherReservedNetworkNc));
+ assertTrue(reservationNc.satisfiedByNetworkCapabilities(offerNc));
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testReservationIdEquals() {
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setReservationId(42);
+ final NetworkCapabilities other = new NetworkCapabilities(nc);
+
+ assertEquals(nc, other);
+
+ nc.setReservationId(43);
+ assertNotEquals(nc, other);
+ }
}
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 31924f0..ca3e77f 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -19,8 +19,7 @@
java_test_helper_library {
name: "CtsHostsideNetworkTestsAidl",
- sdk_version: "current",
- min_sdk_version: "30",
+ sdk_version: "system_current",
srcs: [
"com/android/cts/net/hostside/*.aidl",
"com/android/cts/net/hostside/*.java",
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/ITetheringHelper.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/ITetheringHelper.aidl
new file mode 100644
index 0000000..a9f5ed4
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/ITetheringHelper.aidl
@@ -0,0 +1,23 @@
+/*
+ * 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.net.TetheringInterface;
+
+interface ITetheringHelper {
+ TetheringInterface getTetheredWifiInterface();
+}
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..a1cf968
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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 android.net.TetheringManager.TETHERING_WIFI;
+
+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.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
+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;
+ private TestTetheringEventCallback mTetheringEventCallback;
+
+ @Before
+ public void setUp() throws Exception {
+ Context targetContext = getInstrumentation().getTargetContext();
+ mCtsTetheringUtils = new CtsTetheringUtils(targetContext);
+ mTetheringHelperClient = new TetheringHelperClient(targetContext);
+ mTetheringHelperClient.bind();
+ mTetheringEventCallback = mCtsTetheringUtils.registerTetheringEventCallback();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mTetheringHelperClient.unbind();
+ mCtsTetheringUtils.unregisterTetheringEventCallback(mTetheringEventCallback);
+ mCtsTetheringUtils.stopAllTethering();
+ }
+
+ /**
+ * Starts Wifi tethering and tests that the SoftApConfiguration is redacted from
+ * TetheringEventCallback for other apps.
+ */
+ @Test
+ public void testSoftApConfigurationRedactedForOtherUids() throws Exception {
+ mTetheringEventCallback.assumeWifiTetheringSupported(
+ getInstrumentation().getTargetContext());
+ SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes("This is an SSID!"
+ .getBytes(StandardCharsets.UTF_8))).build();
+ final TetheringInterface tetheringInterface =
+ mCtsTetheringUtils.startWifiTethering(mTetheringEventCallback, softApConfig);
+ assertNotNull(tetheringInterface);
+ assertEquals(softApConfig, tetheringInterface.getSoftApConfiguration());
+ assertEquals(new TetheringInterface(
+ TETHERING_WIFI, tetheringInterface.getInterface(), softApConfig),
+ tetheringInterface);
+ TetheringInterface tetheringInterfaceForApp2 =
+ mTetheringHelperClient.getTetheredWifiInterface();
+ assertNotNull(tetheringInterfaceForApp2);
+ assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
+ assertEquals(
+ tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
+ }
+}
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 d05a8d0..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,6 +20,7 @@
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;
@@ -1209,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;
@@ -1249,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);
}
}
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/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 ae572e6..b5e2450 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -91,8 +91,8 @@
}
private String[] getSysctlDirs() throws Exception {
- String interfaceDirs[] = mDevice.executeAdbCommand("shell", "ls", "-1",
- IPV6_SYSCTL_DIR).split("\n");
+ String[] interfaceDirs = mDevice.executeShellCommand("ls -1 " + IPV6_SYSCTL_DIR)
+ .split("\n");
List<String> interfaceDirsList = new ArrayList<String>(Arrays.asList(interfaceDirs));
interfaceDirsList.remove("all");
interfaceDirsList.remove("lo");
@@ -109,13 +109,13 @@
}
public int readIntFromPath(String path) throws Exception {
- String mode = mDevice.executeAdbCommand("shell", "stat", "-c", "%a", path).trim();
- String user = mDevice.executeAdbCommand("shell", "stat", "-c", "%u", path).trim();
- String group = mDevice.executeAdbCommand("shell", "stat", "-c", "%g", path).trim();
+ String mode = mDevice.executeShellCommand("stat -c %a " + path).trim();
+ String user = mDevice.executeShellCommand("stat -c %u " + path).trim();
+ String group = mDevice.executeShellCommand("stat -c %g " + path).trim();
assertEquals(mode, "644");
assertEquals(user, "0");
assertEquals(group, "0");
- return Integer.parseInt(mDevice.executeAdbCommand("shell", "cat", path).trim());
+ return Integer.parseInt(mDevice.executeShellCommand("cat " + path).trim());
}
/**
@@ -191,7 +191,7 @@
assumeTrue(new DeviceSdkLevel(mDevice).isDeviceAtLeastV());
String path = "/proc/sys/net/ipv4/tcp_congestion_control";
- String value = mDevice.executeAdbCommand("shell", "cat", path).trim();
+ String value = mDevice.executeShellCommand("cat " + path).trim();
assertEquals("cubic", value);
}
}
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index 949be85..a082a95 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -22,6 +22,7 @@
main: "run_tests.py",
srcs: [
"apfv4_test.py",
+ "apfv6_test.py",
"connectivity_multi_devices_test.py",
"run_tests.py",
],
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
index 7795be5..aa535fd 100644
--- a/tests/cts/multidevices/apfv4_test.py
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -53,7 +53,7 @@
) # 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.client_mac_address.replace(":", "") # Destination MAC
packet += self.server_mac_address.replace(":", "") # Source MAC
packet += blocked_ether_type
diff --git a/tests/cts/multidevices/apfv6_test.py b/tests/cts/multidevices/apfv6_test.py
new file mode 100644
index 0000000..fc732d2
--- /dev/null
+++ b/tests/cts/multidevices/apfv6_test.py
@@ -0,0 +1,84 @@
+# 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 apf_test_base, apf_utils, adb_utils, assert_utils, packet_utils
+
+APFV6_VERSION = 6000
+ARP_OFFLOAD_REPLY_LEN = 60
+
+class ApfV6Test(apf_test_base.ApfTestBase):
+ def setup_class(self):
+ super().setup_class()
+
+ # Skip tests for APF version < 6000
+ apf_utils.assume_apf_version_support_at_least(
+ self.clientDevice, self.client_iface_name, APFV6_VERSION
+ )
+
+ def teardown_class(self):
+ # force to stop capture on the server device if any test case failed
+ try:
+ apf_utils.stop_capture_packets(self.serverDevice, self.server_iface_name)
+ except assert_utils.UnexpectedBehaviorError:
+ pass
+ super().teardown_class()
+
+ def test_unicast_arp_request_offload(self):
+ arp_request = packet_utils.construct_arp_packet(
+ src_mac=self.server_mac_address,
+ dst_mac=self.client_mac_address,
+ src_ip=self.server_ipv4_addresses[0],
+ dst_ip=self.client_ipv4_addresses[0],
+ op=packet_utils.ARP_REQUEST_OP
+ )
+
+ arp_reply = packet_utils.construct_arp_packet(
+ src_mac=self.client_mac_address,
+ dst_mac=self.server_mac_address,
+ src_ip=self.client_ipv4_addresses[0],
+ dst_ip=self.server_ipv4_addresses[0],
+ op=packet_utils.ARP_REPLY_OP
+ )
+
+ # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
+ arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+
+ self.send_packet_and_expect_reply_received(
+ arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+ )
+
+ def test_broadcast_arp_request_offload(self):
+ arp_request = packet_utils.construct_arp_packet(
+ src_mac=self.server_mac_address,
+ dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
+ src_ip=self.server_ipv4_addresses[0],
+ dst_ip=self.client_ipv4_addresses[0],
+ op=packet_utils.ARP_REQUEST_OP
+ )
+
+ arp_reply = packet_utils.construct_arp_packet(
+ src_mac=self.client_mac_address,
+ dst_mac=self.server_mac_address,
+ src_ip=self.client_ipv4_addresses[0],
+ dst_ip=self.server_ipv4_addresses[0],
+ op=packet_utils.ARP_REPLY_OP
+ )
+
+ # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
+ arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+
+ self.send_packet_and_expect_reply_received(
+ arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+ )
diff --git a/tests/cts/multidevices/run_tests.py b/tests/cts/multidevices/run_tests.py
index 1391d13..a0d0bec 100644
--- a/tests/cts/multidevices/run_tests.py
+++ b/tests/cts/multidevices/run_tests.py
@@ -16,6 +16,7 @@
import sys
from apfv4_test import ApfV4Test
+from apfv6_test import ApfV6Test
from connectivity_multi_devices_test import ConnectivityMultiDevicesTest
from mobly import suite_runner
@@ -35,4 +36,4 @@
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)
+ suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test, ApfV6Test], sys.argv)
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 49688cc..252052e 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -27,13 +27,11 @@
import android.net.NetworkRequest
import android.net.cts.util.CtsNetUtils
import android.net.cts.util.CtsTetheringUtils
-import android.net.wifi.ScanResult
import android.net.wifi.SoftApConfiguration
import android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
-import android.net.wifi.WifiNetworkSpecifier
import android.net.wifi.WifiSsid
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.PropertyUtil
@@ -89,6 +87,11 @@
ctsNetUtils.expectNetworkIsSystemDefault(network)
}
+ @Rpc(description = "Reconnect to wifi if supported.")
+ fun reconnectWifiIfSupported() {
+ ctsNetUtils.reconnectWifiIfSupported()
+ }
+
@Rpc(description = "Unregister all connections.")
fun unregisterAll() {
cbHelper.unregisterAll()
@@ -104,10 +107,7 @@
// Suppress warning because WifiManager methods to connect to a config are
// documented not to be deprecated for privileged users.
@Suppress("DEPRECATION")
- fun connectToWifi(ssid: String, passphrase: String): Long {
- val specifier = WifiNetworkSpecifier.Builder()
- .setBand(ScanResult.WIFI_BAND_24_GHZ)
- .build()
+ fun connectToWifi(ssid: String, passphrase: String, requireValidation: Boolean): Long {
val wifiConfig = WifiConfiguration()
wifiConfig.SSID = "\"" + ssid + "\""
wifiConfig.preSharedKey = "\"" + passphrase + "\""
@@ -136,7 +136,8 @@
return@runAsShell networkCallback.eventuallyExpect<CapabilitiesChanged> {
// Remove double quotes.
val ssidFromCaps = (WifiInfo::sanitizeSsid)(it.caps.ssid)
- ssidFromCaps == ssid && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+ ssidFromCaps == ssid && (!requireValidation ||
+ it.caps.hasCapability(NET_CAPABILITY_VALIDATED))
}.network.networkHandle
}
}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index a5ad7f2..1ba581a 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -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
@@ -87,15 +95,10 @@
"NetworkStackApiCurrentShims",
],
test_suites: [
+ "automotive-general-tests",
"cts",
- "mts-dnsresolver",
- "mts-networking",
"mts-tethering",
- "mts-wifi",
- "mcts-dnsresolver",
- "mcts-networking",
"mcts-tethering",
- "mcts-wifi",
"general-tests",
],
}
@@ -158,6 +161,7 @@
min_sdk_version: "30",
// Tag this module as a cts test artifact
test_suites: [
+ "automotive-general-tests",
"cts",
"general-tests",
],
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 7590a2b..cb0e575 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -19,6 +19,7 @@
<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="parameter" value="secondary_user_on_secondary_display" />
<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" />
@@ -41,6 +42,7 @@
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="{PACKAGE}" />
+ <option name="shell-timeout" value="1500s"/>
<option name="runtime-hint" value="9m4s" />
<option name="hidden-api-checks" value="false" />
<option name="isolated-storage" value="false" />
@@ -60,8 +62,7 @@
</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="log-data-type" value="CONNDIAG" />
+ <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
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/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index de4a3bf..8138598 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -62,8 +62,6 @@
"cts",
"general-tests",
"mts-dnsresolver",
- "mts-networking",
"mcts-dnsresolver",
- "mcts-networking",
],
}
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 9ac2c67..2a372ce 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -19,7 +19,8 @@
package android.net.cts
-import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
+import android.content.pm.PackageManager.FEATURE_LEANBACK
import android.content.pm.PackageManager.FEATURE_WIFI
import android.net.ConnectivityManager
import android.net.Network
@@ -35,7 +36,7 @@
import android.net.apf.ApfConstants.IPV6_NEXT_HEADER_OFFSET
import android.net.apf.ApfConstants.IPV6_SRC_ADDR_OFFSET
import android.net.apf.ApfCounterTracker
-import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_PING
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD
import android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_16384THS
import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP
import android.net.apf.ApfV4Generator
@@ -49,13 +50,15 @@
import android.os.Handler
import android.os.HandlerThread
import android.os.PowerManager
+import android.os.SystemProperties
+import android.os.UserManager
import android.platform.test.annotations.AppModeFull
-import android.provider.DeviceConfig
-import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
import android.system.Os
import android.system.OsConstants
import android.system.OsConstants.AF_INET6
import android.system.OsConstants.ETH_P_IPV6
+import android.system.OsConstants.ICMP6_ECHO_REPLY
+import android.system.OsConstants.ICMP6_ECHO_REQUEST
import android.system.OsConstants.IPPROTO_ICMPV6
import android.system.OsConstants.SOCK_DGRAM
import android.system.OsConstants.SOCK_NONBLOCK
@@ -83,7 +86,7 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.SkipPresubmit
import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.runAsShell
+import com.android.testutils.pollingCheck
import com.android.testutils.waitForIdle
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
@@ -100,6 +103,7 @@
import kotlin.test.assertNotNull
import org.junit.After
import org.junit.AfterClass
+import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
@@ -108,8 +112,6 @@
private const val TAG = "ApfIntegrationTest"
private const val TIMEOUT_MS = 2000L
-private const val APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"
-private const val POLLING_INTERVAL_MS: Int = 100
private const val RCV_BUFFER_SIZE = 1480
private const val PING_HEADER_LENGTH = 8
@@ -127,28 +129,39 @@
private val powerManager = context.getSystemService(PowerManager::class.java)!!
private val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG)
- fun pollingCheck(condition: () -> Boolean, timeout_ms: Int): Boolean {
- var polling_time = 0
- do {
- Thread.sleep(POLLING_INTERVAL_MS.toLong())
- polling_time += POLLING_INTERVAL_MS
- if (condition()) return true
- } while (polling_time < timeout_ms)
- return false
- }
-
fun turnScreenOff() {
if (!wakeLock.isHeld()) wakeLock.acquire()
runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
- val result = pollingCheck({ !powerManager.isInteractive() }, timeout_ms = 2000)
- assertThat(result).isTrue()
+ waitForInteractiveState(false)
}
fun turnScreenOn() {
if (wakeLock.isHeld()) wakeLock.release()
runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
- val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
- assertThat(result).isTrue()
+ waitForInteractiveState(true)
+ }
+
+ private fun waitForInteractiveState(interactive: Boolean) {
+ // TODO(b/366037029): This test condition should be removed once
+ // PowerManager#isInteractive is fully implemented on automotive
+ // form factor with visible background user.
+ if (isAutomotiveWithVisibleBackgroundUser()) {
+ // Wait for 2 seconds to ensure the interactive state is updated.
+ // This is a workaround for b/366037029.
+ Thread.sleep(2000L)
+ } else {
+ val result = pollingCheck(timeout_ms = 2000) {
+ powerManager.isInteractive()
+ }
+ assertThat(result).isEqualTo(interactive)
+ }
+ }
+
+ private fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
+ val packageManager = context.getPackageManager()
+ val userManager = context.getSystemService(UserManager::class.java)!!
+ return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE) &&
+ userManager.isVisibleBackgroundUsersSupported)
}
@BeforeClass
@@ -156,23 +169,15 @@
@Suppress("ktlint:standard:no-multi-spaces")
fun setupOnce() {
// TODO: assertions thrown in @BeforeClass / @AfterClass are not well supported in the
- // test infrastructure. Consider saving excepion and throwing it in setUp().
+ // test infrastructure. Consider saving exception and throwing it in setUp().
+
// APF must run when the screen is off and the device is not interactive.
turnScreenOff()
+
// Wait for APF to become active.
Thread.sleep(1000)
// TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been
// created.
- // APF adb cmds are only implemented in ApfFilter.java. Enable experiment to prevent
- // LegacyApfFilter.java from being used.
- runAsShell(WRITE_DEVICE_CONFIG) {
- DeviceConfig.setProperty(
- NAMESPACE_CONNECTIVITY,
- APF_NEW_RA_FILTER_VERSION,
- "1", // value => force enabled
- false // makeDefault
- )
- }
}
@AfterClass
@@ -186,8 +191,13 @@
handler: Handler,
private val network: Network
) : PacketReader(handler, RCV_BUFFER_SIZE) {
+ private data class PingContext(
+ val futureReply: CompletableFuture<List<ByteArray>>,
+ val expectReplyCount: Int,
+ val replyPayloads: MutableList<ByteArray> = mutableListOf()
+ )
private var sockFd: FileDescriptor? = null
- private var futureReply: CompletableFuture<ByteArray>? = null
+ private var pingContext: PingContext? = null
override fun createFd(): FileDescriptor {
// sockFd is closed by calling super.stop()
@@ -199,6 +209,8 @@
}
override fun handlePacket(recvbuf: ByteArray, length: Int) {
+ val context = pingContext ?: return
+
// If zero-length or Type is not echo reply: ignore.
if (length == 0 || recvbuf[0] != 0x81.toByte()) {
return
@@ -206,10 +218,14 @@
// Only copy the ping data and complete the future.
val result = recvbuf.sliceArray(8..<length)
Log.i(TAG, "Received ping reply: ${result.toHexString()}")
- futureReply!!.complete(recvbuf.sliceArray(8..<length))
+ context.replyPayloads.add(recvbuf.sliceArray(8..<length))
+ if (context.replyPayloads.size == context.expectReplyCount) {
+ context.futureReply.complete(context.replyPayloads)
+ pingContext = null
+ }
}
- fun sendPing(data: ByteArray, payloadSize: Int) {
+ fun sendPing(data: ByteArray, payloadSize: Int, expectReplyCount: Int = 1) {
require(data.size == payloadSize)
// rfc4443#section-4.1: Echo Request Message
@@ -225,17 +241,20 @@
val icmp6Header = byteArrayOf(0x80.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
val packet = icmp6Header + data
Log.i(TAG, "Sent ping: ${packet.toHexString()}")
- futureReply = CompletableFuture<ByteArray>()
+ pingContext = PingContext(
+ futureReply = CompletableFuture<List<ByteArray>>(),
+ expectReplyCount = expectReplyCount
+ )
Os.sendto(sockFd!!, packet, 0, packet.size, 0, PING_DESTINATION)
}
- fun expectPingReply(timeoutMs: Long = TIMEOUT_MS): ByteArray {
- return futureReply!!.get(timeoutMs, TimeUnit.MILLISECONDS)
+ fun expectPingReply(timeoutMs: Long = TIMEOUT_MS): List<ByteArray> {
+ return pingContext!!.futureReply.get(timeoutMs, TimeUnit.MILLISECONDS)
}
fun expectPingDropped() {
assertFailsWith(TimeoutException::class) {
- futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ pingContext!!.futureReply.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
}
}
@@ -274,10 +293,23 @@
return ApfCapabilities(version, maxLen, packetFormat)
}
+ private fun isTvDeviceSupportFullNetworkingUnder2w(): Boolean {
+ return (pm.hasSystemFeature(FEATURE_LEANBACK) &&
+ pm.hasSystemFeature("com.google.android.tv.full_networking_under_2w"))
+ }
+
@Before
fun setUp() {
assume().that(pm.hasSystemFeature(FEATURE_WIFI)).isTrue()
+ // Based on GTVS-16, Android Packet Filtering (APF) is OPTIONAL for devices that fully
+ // process all network packets on CPU at all times, even in standby, while meeting
+ // the <= 2W standby power demand requirement.
+ assumeFalse(
+ "Skipping test: TV device process full networking on CPU under 2W",
+ isTvDeviceSupportFullNetworkingUnder2w()
+ )
+
networkCallback = TestableNetworkCallback()
cm.requestNetwork(
NetworkRequest.Builder()
@@ -324,10 +356,8 @@
@Test
fun testApfCapabilities() {
// APF became mandatory in Android 14 VSR.
- assume().that(getVsrApiLevel()).isAtLeast(34)
-
- // ApfFilter does not support anything but ARPHRD_ETHER.
- assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
+ val vsrApiLevel = getVsrApiLevel()
+ assume().that(vsrApiLevel).isAtLeast(34)
// DEVICEs launching with Android 14 with CHIPSETs that set ro.board.first_api_level to 34:
// - [GMS-VSR-5.3.12-003] MUST return 4 or higher as the APF version number from calls to
@@ -347,9 +377,22 @@
// ro.board.first_api_level or ro.board.api_level to 202404 or higher:
// - [GMS-VSR-5.3.12-009] MUST indicate at least 2048 bytes of usable memory from calls to
// the getApfPacketFilterCapabilities HAL method.
- if (getVsrApiLevel() >= 202404) {
+ if (vsrApiLevel >= 202404) {
assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
}
+
+ // CHIPSETs (or DEVICES with CHIPSETs) that set ro.board.first_api_level or
+ // ro.board.api_level to 202504 or higher:
+ // - [VSR-5.3.12-018] MUST implement version 6 of the Android Packet Filtering (APF)
+ // interpreter in the Wi-Fi firmware.
+ // - [VSR-5.3.12-019] MUST provide at least 4000 bytes of APF RAM.
+ if (vsrApiLevel >= 202504) {
+ assertThat(caps.apfVersionSupported).isEqualTo(6000)
+ assertThat(caps.maximumApfProgramSize).isAtLeast(4000)
+ }
+
+ // ApfFilter does not support anything but ARPHRD_ETHER.
+ assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
}
// APF is backwards compatible, i.e. a v6 interpreter supports both v2 and v4 functionality.
@@ -357,6 +400,10 @@
assume().that(caps.apfVersionSupported).isAtLeast(version)
}
+ fun assumeNotCuttlefish() {
+ assume().that(SystemProperties.get("ro.product.board", "")).isNotEqualTo("cutf")
+ }
+
fun installProgram(bytes: ByteArray) {
val prog = bytes.toHexString()
val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
@@ -418,15 +465,15 @@
fun ApfV4GeneratorBase<*>.addPassIfNotIcmpv6EchoReply() {
// If not IPv6 -> PASS
- addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+ addLoad16intoR0(ETH_ETHERTYPE_OFFSET)
addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), BaseApfGenerator.PASS_LABEL)
// If not ICMPv6 -> PASS
- addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+ addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), BaseApfGenerator.PASS_LABEL)
// If not echo reply -> PASS
- addLoad8(R0, ICMP6_TYPE_OFFSET)
+ addLoad8intoR0(ICMP6_TYPE_OFFSET)
addJumpIfR0NotEquals(0x81, BaseApfGenerator.PASS_LABEL)
}
@@ -440,6 +487,7 @@
// should be turned on.
assume().that(getVsrApiLevel()).isAtLeast(34)
assumeApfVersionSupportAtLeast(4)
+ assumeNotCuttlefish()
// clear any active APF filter
clearApfMemory()
@@ -453,7 +501,7 @@
}
val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
packetReader.sendPing(data, payloadSize)
- assertThat(packetReader.expectPingReply()).isEqualTo(data)
+ assertThat(packetReader.expectPingReply()[0]).isEqualTo(data)
// Generate an APF program that drops the next ping
val gen = ApfV4Generator(
@@ -492,6 +540,7 @@
assume().that(getVsrApiLevel()).isAtLeast(34)
// Test v4 memory slots on both v4 and v6 interpreters.
assumeApfVersionSupportAtLeast(4)
+ assumeNotCuttlefish()
clearApfMemory()
val gen = ApfV4Generator(
caps.apfVersionSupported,
@@ -518,6 +567,13 @@
val program = gen.generate()
assertThat(program.size).isLessThan(counterRegion)
+ val randomProgram = ByteArray(1) { 0 } +
+ ByteArray(counterRegion - 1).also { Random.nextBytes(it) }
+ // There are known firmware bugs where they calculate the number of non-zero bytes within
+ // the program to determine the program length. Modify the test to first install a longer
+ // program before installing a program that do the program length check. This should help us
+ // catch these types of firmware bugs in CTS. (b/395545572)
+ installAndVerifyProgram(randomProgram)
installAndVerifyProgram(program)
// Trigger the program by sending a ping and waiting on the reply.
@@ -550,6 +606,7 @@
// should be turned on.
assume().that(getVsrApiLevel()).isAtLeast(34)
assumeApfVersionSupportAtLeast(4)
+ assumeNotCuttlefish()
clearApfMemory()
val gen = ApfV4Generator(
caps.apfVersionSupported,
@@ -592,6 +649,7 @@
@Test
fun testFilterAge16384thsIncreasesBetweenPackets() {
assumeApfVersionSupportAtLeast(6000)
+ assumeNotCuttlefish()
clearApfMemory()
val gen = ApfV6Generator(
caps.apfVersionSupported,
@@ -641,6 +699,7 @@
@Test
fun testReplyPing() {
assumeApfVersionSupportAtLeast(6000)
+ assumeNotCuttlefish()
installProgram(ByteArray(caps.maximumApfProgramSize) { 0 }) // Clear previous program
readProgram() // Ensure installation is complete
@@ -665,69 +724,80 @@
// increase PASSED_IPV6_ICMP counter
// pass
// else
- // transmit a ICMPv6 echo request packet with the first byte of the payload in the reply
- // increase DROPPED_IPV6_MULTICAST_PING counter
+ // transmit 3 ICMPv6 echo requests with random first byte
+ // increase DROPPED_IPV6_NS_REPLIED_NON_DAD counter
// drop
- val program = gen
- .addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+ gen.addLoad16intoR0(ETH_ETHERTYPE_OFFSET)
.addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), skipPacketLabel)
- .addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+ .addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
.addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), skipPacketLabel)
- .addLoad8(R0, ICMP6_TYPE_OFFSET)
- .addJumpIfR0NotEquals(0x81, skipPacketLabel) // Echo reply type
+ .addLoad8intoR0(ICMP6_TYPE_OFFSET)
+ .addJumpIfR0NotEquals(ICMP6_ECHO_REPLY.toLong(), skipPacketLabel)
.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
.addCountAndPassIfR0Equals(
- (ETHER_HEADER_LEN + IPV6_HEADER_LEN + PING_HEADER_LENGTH + firstByte.size)
- .toLong(),
- PASSED_IPV6_ICMP
+ (ETHER_HEADER_LEN + IPV6_HEADER_LEN + PING_HEADER_LENGTH + firstByte.size)
+ .toLong(),
+ PASSED_IPV6_ICMP
)
- // Ping Packet Generation
- .addAllocate(pingRequestPktLen)
- // Eth header
- .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // dst MAC address
- .addPacketCopy(ETHER_DST_ADDR_OFFSET, ETHER_ADDR_LEN) // src MAC address
- .addWriteU16(ETH_P_IPV6) // IPv6 type
- // IPv6 Header
- .addWrite32(0x60000000) // IPv6 Header: version, traffic class, flowlabel
- // payload length (2 bytes) | next header: ICMPv6 (1 byte) | hop limit (1 byte)
- .addWrite32(pingRequestIpv6PayloadLen shl 16 or (IPPROTO_ICMPV6 shl 8 or 64))
- .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // src ip
- .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // dst ip
- // ICMPv6
- .addWriteU8(0x80) // type: echo request
- .addWriteU8(0) // code
- .addWriteU16(pingRequestIpv6PayloadLen) // checksum
- // identifier
- .addPacketCopy(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_MIN_LEN, 2)
- .addWriteU16(0) // sequence number
- .addDataCopy(firstByte) // data
- .addTransmitL4(
+
+ val numOfPacketToTransmit = 3
+ val expectReplyPayloads = (0 until numOfPacketToTransmit).map { Random.nextBytes(1) }
+ expectReplyPayloads.forEach { replyPingPayload ->
+ // Ping Packet Generation
+ gen.addAllocate(pingRequestPktLen)
+ // Eth header
+ .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // dst MAC address
+ .addPacketCopy(ETHER_DST_ADDR_OFFSET, ETHER_ADDR_LEN) // src MAC address
+ .addWriteU16(ETH_P_IPV6) // IPv6 type
+ // IPv6 Header
+ .addWrite32(0x60000000) // IPv6 Header: version, traffic class, flowlabel
+ // payload length (2 bytes) | next header: ICMPv6 (1 byte) | hop limit (1 byte)
+ .addWrite32(pingRequestIpv6PayloadLen shl 16 or (IPPROTO_ICMPV6 shl 8 or 64))
+ .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // src ip
+ .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // dst ip
+ // ICMPv6
+ .addWriteU8(ICMP6_ECHO_REQUEST)
+ .addWriteU8(0) // code
+ .addWriteU16(pingRequestIpv6PayloadLen) // checksum
+ // identifier
+ .addPacketCopy(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_MIN_LEN, 2)
+ .addWriteU16(0) // sequence number
+ .addDataCopy(replyPingPayload) // data
+ .addTransmitL4(
ETHER_HEADER_LEN, // ip_ofs
ICMP6_CHECKSUM_OFFSET, // csum_ofs
IPV6_SRC_ADDR_OFFSET, // csum_start
IPPROTO_ICMPV6, // partial_sum
false // udp
- )
- // Warning: the program abuse DROPPED_IPV6_MULTICAST_PING for debugging purpose
- .addCountAndDrop(DROPPED_IPV6_MULTICAST_PING)
- .defineLabel(skipPacketLabel)
- .addPass()
- .generate()
+ )
+ }
+ // Warning: the program abuse DROPPED_IPV6_NS_REPLIED_NON_DAD for debugging purpose
+ gen.addCountAndDrop(DROPPED_IPV6_NS_REPLIED_NON_DAD)
+ .defineLabel(skipPacketLabel)
+ .addPass()
+
+ val program = gen.generate()
installAndVerifyProgram(program)
- packetReader.sendPing(payload, payloadSize)
-
- val replyPayload = try {
+ packetReader.sendPing(payload, payloadSize, expectReplyCount = numOfPacketToTransmit)
+ val replyPayloads = try {
packetReader.expectPingReply(TIMEOUT_MS * 2)
} catch (e: TimeoutException) {
- byteArrayOf() // Empty payload if timeout occurs
+ emptyList()
}
val apfCounterTracker = ApfCounterTracker()
apfCounterTracker.updateCountersFromData(readProgram())
Log.i(TAG, "counter map: ${apfCounterTracker.counters}")
- assertThat(replyPayload).isEqualTo(firstByte)
+ assertThat(replyPayloads.size).isEqualTo(expectReplyPayloads.size)
+
+ // Sort the payload list before comparison to ensure consistency.
+ val sortedReplyPayloads = replyPayloads.sortedBy { it[0] }
+ val sortedExpectReplyPayloads = expectReplyPayloads.sortedBy { it[0] }
+ for (i in sortedReplyPayloads.indices) {
+ assertThat(sortedReplyPayloads[i]).isEqualTo(sortedExpectReplyPayloads[i])
+ }
}
}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index ceb48d4..faaadee 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -88,9 +88,11 @@
import com.android.net.module.util.ArrayTrackRecord;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.com.android.testutils.CarrierConfigRule;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -110,6 +112,9 @@
public class ConnectivityDiagnosticsManagerTest {
private static final String TAG = ConnectivityDiagnosticsManagerTest.class.getSimpleName();
+ @Rule
+ public final CarrierConfigRule mCarrierConfigRule = new CarrierConfigRule();
+
private static final int CALLBACK_TIMEOUT_MILLIS = 5000;
private static final int NO_CALLBACK_INVOKED_TIMEOUT = 500;
private static final long TIMESTAMP = 123456789L;
@@ -264,9 +269,6 @@
doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable(
subId, carrierConfigReceiver, testNetworkCallback);
}, () -> {
- runWithShellPermissionIdentity(
- () -> mCarrierConfigManager.overrideConfig(subId, null),
- android.Manifest.permission.MODIFY_PHONE_STATE);
mConnectivityManager.unregisterNetworkCallback(testNetworkCallback);
mContext.unregisterReceiver(carrierConfigReceiver);
});
@@ -291,9 +293,9 @@
CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
new String[] {getCertHashForThisPackage()});
+ mCarrierConfigRule.addConfigOverrides(subId, carrierConfigs);
runWithShellPermissionIdentity(
() -> {
- mCarrierConfigManager.overrideConfig(subId, carrierConfigs);
mCarrierConfigManager.notifyConfigChangedForSubId(subId);
},
android.Manifest.permission.MODIFY_PHONE_STATE);
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 3a8252a..7292c5d 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -90,7 +90,6 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
-import static android.net.cts.util.CtsNetUtils.ConnectivityActionReceiver;
import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
import static android.net.cts.util.CtsNetUtils.NETWORK_CALLBACK_ACTION;
import static android.net.cts.util.CtsNetUtils.TEST_HOST;
@@ -111,6 +110,7 @@
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.networkstack.apishim.ConstantsShim.RECEIVER_NOT_EXPORTED;
import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.MiscAsserts.assertEventuallyTrue;
@@ -127,6 +127,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import android.annotation.NonNull;
@@ -145,6 +146,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;
@@ -176,6 +178,7 @@
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.ConditionVariable;
import android.os.Handler;
import android.os.Looper;
import android.os.MessageQueue;
@@ -201,6 +204,7 @@
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;
@@ -249,7 +253,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;
@@ -306,6 +309,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;
@@ -861,6 +865,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.
@@ -871,6 +924,7 @@
public void testOpenConnection() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
+ assumeFalse(Build.MODEL.contains("Cuttlefish"));
Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
Network cellNetwork = networkCallbackRule.requestCell();
@@ -886,8 +940,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));
}
@@ -919,17 +995,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));
@@ -1165,42 +1230,39 @@
* {@link #testRegisterNetworkCallback} except that a {@code PendingIntent} is used instead
* of a {@code NetworkCallback}.
*/
- @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
@Test
public void testRegisterNetworkCallback_withPendingIntent() {
- assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
+ final ConditionVariable received = new ConditionVariable();
- // Create a ConnectivityActionReceiver that has an IntentFilter for our locally defined
- // action, NETWORK_CALLBACK_ACTION.
- final IntentFilter filter = new IntentFilter();
- filter.addAction(NETWORK_CALLBACK_ACTION);
+ // Register a callback with intent and a request for any Internet-providing network,
+ // which should match the currently connected network.
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ received.open();
+ }
+ };
- final ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
- mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
- final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0;
- mContext.registerReceiver(receiver, filter, flags);
+ final int flags = SdkLevel.isAtLeastT() ? RECEIVER_NOT_EXPORTED : 0;
+ mContext.registerReceiver(receiver, new IntentFilter(NETWORK_CALLBACK_ACTION), flags);
// Create a broadcast PendingIntent for NETWORK_CALLBACK_ACTION.
final Intent intent = new Intent(NETWORK_CALLBACK_ACTION)
.setPackage(mContext.getPackageName());
- // While ConnectivityService would put extra info such as network or request id before
- // broadcasting the inner intent. The MUTABLE flag needs to be added accordingly.
final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0 /*requestCode*/,
intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
- // We will register for a WIFI network being available or lost.
- mCm.registerNetworkCallback(makeWifiNetworkRequest(), pendingIntent);
+ // Register for a network providing Internet being available or lost.
+ final NetworkRequest nr = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+ mCm.registerNetworkCallback(nr, pendingIntent);
try {
- mCtsNetUtils.ensureWifiConnected();
-
- // Now we expect to get the Intent delivered notifying of the availability of the wifi
- // network even if it was already connected as a state-based action when the callback
- // is registered.
- assertTrue("Did not receive expected Intent " + intent + " for TRANSPORT_WIFI",
- receiver.waitForState());
- } catch (InterruptedException e) {
- fail("Broadcast receiver or NetworkCallback wait was interrupted.");
+ // Wait for delivery of the Intent notifying of the availability of the
+ // INTERNET-providing network. Test setup makes sure it's already connected.
+ assertTrue("Did not receive expected Intent " + intent + " for INTERNET",
+ received.block(NETWORK_CALLBACK_TIMEOUT_MS));
} finally {
mCm.unregisterNetworkCallback(pendingIntent);
pendingIntent.cancel();
@@ -1208,6 +1270,31 @@
}
}
+ // Up to R ConnectivityService can't be updated through mainline, and there was a bug
+ // where registering a callback with a canceled pending intent would crash the system.
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testRegisterNetworkCallback_pendingIntent_classNotFound() {
+ final Intent intent = new Intent()
+ .setClassName(mContext.getPackageName(), "NonExistent");
+ final PendingIntent pi = PendingIntent.getActivity(mContext, /* requestCode */ 1,
+ intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
+
+ final NetworkRequest nr = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+ try {
+ // Before the fix delivered through Mainline, this used to crash the system, because
+ // trying to send the pending intent would throw which would prompt ConnectivityService
+ // to release the wake lock, but it would still send a finished notification at which
+ // point CS would try to release the wake lock again and crash.
+ mCm.registerNetworkCallback(nr, pi);
+ } finally {
+ mCm.unregisterNetworkCallback(pi);
+ pi.cancel();
+ }
+ }
+
private void runIdenticalPendingIntentsRequestTest(boolean useListen) throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
@@ -1553,7 +1640,7 @@
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
final ContentResolver resolver = mContext.getContentResolver();
mCtsNetUtils.ensureWifiConnected();
- final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+ final String ssid = unquoteSSID(getSSID());
final String oldMeteredSetting = getWifiMeteredStatus(ssid);
final String oldMeteredMultipathPreference = Settings.Global.getString(
resolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
@@ -1566,7 +1653,7 @@
// since R.
final Network network = setWifiMeteredStatusAndWait(ssid, true /* isMetered */,
false /* waitForValidation */);
- assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID()));
+ assertEquals(ssid, unquoteSSID(getSSID()));
assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
NET_CAPABILITY_NOT_METERED), false);
assertMultipathPreferenceIsEventually(network, initialMeteredPreference,
@@ -1713,7 +1800,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;
@@ -2266,8 +2354,10 @@
// Verify that turning airplane mode off takes effect as expected.
// connectToCell only registers a request, it cannot / does not need to be called twice
- mCtsNetUtils.ensureWifiConnected();
- if (verifyWifi) waitForAvailable(wifiCb);
+ if (verifyWifi) {
+ mCtsNetUtils.ensureWifiConnected();
+ waitForAvailable(wifiCb);
+ }
if (supportTelephony) {
telephonyCb.eventuallyExpect(
CallbackEntry.AVAILABLE, CELL_DATA_AVAILABLE_TIMEOUT_MS);
@@ -2366,7 +2456,7 @@
mPackageManager.hasSystemFeature(FEATURE_WIFI));
final Network network = mCtsNetUtils.ensureWifiConnected();
- final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+ final String ssid = unquoteSSID(getSSID());
assertNotNull("Ssid getting from WifiManager is null", ssid);
// This package should have no NETWORK_SETTINGS permission. Verify that no ssid is contained
// in the NetworkCapabilities.
@@ -2877,6 +2967,15 @@
new Handler(Looper.getMainLooper())), NETWORK_SETTINGS);
}
+ /**
+ * It needs android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
+ * to use WifiManager.getConnectionInfo() on the visible background user.
+ */
+ private String getSSID() {
+ return runWithShellPermissionIdentity(() ->
+ mWifiManager.getConnectionInfo().getSSID());
+ }
+
private static final class OnCompleteListenerCallback {
final CompletableFuture<Object> mDone = new CompletableFuture<>();
@@ -4011,4 +4110,11 @@
// shims, and @IgnoreUpTo does not check that.
assumeTrue(TestUtils.shouldTestSApis());
}
+
+ @Test
+ public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ assertThrows(UnsupportedOperationException.class, () -> mCm.tether("iface"));
+ assertThrows(UnsupportedOperationException.class, () -> mCm.untether("iface"));
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
new file mode 100644
index 0000000..ff608f2
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
@@ -0,0 +1,185 @@
+/*
+ * 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
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.net.DnsResolver
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.MacAddress
+import android.net.RouteInfo
+import android.os.CancellationSignal
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_NETD_NATIVE
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
+import com.android.testutils.AutoReleaseNetworkCallbackRule
+import com.android.testutils.DeviceConfigRule
+import com.android.testutils.DnsResolverModuleTest
+import com.android.testutils.IPv6UdpFilter
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RouterAdvertisementResponder
+import com.android.testutils.TapPacketReaderRule
+import com.android.testutils.TestableNetworkAgent
+import com.android.testutils.TestDnsPacket
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule
+import com.android.testutils.runAsShell
+import java.net.Inet6Address
+import java.net.InetAddress
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val TEST_DNSSERVER_MAC = MacAddress.fromString("00:11:22:33:44:55")
+private val TAG = DnsResolverTapTest::class.java.simpleName
+private const val TEST_TIMEOUT_MS = 10_000L
+
+@AppModeFull(reason = "Test networks cannot be created in instant app mode")
+@DnsResolverModuleTest
+@RunWith(AndroidJUnit4::class)
+class DnsResolverTapTest {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val handlerThread = HandlerThread(TAG)
+
+ @get:Rule(order = 1)
+ val deviceConfigRule = DeviceConfigRule()
+
+ @get:Rule(order = 2)
+ val featureFlagsRule = SetFeatureFlagsRule(
+ setFlagsMethod = { name, enabled ->
+ val value = when (enabled) {
+ null -> null
+ true -> "1"
+ false -> "0"
+ }
+ deviceConfigRule.setConfig(NAMESPACE_NETD_NATIVE, name, value)
+ },
+ getFlagsMethod = {
+ runAsShell(READ_DEVICE_CONFIG) {
+ DeviceConfig.getInt(NAMESPACE_NETD_NATIVE, it, 0) == 1
+ }
+ }
+ )
+
+ @get:Rule(order = 3)
+ val packetReaderRule = TapPacketReaderRule()
+
+ @get:Rule(order = 4)
+ val cbRule = AutoReleaseNetworkCallbackRule()
+
+ private val ndResponder by lazy { RouterAdvertisementResponder(packetReaderRule.reader) }
+ private val dnsServerAddr by lazy {
+ parseNumericAddress("fe80::124%${packetReaderRule.iface.interfaceName}") as Inet6Address
+ }
+ private lateinit var agent: TestableNetworkAgent
+
+ @Before
+ fun setUp() {
+ handlerThread.start()
+ val interfaceName = packetReaderRule.iface.interfaceName
+ val cb = cbRule.requestNetwork(TestableNetworkAgent.makeNetworkRequestForInterface(
+ interfaceName))
+ agent = runAsShell(MANAGE_TEST_NETWORKS) {
+ TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ interfaceName, TEST_TIMEOUT_MS)
+ }
+ ndResponder.addNeighborEntry(TEST_DNSSERVER_MAC, dnsServerAddr)
+ ndResponder.start()
+ agent.lp.apply {
+ addDnsServer(dnsServerAddr)
+ // A default route is needed for DnsResolver.java to send queries over IPv6
+ // (see usage of DnsUtils.haveIpv6).
+ addRoute(RouteInfo(IpPrefix("::/0"), null, null))
+ }
+ agent.sendLinkProperties(agent.lp)
+ cb.eventuallyExpect<LinkPropertiesChanged> { it.lp.dnsServers.isNotEmpty() }
+ }
+
+ @After
+ fun tearDown() {
+ ndResponder.stop()
+ if (::agent.isInitialized) {
+ agent.unregister()
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ private class DnsCallback : DnsResolver.Callback<List<InetAddress>> {
+ override fun onAnswer(answer: List<InetAddress>, rcode: Int) = Unit
+ override fun onError(error: DnsResolver.DnsException) = Unit
+ }
+
+ /**
+ * Run a cancellation test.
+ *
+ * @param domain Domain name to query
+ * @param waitTimeForNoRetryAfterCancellationMs If positive, cancel the query and wait for that
+ * delay to check no retry is sent.
+ * @return The duration it took to receive all expected replies.
+ */
+ fun doCancellationTest(domain: String, waitTimeForNoRetryAfterCancellationMs: Long): Long {
+ val cancellationSignal = CancellationSignal()
+ val dnsCb = DnsCallback()
+ val queryStart = SystemClock.elapsedRealtime()
+ DnsResolver.getInstance().query(
+ agent.network, domain, 0 /* flags */,
+ Runnable::run /* executor */, cancellationSignal, dnsCb
+ )
+
+ if (waitTimeForNoRetryAfterCancellationMs > 0) {
+ cancellationSignal.cancel()
+ }
+ // Filter for queries on UDP port 53 for the specified domain
+ val filter = IPv6UdpFilter(dstPort = 53).and {
+ TestDnsPacket(
+ it.copyOfRange(ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, it.size),
+ dstAddr = dnsServerAddr
+ ).isQueryFor(domain, DnsResolver.TYPE_AAAA)
+ }
+
+ val reader = packetReaderRule.reader
+ assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Original query not found")
+ if (waitTimeForNoRetryAfterCancellationMs > 0) {
+ assertNull(reader.poll(waitTimeForNoRetryAfterCancellationMs, filter),
+ "Expected no retry query")
+ } else {
+ assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Retry query not found")
+ }
+ return SystemClock.elapsedRealtime() - queryStart
+ }
+
+ @SetFeatureFlagsRule.FeatureFlag("no_retry_after_cancel", true)
+ @Test
+ fun testCancellation() {
+ val timeWithRetryWhenNotCancelled = doCancellationTest("test1.example.com",
+ waitTimeForNoRetryAfterCancellationMs = 0L)
+ doCancellationTest("test2.example.com",
+ waitTimeForNoRetryAfterCancellationMs = timeWithRetryWhenNotCancelled + 50L)
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index fa44ae9..b66b853 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -58,6 +58,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.net.module.util.DnsPacket;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DeviceConfigRule;
@@ -394,7 +395,22 @@
@Test
@DnsResolverModuleTest
public void testRawQueryNXDomainWithPrivateDns() throws Exception {
- doTestRawQueryNXDomainWithPrivateDns(mExecutor);
+ try {
+ doTestRawQueryNXDomainWithPrivateDns(mExecutor);
+ } catch (Throwable e) {
+ final ConnectivityDiagnosticsCollector collector =
+ ConnectivityDiagnosticsCollector.getInstance();
+ if (collector != null) {
+ // IWLAN on U QPR3 release may cause failures in this test, see
+ // CarrierConfigSetupTest which is supposed to avoid the issue. Collect IWLAN
+ // related dumpsys if the test still fails.
+ collector.collectDumpsys("carrier_config", e);
+ collector.collectDumpsys("telecom", e);
+ collector.collectDumpsys("telephony_ims", e);
+ collector.collectDumpsys("telephony.registry", e);
+ }
+ throw e;
+ }
}
@Test
diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java
index b1e5680..e367b7d 100644
--- a/tests/cts/net/src/android/net/cts/DnsTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -26,6 +27,7 @@
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkInfo;
+import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
@@ -299,6 +301,8 @@
}
private void ensureIpv6Connectivity() throws InterruptedException {
+ assumeFalse(Build.MODEL.contains("Cuttlefish"));
+
CountDownLatch latch = new CountDownLatch(1);
final int TIMEOUT_MS = 5_000;
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index 1de4cf9..ceccf0b 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -44,6 +44,7 @@
import android.net.RouteInfo
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
+import android.net.TestNetworkManager.TestInterfaceRequest
import android.net.cts.util.CtsNetUtils.TestNetworkCallback
import android.os.HandlerThread
import android.os.SystemClock
@@ -164,7 +165,11 @@
// Only statically configure the IPv4 address; for IPv6, use the SLAAC generated
// address.
- iface = tnm.createTapInterface(arrayOf(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN)))
+ val req = TestInterfaceRequest.Builder()
+ .setTap()
+ .addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN))
+ .build()
+ iface = tnm.createTestInterface(req)
assertNotNull(iface)
}
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 1e2a212..9f32132 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -51,6 +51,7 @@
import android.net.StaticIpConfiguration
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
+import android.net.TestNetworkManager.TestInterfaceRequest
import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.EthernetStateChanged
import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
import android.os.Build
@@ -77,6 +78,7 @@
import com.android.testutils.assertThrows
import com.android.testutils.runAsShell
import com.android.testutils.waitForIdle
+import com.google.common.truth.Truth.assertThat
import java.io.IOException
import java.net.Inet6Address
import java.net.Socket
@@ -145,6 +147,8 @@
private var tetheredInterfaceRequest: TetheredInterfaceRequest? = null
+ private var ethernetEnabled = true
+
private class EthernetTestInterface(
context: Context,
private val handler: Handler,
@@ -166,7 +170,12 @@
// false, it is subsequently disabled. This means that the interface may briefly get
// link. With IPv6 provisioning delays (RS delay and DAD) disabled, this can cause
// tests that expect no network to come up when hasCarrier is false to become flaky.
- tnm.createTapInterface(hasCarrier, false /* bringUp */)
+ val req = TestInterfaceRequest.Builder()
+ .setTap()
+ .setHasCarrier(hasCarrier)
+ .setBringUp(false)
+ .build()
+ tnm.createTestInterface(req)
}
val mtu = tapInterface.mtu
packetReader = PollPacketReader(
@@ -428,7 +437,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
@@ -514,6 +523,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)
@@ -600,27 +610,10 @@
}
}
- @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() {
+ // Requesting a tethered interface will stop IpClient. Prevent it from doing so
+ // if adb is connected over ethernet.
+ assumeFalse(isAdbOverEthernet())
// Interfaces that have configured NetworkCapabilities will never be used for tethering,
// see aosp/2123900.
try {
@@ -911,6 +904,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))
@@ -1018,4 +1035,91 @@
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() {
+ // do not run this test if an interface that can be used for tethering already exists.
+ assumeNoInterfaceForTetheringAvailable()
+
+ 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)
+ }
+
+ @Test
+ fun testGetInterfaceList_disableEnableEthernet() {
+ // Test that interface list can be obtained when ethernet is disabled.
+ setEthernetEnabled(false)
+ // Create two test interfaces and check the return list contains the interface names.
+ val iface1 = createInterface()
+ val iface2 = createInterface()
+ var ifaces = em.getInterfaceList()
+ assertThat(ifaces).containsAtLeast(iface1.name, iface2.name)
+
+ // Remove one existing test interface and check the return list doesn't contain the
+ // removed interface name.
+ removeInterface(iface1)
+ ifaces = em.getInterfaceList()
+ assertThat(ifaces).doesNotContain(iface1.name)
+ assertThat(ifaces).contains(iface2.name)
+
+ removeInterface(iface2)
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index e94d94f..4a21f09 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -375,6 +375,7 @@
}
private static boolean isIpv6UdpEncapSupportedByKernel() {
+ if (SdkLevel.isAtLeastB() && isKernelVersionAtLeast("5.10.0")) return true;
return isKernelVersionAtLeast("5.15.31")
|| (isKernelVersionAtLeast("5.10.108") && !isKernelVersionAtLeast("5.15.0"));
}
@@ -390,8 +391,8 @@
assumeTrue("Not supported by kernel", isIpv6UdpEncapSupportedByKernel());
}
- // TODO: b/319532485 Figure out whether to support x86_32
private static boolean isRequestTransformStateSupportedByKernel() {
+ if (SdkLevel.isAtLeastB()) return true;
return NetworkUtils.isKernel64Bit() || !NetworkUtils.isKernelX86();
}
diff --git a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
new file mode 100644
index 0000000..484cce8
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2025 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
+
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.PSM_ANY
+import android.net.L2capNetworkSpecifier.ROLE_CLIENT
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.MacAddress
+import android.os.Build
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capNetworkSpecifierTest {
+ @Test
+ fun testParcelUnparcel() {
+ val remoteMac = MacAddress.fromString("01:02:03:04:05:06")
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setPsm(42)
+ .setRemoteAddress(remoteMac)
+ .build()
+ assertParcelingIsLossless(specifier)
+ }
+
+ @Test
+ fun testGetters() {
+ val remoteMac = MacAddress.fromString("11:22:33:44:55:66")
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setPsm(123)
+ .setRemoteAddress(remoteMac)
+ .build()
+ assertEquals(ROLE_CLIENT, specifier.getRole())
+ assertEquals(HEADER_COMPRESSION_NONE, specifier.getHeaderCompression())
+ assertEquals(123, specifier.getPsm())
+ assertEquals(remoteMac, specifier.getRemoteAddress())
+ }
+
+ @Test
+ fun testCanBeSatisfiedBy() {
+ val blanketOffer = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_ANY)
+ .setPsm(PSM_ANY)
+ .build()
+
+ val reservedOffer = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setPsm(42)
+ .build()
+
+ val clientOffer = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_ANY)
+ .build()
+
+ val serverReservation = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+
+ assertTrue(serverReservation.canBeSatisfiedBy(blanketOffer))
+ assertTrue(serverReservation.canBeSatisfiedBy(reservedOffer))
+ // Note: serverReservation can be filed using reserveNetwork, or it could be a regular
+ // request filed using requestNetwork.
+ assertFalse(serverReservation.canBeSatisfiedBy(clientOffer))
+
+ val clientRequest = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setRemoteAddress(MacAddress.fromString("00:01:02:03:04:05"))
+ .setPsm(42)
+ .build()
+
+ assertTrue(clientRequest.canBeSatisfiedBy(clientOffer))
+ // Note: the BlanketOffer also includes a RES_ID_MATCH_ALL_RESERVATIONS. Since the
+ // clientRequest is not a reservation, it won't match that request to begin with.
+ assertFalse(clientRequest.canBeSatisfiedBy(blanketOffer))
+ assertFalse(clientRequest.canBeSatisfiedBy(reservedOffer))
+
+ val matchAny = L2capNetworkSpecifier.Builder().build()
+ assertTrue(matchAny.canBeSatisfiedBy(blanketOffer))
+ assertTrue(matchAny.canBeSatisfiedBy(reservedOffer))
+ assertTrue(matchAny.canBeSatisfiedBy(clientOffer))
+ }
+}
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..8fcc703 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,10 +109,15 @@
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
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.PollPacketReader
import com.android.testutils.RecorderCallback.CallbackEntry.Available
import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
@@ -131,19 +140,24 @@
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.assertThrows
+import com.android.testutils.com.android.testutils.CarrierConfigRule
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
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
+import java.nio.ByteBuffer
import java.security.MessageDigest
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
@@ -154,6 +168,7 @@
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
@@ -165,10 +180,12 @@
import org.mockito.Mockito.verify
private const val TAG = "NetworkAgentTest"
+
// This test doesn't really have a constraint on how fast the methods should return. If it's
// going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
// without affecting the run time of successful runs. Thus, set a very high timeout.
private const val DEFAULT_TIMEOUT_MS = 5000L
+
// When waiting for a NetworkCallback to determine there was no timeout, waiting is the
// only possible thing (the relevant handler is the one in the real ConnectivityService,
// and then there is the Binder call), so have a short timeout for this as it will be
@@ -188,6 +205,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.
@@ -205,6 +227,9 @@
@IgnoreUpTo(Build.VERSION_CODES.R)
@RunWith(DevSdkIgnoreRunner::class)
class NetworkAgentTest {
+ @get:Rule
+ val carrierConfigRule = CarrierConfigRule()
+
private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
@@ -321,6 +346,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 +375,7 @@
private fun createConnectedNetworkAgent(
context: Context = realContext,
+ lp: LinkProperties? = null,
specifier: String? = UUID.randomUUID().toString(),
initialConfig: NetworkAgentConfig? = null,
expectedInitSignalStrengthThresholds: IntArray = intArrayOf(),
@@ -350,7 +385,12 @@
// 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 +401,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 +423,10 @@
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)
}
@@ -515,9 +558,12 @@
@Test
fun testSocketKeepalive(): Unit = createNetworkAgentWithFakeCS().let { agent ->
val packet = NattKeepalivePacketData(
- LOCAL_IPV4_ADDRESS /* srcAddress */, 1234 /* srcPort */,
- REMOTE_IPV4_ADDRESS /* dstAddress */, 4567 /* dstPort */,
- ByteArray(100 /* size */))
+ LOCAL_IPV4_ADDRESS /* srcAddress */,
+ 1234 /* srcPort */,
+ REMOTE_IPV4_ADDRESS /* dstAddress */,
+ 4567 /* dstPort */,
+ ByteArray(100 /* size */)
+ )
val slot = 4
val interval = 37
@@ -622,8 +668,13 @@
uid: Int,
expectUidsPresent: Boolean
) {
- doTestAllowedUids(intArrayOf(transport), uid, expectUidsPresent,
- specifier = null, transportInfo = null)
+ doTestAllowedUids(
+ intArrayOf(transport),
+ uid,
+ expectUidsPresent,
+ specifier = null,
+ transportInfo = null
+ )
}
private fun doTestAllowedUidsWithSubId(
@@ -658,21 +709,28 @@
private fun setHoldCarrierPrivilege(hold: Boolean, subId: Int) {
fun getCertHash(): String {
- val pkgInfo = realContext.packageManager.getPackageInfo(realContext.opPackageName,
- PackageManager.GET_SIGNATURES)
+ val pkgInfo = realContext.packageManager.getPackageInfo(
+ realContext.opPackageName,
+ PackageManager.GET_SIGNATURES
+ )
val digest = MessageDigest.getInstance("SHA-256")
val certHash = digest.digest(pkgInfo.signatures!![0]!!.toByteArray())
return UiccUtil.bytesToHexString(certHash)!!
}
val tm = realContext.getSystemService(TelephonyManager::class.java)!!
- val ccm = realContext.getSystemService(CarrierConfigManager::class.java)!!
val cv = ConditionVariable()
val cpb = PrivilegeWaiterCallback(cv)
- tryTest {
+ // The lambda below is capturing |cpb|, whose type inherits from a class that appeared in
+ // T. This means the lambda will compile as a private method of this class taking a
+ // PrivilegeWaiterCallback argument. As JUnit uses reflection to enumerate all methods
+ // including private methods, this would fail with a link error when running on S-.
+ // To solve this, make the lambda serializable, which causes the compiler to emit a
+ // synthetic class instead of a synthetic method.
+ tryTest @JvmSerializableLambda {
val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
}
// Wait for the callback to be registered
@@ -685,21 +743,16 @@
}
return@tryTest
}
- cv.close()
- runAsShell(MODIFY_PHONE_STATE) {
- val carrierConfigs = if (hold) {
- PersistableBundle().also {
- it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
- arrayOf(getCertHash()))
- }
- } else {
- null
- }
- ccm.overrideConfig(subId, carrierConfigs)
+ if (hold) {
+ carrierConfigRule.addConfigOverrides(subId, PersistableBundle().also {
+ it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
+ arrayOf(getCertHash()))
+ })
+ } else {
+ carrierConfigRule.cleanUpNow()
}
- assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't change carrier privilege")
- } cleanup {
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ } cleanup @JvmSerializableLambda {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.unregisterCarrierPrivilegesCallback(cpb)
}
}
@@ -713,9 +766,15 @@
val cv = ConditionVariable()
val cpb = CarrierServiceChangedWaiterCallback(cv)
- tryTest {
+ // The lambda below is capturing |cpb|, whose type inherits from a class that appeared in
+ // T. This means the lambda will compile as a private method of this class taking a
+ // PrivilegeWaiterCallback argument. As JUnit uses reflection to enumerate all methods
+ // including private methods, this would fail with a link error when running on S-.
+ // To solve this, make the lambda serializable, which causes the compiler to emit a
+ // synthetic class instead of a synthetic method.
+ tryTest @JvmSerializableLambda {
val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
}
// Wait for the callback to be registered
@@ -737,8 +796,8 @@
}
}
assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't modify carrier service package")
- } cleanup {
- runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ } cleanup @JvmSerializableLambda {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) @JvmSerializableLambda {
tm.unregisterCarrierPrivilegesCallback(cpb)
}
}
@@ -768,14 +827,19 @@
val uid = try {
realContext.packageManager.getApplicationInfo(servicePackage, 0).uid
} catch (e: PackageManager.NameNotFoundException) {
- fail("$servicePackage could not be installed, please check the SuiteApkInstaller" +
- " installed CtsCarrierServicePackage.apk", e)
+ fail(
+ "$servicePackage could not be installed, please check the SuiteApkInstaller" +
+ " installed CtsCarrierServicePackage.apk",
+ e
+ )
}
val tm = realContext.getSystemService(TelephonyManager::class.java)!!
val defaultSubId = SubscriptionManager.getDefaultSubscriptionId()
- assertTrue(defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID,
- "getDefaultSubscriptionId returns INVALID_SUBSCRIPTION_ID")
+ assertTrue(
+ defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+ "getDefaultSubscriptionId returns INVALID_SUBSCRIPTION_ID"
+ )
tryTest {
// This process is not the carrier service UID, so allowedUids should be ignored in all
// the following cases.
@@ -879,8 +943,10 @@
// If using the int ranking, agent1 must be upgraded to a better score so that there is
// no ambiguity when agent2 connects that agent1 is still better. If using policy
// ranking, this is not necessary.
- agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE)
- .build())
+ agent1.sendNetworkScore(
+ NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE)
+ .build()
+ )
// Connect the second agent.
val (agent2, _) = createConnectedNetworkAgent()
@@ -889,10 +955,12 @@
// virtue of already satisfying the request.
callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
// Now downgrade the score and expect the callback now prefers agent2
- agent1.sendNetworkScore(NetworkScore.Builder()
+ agent1.sendNetworkScore(
+ NetworkScore.Builder()
.setLegacyInt(WORSE_NETWORK_SCORE)
.setExiting(true)
- .build())
+ .build()
+ )
callback.expect<Available>(agent2.network!!)
// tearDown() will unregister the requests and agents
@@ -937,16 +1005,20 @@
// Check that the default network's transport is propagated to the VPN.
var vpnNc = mCM.getNetworkCapabilities(agent.network!!)
assertNotNull(vpnNc)
- assertEquals(VpnManager.TYPE_VPN_SERVICE,
- (vpnNc.transportInfo as VpnTransportInfo).type)
+ assertEquals(
+ VpnManager.TYPE_VPN_SERVICE,
+ (vpnNc.transportInfo as VpnTransportInfo).type
+ )
assertEquals(mySessionId, (vpnNc.transportInfo as VpnTransportInfo).sessionId)
val testAndVpn = intArrayOf(TRANSPORT_TEST, TRANSPORT_VPN)
assertTrue(vpnNc.hasAllTransports(testAndVpn))
assertFalse(vpnNc.hasCapability(NET_CAPABILITY_NOT_VPN))
- assertTrue(vpnNc.hasAllTransports(defaultNetworkTransports),
- "VPN transports ${Arrays.toString(vpnNc.transportTypes)}" +
- " lacking transports from ${Arrays.toString(defaultNetworkTransports)}")
+ assertTrue(
+ vpnNc.hasAllTransports(defaultNetworkTransports),
+ "VPN transports ${Arrays.toString(vpnNc.transportTypes)}" +
+ " lacking transports from ${Arrays.toString(defaultNetworkTransports)}"
+ )
// Check that when no underlying networks are announced the underlying transport disappears.
agent.setUnderlyingNetworks(listOf<Network>())
@@ -968,9 +1040,11 @@
// underlying networks, and because not congested, not roaming, and not suspended are the
// default anyway. It's still useful as an extra check though.
vpnNc = mCM.getNetworkCapabilities(agent.network!!)!!
- for (cap in listOf(NET_CAPABILITY_NOT_CONGESTED,
- NET_CAPABILITY_NOT_ROAMING,
- NET_CAPABILITY_NOT_SUSPENDED)) {
+ for (cap in listOf(
+ NET_CAPABILITY_NOT_CONGESTED,
+ NET_CAPABILITY_NOT_ROAMING,
+ NET_CAPABILITY_NOT_SUSPENDED
+ )) {
val capStr = valueToString(NetworkCapabilities::class.java, "NET_CAPABILITY_", cap)
if (defaultNetworkCapabilities.hasCapability(cap) && !vpnNc.hasCapability(cap)) {
fail("$capStr not propagated from underlying: $defaultNetworkCapabilities")
@@ -995,13 +1069,15 @@
doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
val agent = createNetworkAgent(mockContext)
agent.register()
- verify(mockCm).registerNetworkAgent(any(),
- argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
- any(LinkProperties::class.java),
- any(NetworkCapabilities::class.java),
- any(NetworkScore::class.java),
- any(NetworkAgentConfig::class.java),
- eq(NetworkProvider.ID_NONE))
+ verify(mockCm).registerNetworkAgent(
+ any(),
+ argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
+ any(LinkProperties::class.java),
+ any(NetworkCapabilities::class.java),
+ any(NetworkScore::class.java),
+ any(NetworkAgentConfig::class.java),
+ eq(NetworkProvider.ID_NONE)
+ )
}
@Test
@@ -1048,8 +1124,10 @@
@Test
fun testValidationStatus() = createNetworkAgentWithFakeCS().let { agent ->
val uri = Uri.parse("http://www.google.com")
- mFakeConnectivityService.agent.onValidationStatusChanged(VALID_NETWORK,
- uri.toString())
+ mFakeConnectivityService.agent.onValidationStatusChanged(
+ VALID_NETWORK,
+ uri.toString()
+ )
agent.expectCallback<OnValidationStatus>().let {
assertEquals(it.status, VALID_NETWORK)
assertEquals(it.uri, uri)
@@ -1124,7 +1202,8 @@
}
assertFailsWith<IllegalArgumentException> {
agentWeaker.setLingerDuration(Duration.ofMillis(
- NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1))
+ NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1
+ ))
}
// Verify valid linger timer can be set, but it should not take effect since the network
// is still needed.
@@ -1134,11 +1213,14 @@
agentWeaker.setLingerDuration(Duration.ofMillis(NetworkAgent.MIN_LINGER_TIMER_MS.toLong()))
// Make a listener which can observe agentWeaker lost later.
val callbackWeaker = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
- registerNetworkCallback(NetworkRequest.Builder()
+ registerNetworkCallback(
+ NetworkRequest.Builder()
.clearCapabilities()
.addTransportType(TRANSPORT_TEST)
.setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifierWeaker))
- .build(), callbackWeaker)
+ .build(),
+ callbackWeaker
+ )
callbackWeaker.expectAvailableCallbacks(agentWeaker.network!!)
// Connect the agentStronger with a score better than agentWeaker. Verify the callback for
@@ -1155,8 +1237,10 @@
val expectedRemainingLingerDuration = lingerStart +
NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - SystemClock.elapsedRealtime()
// If the available callback is too late. The remaining duration will be reduced.
- assertTrue(expectedRemainingLingerDuration > 0,
- "expected remaining linger duration is $expectedRemainingLingerDuration")
+ assertTrue(
+ expectedRemainingLingerDuration > 0,
+ "expected remaining linger duration is $expectedRemainingLingerDuration"
+ )
callbackWeaker.assertNoCallback(expectedRemainingLingerDuration)
callbackWeaker.expect<Lost>(agentWeaker.network!!)
}
@@ -1174,20 +1258,24 @@
assertEquals(imsi, testNetworkSnapshot!!.subscriberId)
}
- @Test
- @IgnoreUpTo(Build.VERSION_CODES.R)
// TODO: Refactor helper functions to util class and move this test case to
// {@link android.net.cts.ConnectivityManagerTest}.
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
fun testRegisterBestMatchingNetworkCallback() {
// Register best matching network callback with additional condition that will be
// exercised later. This assumes the test network agent has NOT_VCN_MANAGED in it and
// does not have NET_CAPABILITY_TEMPORARILY_NOT_METERED.
val bestMatchingCb = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
- registerBestMatchingNetworkCallback(NetworkRequest.Builder()
+ registerBestMatchingNetworkCallback(
+ NetworkRequest.Builder()
.clearCapabilities()
.addTransportType(TRANSPORT_TEST)
.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
- .build(), bestMatchingCb, mHandlerThread.threadHandler)
+ .build(),
+ bestMatchingCb,
+ mHandlerThread.threadHandler
+ )
val (agent1, _) = createConnectedNetworkAgent(specifier = "AGENT-1")
bestMatchingCb.expectAvailableThenValidatedCallbacks(agent1.network!!)
@@ -1265,8 +1353,10 @@
}
fun assertNoCallback() {
- assertNull(history.poll(NO_CALLBACK_TIMEOUT),
- "Callback received")
+ assertNull(
+ history.poll(NO_CALLBACK_TIMEOUT),
+ "Callback received"
+ )
}
}
@@ -1281,7 +1371,8 @@
private fun setupForQosDatagram() = setupForQosCallbackTest {
agent: TestableNetworkAgent -> DatagramSocket(
- InetSocketAddress(InetAddress.getLoopbackAddress(), 0))
+ InetSocketAddress(InetAddress.getLoopbackAddress(), 0)
+ )
.also { assertNotNull(agent.network?.bindSocket(it)) }
}
@@ -1314,7 +1405,8 @@
assertFailsWith<QosCallbackRegistrationException>(
"The same callback cannot be " +
- "registered more than once without first being unregistered") {
+ "registered more than once without first being unregistered"
+ ) {
mCM.registerQosCallback(info, executor, qosCallback)
}
} finally {
@@ -1407,8 +1499,10 @@
qosCallback.expectCallback<OnQosSessionAvailable>()
// Check that onError is coming through correctly
- agent.sendQosCallbackError(callbackId,
- QosCallbackException.EX_TYPE_FILTER_NOT_SUPPORTED)
+ agent.sendQosCallbackError(
+ callbackId,
+ QosCallbackException.EX_TYPE_FILTER_NOT_SUPPORTED
+ )
qosCallback.expectCallback<OnError> {
it.ex.cause is UnsupportedOperationException
}
@@ -1501,15 +1595,85 @@
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
+ 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 +1683,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 +1714,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 +1737,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 +1753,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 +1803,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)
@@ -1657,8 +1823,10 @@
assertNotNull(wifiSpecifier)
assertTrue(wifiSpecifier is EthernetNetworkSpecifier)
- val wifiNc = makeTestNetworkCapabilities(wifiSpecifier.interfaceName,
- intArrayOf(TRANSPORT_WIFI))
+ val wifiNc = makeTestNetworkCapabilities(
+ wifiSpecifier.interfaceName,
+ intArrayOf(TRANSPORT_WIFI)
+ )
wifiAgent.sendNetworkCapabilities(wifiNc)
val wifiLp = mCM.getLinkProperties(wifiNetwork)!!
val newRoute = RouteInfo(IpPrefix("192.0.2.42/24"))
@@ -1674,6 +1842,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
@@ -1701,19 +1897,16 @@
val nc = makeTestNetworkCapabilities(ifName, transports).also {
if (transports.contains(TRANSPORT_VPN)) {
val sessionId = "NetworkAgentTest-${Process.myPid()}"
- it.setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, sessionId,
- /*bypassable=*/ false, /*longLivedTcpConnectionsExpensive=*/ false))
+ it.setTransportInfo(VpnTransportInfo(
+ VpnManager.TYPE_VPN_PLATFORM,
+ sessionId,
+ /*bypassable=*/ false,
+ /*longLivedTcpConnectionsExpensive=*/ false
+ ))
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.
@@ -1754,9 +1947,11 @@
listenCallback.expect<Available>(network)
requestCallback.expect<CapabilitiesChanged>(network) { it.caps.hasCapability(
- NET_CAPABILITY_TEMPORARILY_NOT_METERED) }
+ NET_CAPABILITY_TEMPORARILY_NOT_METERED
+ ) }
listenCallback.expect<CapabilitiesChanged>(network) { it.caps.hasCapability(
- NET_CAPABILITY_TEMPORARILY_NOT_METERED) }
+ NET_CAPABILITY_TEMPORARILY_NOT_METERED
+ ) }
requestCallback.expect<LinkPropertiesChanged>(network) { it.lp.equals(lp) }
listenCallback.expect<LinkPropertiesChanged>(network) { it.lp.equals(lp) }
@@ -1782,7 +1977,8 @@
fun testNativeNetworkCreation_PhysicalNetwork() {
doTestNativeNetworkCreation(
expectCreatedImmediately = SHOULD_CREATE_NETWORKS_IMMEDIATELY,
- intArrayOf(TRANSPORT_CELLULAR))
+ intArrayOf(TRANSPORT_CELLULAR)
+ )
}
@Test
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index ff10e1a..2fb140a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -16,6 +16,7 @@
package android.net.cts;
+import static android.net.ConnectivityManager.TYPE_NONE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -32,6 +33,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.fail;
@@ -41,6 +43,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
@@ -101,6 +104,16 @@
}
}
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testParceling() {
+ NetworkCapabilities nc = new NetworkCapabilities.Builder().build();
+ NetworkRequest request = new NetworkRequest(nc, TYPE_NONE, 42 /* rId */,
+ NetworkRequest.Type.RESERVATION);
+
+ assertParcelingIsLossless(request);
+ }
+
@Test
public void testCapabilities() {
assertTrue(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build()
@@ -130,7 +143,7 @@
verifyNoCapabilities(builder.build());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testTemporarilyNotMeteredCapability() {
assertTrue(new NetworkRequest.Builder()
.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
@@ -157,7 +170,6 @@
}
@Test
- @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testSpecifier() {
assertNull(new NetworkRequest.Builder().build().getNetworkSpecifier());
final WifiNetworkSpecifier specifier = new WifiNetworkSpecifier.Builder()
@@ -192,7 +204,6 @@
}
@Test
- @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testRequestorPackageName() {
assertNull(new NetworkRequest.Builder().build().getRequestorPackageName());
final String pkgName = "android.net.test";
@@ -216,7 +227,6 @@
}
@Test
- @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testCanBeSatisfiedBy() {
final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */);
@@ -284,7 +294,6 @@
}
@Test
- @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testInvariantInCanBeSatisfiedBy() {
// Test invariant that result of NetworkRequest.canBeSatisfiedBy() should be the same with
// NetworkCapabilities.satisfiedByNetworkCapabilities().
@@ -388,7 +397,7 @@
otherUidsRequest.canBeSatisfiedBy(ncWithOtherUid));
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testRequestorUid() {
final NetworkCapabilities nc = new NetworkCapabilities();
// Verify default value is INVALID_UID
@@ -558,4 +567,43 @@
.setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
.build();
}
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testNetworkReservation() {
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ final NetworkCapabilities blanketOffer = new NetworkCapabilities(nc);
+ blanketOffer.setReservationId(NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS);
+ final NetworkCapabilities specificOffer = new NetworkCapabilities(nc);
+ specificOffer.setReservationId(42);
+ final NetworkCapabilities otherSpecificOffer = new NetworkCapabilities(nc);
+ otherSpecificOffer.setReservationId(43);
+ final NetworkCapabilities regularOffer = new NetworkCapabilities(nc);
+
+ final NetworkRequest reservationNR = new NetworkRequest(new NetworkCapabilities(nc),
+ TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION);
+ final NetworkRequest requestNR = new NetworkRequest(new NetworkCapabilities(nc),
+ TYPE_NONE, 42 /* rId */, NetworkRequest.Type.REQUEST);
+
+ assertTrue(reservationNR.canBeSatisfiedBy(blanketOffer));
+ assertTrue(reservationNR.canBeSatisfiedBy(specificOffer));
+ assertFalse(reservationNR.canBeSatisfiedBy(otherSpecificOffer));
+ assertFalse(reservationNR.canBeSatisfiedBy(regularOffer));
+
+ assertFalse(requestNR.canBeSatisfiedBy(blanketOffer));
+ assertTrue(requestNR.canBeSatisfiedBy(specificOffer));
+ assertTrue(requestNR.canBeSatisfiedBy(otherSpecificOffer));
+ assertTrue(requestNR.canBeSatisfiedBy(regularOffer));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testNetworkRequest_throwsWhenPassingCapsWithReservationId() {
+ final NetworkCapabilities capsWithResId = new NetworkCapabilities();
+ capsWithResId.setReservationId(42);
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ new NetworkRequest(capsWithResId, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.REQUEST);
+ });
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
new file mode 100644
index 0000000..f43b927
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2025 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
+
+import android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.bluetooth.BluetoothManager
+import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
+import android.net.ConnectivityManager
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.platform.test.annotations.AppModeFull
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkOfferCallback
+import com.android.testutils.pollingCheck
+import com.android.testutils.runAsShell
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TAG = "NetworkReservationTest"
+
+private val NETWORK_SCORE = NetworkScore.Builder().build()
+private val ETHERNET_CAPS = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+ reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private const val TIMEOUT_MS = 5_000L
+private const val NO_CB_TIMEOUT_MS = 200L
+
+// TODO: integrate with CSNetworkReservationTest and move to common tests.
+@AppModeFull(reason = "CHANGE_NETWORK_STATE, MANAGE_TEST_NETWORKS not grantable to instant apps")
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class NetworkReservationTest {
+ private val context = InstrumentationRegistry.getInstrumentation().context
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+ private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+ private val provider = NetworkProvider(context, handlerThread.looper, TAG)
+
+ private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
+ private val bm = context.getSystemService(BluetoothManager::class.java)!!
+ private var disableBluetoothInTearDown = false
+
+ @Before
+ fun setUp() {
+ runAsShell(NETWORK_SETTINGS) {
+ cm.registerNetworkProvider(provider)
+ }
+ }
+
+ private fun enableBluetooth() {
+ val adapter = bm.adapter
+ assertNotNull(adapter)
+ if (adapter.isEnabled()) return
+
+ runShellCommandOrThrow("svc bluetooth enable")
+ val bluetoothEnabled = pollingCheck(TIMEOUT_MS) {
+ adapter.isEnabled()
+ }
+ assertTrue(bluetoothEnabled)
+ // Only disable Bluetooth in tear down when it hasn't already been enabled.
+ disableBluetoothInTearDown = true
+ }
+
+ private fun disableBluetooth() {
+ // adapter can't actually be null here, because this function does not run unless
+ // disableBluetoothInTearDown is true. Just in case, refrain from throwing an exception in
+ // tearDown.
+ val adapter = bm.adapter
+ if (adapter == null) return
+
+ runShellCommandOrThrow("svc bluetooth disable")
+ // Wait for #isEnabled() to return false; ignore failures.
+ pollingCheck(TIMEOUT_MS) {
+ !adapter.isEnabled()
+ }
+ }
+
+ @After
+ fun tearDown() {
+ registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
+ runAsShell(NETWORK_SETTINGS) {
+ // unregisterNetworkProvider unregisters all associated NetworkOffers.
+ cm.unregisterNetworkProvider(provider)
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+
+ if (disableBluetoothInTearDown) {
+ disableBluetooth()
+ }
+ }
+
+ fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
+ it.reservationId = resId
+ }
+
+ fun reserveNetwork(nr: NetworkRequest): TestableNetworkCallback {
+ return TestableNetworkCallback().also {
+ cm.reserveNetwork(nr, handler, it)
+ registeredCallbacks.add(it)
+ }
+ }
+
+ @Test
+ fun testReserveNetwork() {
+ // register blanket offer
+ val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ provider.registerNetworkOffer(NETWORK_SCORE, BLANKET_CAPS, handler::post, blanketOffer)
+ }
+
+ val cb = reserveNetwork(ETHERNET_REQUEST)
+
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ // bring up reserved reservation offer
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ provider.registerNetworkOffer(NETWORK_SCORE, reservedCaps, handler::post, reservedOffer)
+ }
+
+ // validate onReserved was sent to the app
+ val appObservedCaps = cb.expect<Reserved>().caps
+ assertEquals(reservedCaps, appObservedCaps)
+
+ // validate the reservation matches the reserved offer.
+ reservedOffer.expectOnNetworkNeeded(reservedCaps)
+
+ // reserved offer goes away
+ provider.unregisterNetworkOffer(reservedOffer)
+ cb.expect<Unavailable>()
+ }
+
+ @Test
+ fun testReserveL2capNetwork() {
+ assumeTrue(context.packageManager.hasSystemFeature(FEATURE_BLUETOOTH_LE))
+ enableBluetooth()
+
+ val l2capReservationSpecifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ val l2capRequest = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .setNetworkSpecifier(l2capReservationSpecifier)
+ .build()
+ val cb = runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
+ reserveNetwork(l2capRequest)
+ }
+
+ val caps = cb.expect<Reserved>().caps
+ val reservedSpec = caps.networkSpecifier
+ assertTrue(reservedSpec is L2capNetworkSpecifier)
+ assertContains(0x80..0xFF, reservedSpec.psm, "PSM is outside of dynamic range")
+ assertEquals(HEADER_COMPRESSION_6LOWPAN, reservedSpec.headerCompression)
+ assertNull(reservedSpec.remoteAddress)
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
index 1a48983..65daf57 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 | NoSuchMethodError 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 fef085d..eb2dbf7 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -63,6 +63,7 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.platform.test.annotations.AppModeFull;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -74,6 +75,7 @@
import com.android.compatibility.common.util.SystemUtil;
import com.android.modules.utils.build.SdkLevel;
import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
import com.android.testutils.ConnectivityModuleTest;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -94,8 +96,10 @@
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -113,8 +117,8 @@
private static final String LOG_TAG = "NetworkStatsManagerTest";
- private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} {1} {2}";
- private static final String APPOPS_GET_SHELL_COMMAND = "appops get {0} {1}";
+ private static final String APPOPS_SET_SHELL_COMMAND = "appops set --user {0} {1} {2} {3}";
+ private static final String APPOPS_GET_SHELL_COMMAND = "appops get --user {0} {1} {2}";
private static final long MINUTE = 1000 * 60;
private static final int TIMEOUT_MILLIS = 15000;
@@ -329,12 +333,14 @@
}
private void setAppOpsMode(String appop, String mode) throws Exception {
- final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND, mPkg, appop, mode);
+ final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND,
+ UserHandle.myUserId(), mPkg, appop, mode);
SystemUtil.runShellCommand(mInstrumentation, command);
}
private String getAppOpsMode(String appop) throws Exception {
- final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND, mPkg, appop);
+ final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND,
+ UserHandle.myUserId(), mPkg, appop);
String result = SystemUtil.runShellCommand(mInstrumentation, command);
if (result == null) {
Log.w(LOG_TAG, "App op " + appop + " could not be read.");
@@ -708,27 +714,57 @@
}
}
- class QueryResult {
- public final int tag;
- public final int state;
- public final long total;
+ class QueryResults {
+ private static class QueryKey {
+ private final int mTag;
+ private final int mState;
- QueryResult(int tag, int state, NetworkStats stats) {
- this.tag = tag;
- this.state = state;
- total = getTotalAndAssertNotEmpty(stats, tag, state);
+ QueryKey(int tag, int state) {
+ this.mTag = tag;
+ this.mState = state;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof QueryKey)) return false;
+
+ QueryKey queryKey = (QueryKey) o;
+ return mTag == queryKey.mTag && mState == queryKey.mState;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTag, mState);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("QueryKey(tag=%s, state=%s)", tagToString(mTag),
+ stateToString(mState));
+ }
}
- public String toString() {
- return String.format("QueryResult(tag=%s state=%s total=%d)",
- tagToString(tag), stateToString(state), total);
+ private final HashMap<QueryKey, Long> mSnapshot = new HashMap<>();
+
+ public long get(int tag, int state) {
+ // Expect all results are stored before access.
+ return Objects.requireNonNull(mSnapshot.get(new QueryKey(tag, state)));
+ }
+
+ public void put(int tag, int state, long total) {
+ mSnapshot.put(new QueryKey(tag, state), total);
}
}
- private NetworkStats getNetworkStatsForTagState(int i, int tag, int state) {
- return mNsm.queryDetailsForUidTagState(
+ private long getTotalForTagState(int i, int tag, int state, boolean assertNotEmpty,
+ long startTime, long endTime) {
+ final NetworkStats stats = mNsm.queryDetailsForUidTagState(
mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i),
- mStartTime, mEndTime, Process.myUid(), tag, state);
+ startTime, endTime, Process.myUid(), tag, state);
+ final long total = getTotal(stats, tag, state, assertNotEmpty, startTime, endTime);
+ stats.close();
+ return total;
}
private void assertWithinPercentage(String msg, long expected, long actual, int percentage) {
@@ -739,92 +775,103 @@
assertTrue(msg, upperBound >= actual);
}
- private void assertAlmostNoUnexpectedTraffic(NetworkStats result, int expectedTag,
+ private void assertAlmostNoUnexpectedTraffic(long total, int expectedTag,
int expectedState, long maxUnexpected) {
- long total = 0;
- NetworkStats.Bucket bucket = new NetworkStats.Bucket();
- while (result.hasNextBucket()) {
- assertTrue(result.getNextBucket(bucket));
- total += bucket.getRxBytes() + bucket.getTxBytes();
- }
if (total <= maxUnexpected) return;
- fail(String.format("More than %d bytes of traffic when querying for "
- + "tag %s state %s. Last bucket: uid=%d tag=%s state=%s bytes=%d/%d",
- maxUnexpected, tagToString(expectedTag), stateToString(expectedState),
- bucket.getUid(), tagToString(bucket.getTag()), stateToString(bucket.getState()),
- bucket.getRxBytes(), bucket.getTxBytes()));
+ fail(String.format("More than %d bytes of traffic when querying for tag %s state %s.",
+ maxUnexpected, tagToString(expectedTag), stateToString(expectedState)));
}
+ @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
@Test
public void testUidTagStateDetails() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
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 {
- int currentState = isInForeground() ? STATE_FOREGROUND : STATE_DEFAULT;
- int otherState = (currentState == STATE_DEFAULT) ? STATE_FOREGROUND : STATE_DEFAULT;
- int[] tagsWithTraffic = {NETWORK_TAG, TAG_NONE};
- int[] statesWithTraffic = {currentState, STATE_ALL};
- ArrayList<QueryResult> resultsWithTraffic = new ArrayList<>();
+ int currentState = isInForeground() ? STATE_FOREGROUND : STATE_DEFAULT;
+ int otherState = (currentState == STATE_DEFAULT) ? STATE_FOREGROUND : STATE_DEFAULT;
- int[] statesWithNoTraffic = {otherState};
- int[] tagsWithNoTraffic = {NETWORK_TAG + 1};
- ArrayList<QueryResult> resultsWithNoTraffic = new ArrayList<>();
+ final List<Integer> statesWithTraffic = List.of(currentState, STATE_ALL);
+ final List<Integer> statesWithNoTraffic = List.of(otherState);
+ final ArrayList<Integer> allStates = new ArrayList<>();
+ allStates.addAll(statesWithTraffic);
+ allStates.addAll(statesWithNoTraffic);
- // Expect to see traffic when querying for any combination of a tag in
- // tagsWithTraffic and a state in statesWithTraffic.
- for (int tag : tagsWithTraffic) {
- for (int state : statesWithTraffic) {
- result = getNetworkStatsForTagState(i, tag, state);
- resultsWithTraffic.add(new QueryResult(tag, state, result));
- result.close();
- result = null;
+ final List<Integer> tagsWithTraffic = List.of(NETWORK_TAG, TAG_NONE);
+ final List<Integer> tagsWithNoTraffic = List.of(NETWORK_TAG + 1);
+ final ArrayList<Integer> allTags = new ArrayList<>();
+ allTags.addAll(tagsWithTraffic);
+ allTags.addAll(tagsWithNoTraffic);
+
+ // Relatively large tolerance to accommodate for history bucket size,
+ // and covering the entire test duration.
+ final long now = System.currentTimeMillis();
+ final long startTime = now - LONG_TOLERANCE;
+ final long endTime = now + LONG_TOLERANCE;
+
+ // Collect a baseline before generating network traffic.
+ QueryResults baseline = new QueryResults();
+ final ArrayList<String> logNonEmptyBaseline = new ArrayList<>();
+ for (int tag : allTags) {
+ for (int state : allStates) {
+ final long total = getTotalForTagState(i, tag, state, false,
+ startTime, endTime);
+ baseline.put(tag, state, total);
+ if (total > 0) {
+ logNonEmptyBaseline.add(
+ new QueryResults.QueryKey(tag, state) + "=" + total);
}
}
-
- // Expect that the results are within a few percentage points of each other.
- // This is ensures that FIN retransmits after the transfer is complete don't cause
- // the test to be flaky. The test URL currently returns just over 100k so this
- // should not be too noisy. It also ensures that the traffic sent by the test
- // harness, which is untagged, won't cause a failure.
- long firstTotal = resultsWithTraffic.get(0).total;
- for (QueryResult queryResult : resultsWithTraffic) {
- assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 16);
- }
-
- // Expect to see no traffic when querying for any tag in tagsWithNoTraffic or any
- // state in statesWithNoTraffic.
- for (int tag : tagsWithNoTraffic) {
- for (int state : statesWithTraffic) {
- result = getNetworkStatsForTagState(i, tag, state);
- assertAlmostNoUnexpectedTraffic(result, tag, state, firstTotal / 100);
- result.close();
- result = null;
- }
- }
- for (int tag : tagsWithTraffic) {
- for (int state : statesWithNoTraffic) {
- result = getNetworkStatsForTagState(i, tag, state);
- assertAlmostNoUnexpectedTraffic(result, tag, state, firstTotal / 100);
- result.close();
- result = null;
- }
- }
- } finally {
- if (result != null) {
- result.close();
- }
}
+ // TODO: Remove debug log for b/368624224.
+ if (logNonEmptyBaseline.size() > 0) {
+ Log.v(LOG_TAG, "Baseline=" + logNonEmptyBaseline);
+ }
+
+ // Generate some traffic and release the network.
+ requestNetworkAndGenerateTraffic(i, LONG_TOLERANCE);
+
+ QueryResults results = new QueryResults();
+ // Collect results for all combinations of tags and states.
+ for (int tag : allTags) {
+ for (int state : allStates) {
+ final boolean assertNotEmpty = tagsWithTraffic.contains(tag)
+ && statesWithTraffic.contains(state);
+ final long total = getTotalForTagState(i, tag, state, assertNotEmpty,
+ startTime, endTime) - baseline.get(tag, state);
+ results.put(tag, state, total);
+ }
+ }
+
+ // Expect that the results are within a few percentage points of each other.
+ // This is ensures that FIN retransmits after the transfer is complete don't cause
+ // the test to be flaky. The test URL currently returns just over 100k so this
+ // should not be too noisy. It also ensures that the traffic sent by the test
+ // harness, which is untagged, won't cause a failure.
+ long totalOfNetworkTagAndCurrentState = results.get(NETWORK_TAG, currentState);
+ for (int tag : allTags) {
+ for (int state : allStates) {
+ final long result = results.get(tag, state);
+ final String queryKeyStr = new QueryResults.QueryKey(tag, state).toString();
+ if (tagsWithTraffic.contains(tag) && statesWithTraffic.contains(state)) {
+ assertWithinPercentage(queryKeyStr,
+ totalOfNetworkTagAndCurrentState, result, 16);
+ } else {
+ // Expect to see no traffic when querying for any combination with tag
+ // in tagsWithNoTraffic or any state in statesWithNoTraffic.
+ assertAlmostNoUnexpectedTraffic(result, tag, state,
+ totalOfNetworkTagAndCurrentState / 100);
+ }
+ }
+ }
+
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny");
try {
- result = mNsm.queryDetailsForUidTag(
+ mNsm.queryDetailsForUidTag(
mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i),
mStartTime, mEndTime, Process.myUid(), NETWORK_TAG);
fail("negative testUidDetails fails: no exception thrown.");
@@ -897,7 +944,7 @@
}
}
- private String tagToString(Integer tag) {
+ private static String tagToString(Integer tag) {
if (tag == null) return "null";
switch (tag) {
case TAG_NONE:
@@ -907,7 +954,7 @@
}
}
- private String stateToString(Integer state) {
+ private static String stateToString(Integer state) {
if (state == null) return "null";
switch (state) {
case STATE_ALL:
@@ -920,8 +967,8 @@
throw new IllegalArgumentException("Unknown state " + state);
}
- private long getTotalAndAssertNotEmpty(NetworkStats result, Integer expectedTag,
- Integer expectedState) {
+ private long getTotal(NetworkStats result, Integer expectedTag,
+ Integer expectedState, boolean assertNotEmpty, long startTime, long endTime) {
assertTrue(result != null);
NetworkStats.Bucket bucket = new NetworkStats.Bucket();
long totalTxPackets = 0;
@@ -930,7 +977,7 @@
long totalRxBytes = 0;
while (result.hasNextBucket()) {
assertTrue(result.getNextBucket(bucket));
- assertTimestamps(bucket);
+ assertTimestamps(bucket, startTime, endTime);
if (expectedTag != null) assertEquals(bucket.getTag(), (int) expectedTag);
if (expectedState != null) assertEquals(bucket.getState(), (int) expectedState);
assertEquals(bucket.getMetered(), METERED_ALL);
@@ -946,23 +993,29 @@
assertFalse(result.getNextBucket(bucket));
String msg = String.format("uid %d tag %s state %s",
Process.myUid(), tagToString(expectedTag), stateToString(expectedState));
- assertTrue("No Rx bytes usage for " + msg, totalRxBytes > 0);
- assertTrue("No Rx packets usage for " + msg, totalRxPackets > 0);
- assertTrue("No Tx bytes usage for " + msg, totalTxBytes > 0);
- assertTrue("No Tx packets usage for " + msg, totalTxPackets > 0);
+ if (assertNotEmpty) {
+ assertTrue("No Rx bytes usage for " + msg, totalRxBytes > 0);
+ assertTrue("No Rx packets usage for " + msg, totalRxPackets > 0);
+ assertTrue("No Tx bytes usage for " + msg, totalTxBytes > 0);
+ assertTrue("No Tx packets usage for " + msg, totalTxPackets > 0);
+ }
return totalRxBytes + totalTxBytes;
}
private long getTotalAndAssertNotEmpty(NetworkStats result) {
- return getTotalAndAssertNotEmpty(result, null, STATE_ALL);
+ return getTotal(result, null, STATE_ALL, true /*assertEmpty*/, mStartTime, mEndTime);
}
private void assertTimestamps(final NetworkStats.Bucket bucket) {
+ assertTimestamps(bucket, mStartTime, mEndTime);
+ }
+
+ private void assertTimestamps(final NetworkStats.Bucket bucket, long startTime, long endTime) {
assertTrue("Start timestamp " + bucket.getStartTimeStamp() + " is less than "
- + mStartTime, bucket.getStartTimeStamp() >= mStartTime);
+ + startTime, bucket.getStartTimeStamp() >= startTime);
assertTrue("End timestamp " + bucket.getEndTimeStamp() + " is greater than "
- + mEndTime, bucket.getEndTimeStamp() <= mEndTime);
+ + endTime, bucket.getEndTimeStamp() <= endTime);
}
private static class TestUsageCallback extends NetworkStatsManager.UsageCallback {
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 24af42b..1973899 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -92,7 +92,7 @@
assertEquals(downstreamIface.name, iface)
val request = TetheringRequest.Builder(TETHERING_ETHERNET)
.setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
- tetheringEventCallback = enableEthernetTethering(
+ tetheringEventCallback = enableTethering(
iface, request,
null /* any upstream */
).apply {
@@ -125,7 +125,7 @@
val request = TetheringRequest.Builder(TETHERING_ETHERNET)
.setStaticIpv4Addresses(localAddr, clientAddr)
.setShouldShowEntitlementUi(false).build()
- tetheringEventCallback = enableEthernetTethering(
+ tetheringEventCallback = enableTethering(
iface, request,
null /* any upstream */
).apply {
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index ad6fe63..ee31f1a 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -22,14 +22,10 @@
import android.net.ConnectivityManager.NetworkCallback
import android.net.DnsResolver
import android.net.InetAddresses.parseNumericAddress
-import android.net.LinkAddress
-import android.net.LinkProperties
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.net.MacAddress
import android.net.Network
-import android.net.NetworkAgentConfig
-import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
@@ -40,8 +36,11 @@
import android.net.TestNetworkSpecifier
import android.net.connectivity.ConnectivityCompatChanges
import android.net.cts.util.CtsNetUtils
+import android.net.nsd.AdvertisingRequest
+import android.net.nsd.AdvertisingRequest.FLAG_SKIP_PROBING
import android.net.nsd.DiscoveryRequest
import android.net.nsd.NsdManager
+import android.net.nsd.NsdManager.PROTOCOL_DNS_SD
import android.net.nsd.NsdServiceInfo
import android.net.nsd.OffloadEngine
import android.net.nsd.OffloadServiceInfo
@@ -50,16 +49,10 @@
import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig.NAMESPACE_TETHERING
-import android.system.ErrnoException
-import android.system.Os
-import android.system.OsConstants.AF_INET6
-import android.system.OsConstants.EADDRNOTAVAIL
-import android.system.OsConstants.ENETUNREACH
import android.system.OsConstants.ETH_P_IPV6
import android.system.OsConstants.IPPROTO_IPV6
import android.system.OsConstants.IPPROTO_UDP
import android.system.OsConstants.RT_SCOPE_LINK
-import android.system.OsConstants.SOCK_DGRAM
import android.util.Log
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
@@ -98,12 +91,11 @@
import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
+import com.android.testutils.PollPacketReader
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
-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.assertEmpty
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
@@ -244,16 +236,12 @@
val tnm = context.getSystemService(TestNetworkManager::class.java)!!
val iface = tnm.createTapInterface()
val cb = TestableNetworkCallback()
- val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
cm.requestNetwork(
- NetworkRequest.Builder()
- .removeCapability(NET_CAPABILITY_TRUSTED)
- .addTransportType(TRANSPORT_TEST)
- .setNetworkSpecifier(testNetworkSpecifier)
- .build(),
+ TestableNetworkAgent.makeNetworkRequestForInterface(iface.interfaceName),
cb
)
- val agent = registerTestNetworkAgent(iface.interfaceName)
+ val agent = TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ iface.interfaceName, TIMEOUT_MS)
val network = agent.network ?: fail("Registered agent should have a network")
cb.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
@@ -268,57 +256,6 @@
return TestTapNetwork(iface, cb, agent, network)
}
- private fun registerTestNetworkAgent(ifaceName: String): TestableNetworkAgent {
- val lp = LinkProperties().apply {
- interfaceName = ifaceName
- }
- val agent = TestableNetworkAgent(
- context,
- handlerThread.looper,
- NetworkCapabilities().apply {
- removeCapability(NET_CAPABILITY_TRUSTED)
- addTransportType(TRANSPORT_TEST)
- setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
- },
- lp,
- NetworkAgentConfig.Builder().build()
- )
- val network = agent.register()
- agent.markConnected()
- agent.expectCallback<OnNetworkCreated>()
-
- // Wait until the link-local address can be used. Address flags are not available without
- // elevated permissions, so check that bindSocket works.
- PollingCheck.check("No usable v6 address on interface after $TIMEOUT_MS ms", TIMEOUT_MS) {
- // To avoid race condition between socket connection succeeding and interface returning
- // a non-empty address list. Verify that interface returns a non-empty list, before
- // trying the socket connection.
- if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
- return@check false
- }
-
- val sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
- tryTest {
- network.bindSocket(sock)
- Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
- true
- }.catch<ErrnoException> {
- if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
- throw it
- }
- false
- } cleanup {
- Os.close(sock)
- }
- }
-
- lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
- LinkAddress(it.address, it.networkPrefixLength.toInt())
- })
- agent.sendLinkProperties(lp)
- return agent
- }
-
private fun makeTestServiceInfo(network: Network? = null) = NsdServiceInfo().also {
it.serviceType = serviceType
it.serviceName = serviceName
@@ -326,6 +263,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) {
@@ -564,7 +510,9 @@
assertEquals(testNetwork1.network, serviceLost.serviceInfo.network)
val newAgent = runAsShell(MANAGE_TEST_NETWORKS) {
- registerTestNetworkAgent(testNetwork1.iface.interfaceName)
+ TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ testNetwork1.iface.interfaceName,
+ TIMEOUT_MS)
}
val newNetwork = newAgent.network ?: fail("Registered agent should have a network")
val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>()
@@ -1298,14 +1246,7 @@
assumeTrue(TestUtils.shouldTestTApis())
val si = makeTestServiceInfo(testNetwork1.network)
-
- val packetReader = PollPacketReader(
- 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()
@@ -1345,13 +1286,7 @@
parseNumericAddress("2001:db8::3"))
}
- val packetReader = PollPacketReader(
- 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()
@@ -1391,13 +1326,7 @@
hostname = customHostname
}
- val packetReader = PollPacketReader(
- 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()
@@ -1438,13 +1367,7 @@
val registrationRecord = NsdRegistrationRecord()
val discoveryRecord = NsdDiscoveryRecord()
val registeredService = registerService(registrationRecord, si)
- val packetReader = PollPacketReader(
- Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor,
- 1500 /* maxPacketSize */
- )
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
tryTest {
assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
@@ -1518,13 +1441,7 @@
val registrationRecord = NsdRegistrationRecord()
val discoveryRecord = NsdDiscoveryRecord()
val registeredService = registerService(registrationRecord, si)
- val packetReader = PollPacketReader(
- Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor,
- 1500 /* maxPacketSize */
- )
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
tryTest {
assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
@@ -1587,13 +1504,7 @@
val registrationRecord = NsdRegistrationRecord()
val discoveryRecord = NsdDiscoveryRecord()
val registeredService = registerService(registrationRecord, si)
- val packetReader = PollPacketReader(
- Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor,
- 1500 /* maxPacketSize */
- )
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
tryTest {
assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
@@ -1630,13 +1541,7 @@
fun testDiscoveryWithPtrOnlyResponse_ServiceIsFound() {
// Register service on testNetwork1
val discoveryRecord = NsdDiscoveryRecord()
- val packetReader = PollPacketReader(
- Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor,
- 1500 /* maxPacketSize */
- )
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
nsdManager.discoverServices(
serviceType,
@@ -1675,9 +1580,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)
}
}
@@ -1688,79 +1596,77 @@
fun testResolveWhenServerSendsNoAdditionalRecord() {
// Resolve service on testNetwork1
val resolveRecord = NsdResolveRecord()
- val packetReader = PollPacketReader(
- 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
@@ -1774,13 +1680,9 @@
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
var nsResponder: NSResponder? = null
+ val packetReader = makePacketReader()
tryTest {
registerService(registrationRecord, si)
- val packetReader = PollPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
-
- handlerThread.waitForIdle(TIMEOUT_MS)
/*
Send a "query unicast" query.
Generated with:
@@ -1805,10 +1707,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)
}
}
@@ -1824,13 +1729,9 @@
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
var nsResponder: NSResponder? = null
+ val packetReader = makePacketReader()
tryTest {
registerService(registrationRecord, si)
- val packetReader = PollPacketReader(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.
@@ -1895,10 +1796,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)
}
}
@@ -1914,13 +1818,9 @@
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
var nsResponder: NSResponder? = null
+ val packetReader = makePacketReader()
tryTest {
registerService(registrationRecord, si)
- val packetReader = PollPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
- packetReader.startAsyncForTest()
-
- handlerThread.waitForIdle(TIMEOUT_MS)
/*
Send a query with truncated bit set.
Generated with:
@@ -1976,10 +1876,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)
}
}
@@ -1991,13 +1894,7 @@
// Register service on testNetwork1
val discoveryRecord = NsdDiscoveryRecord()
- val packetReader = PollPacketReader(
- Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor,
- 1500 /* maxPacketSize */
- )
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ val packetReader = makePacketReader()
nsdManager.discoverServices(
serviceType,
@@ -2043,9 +1940,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)
}
}
@@ -2355,14 +2255,7 @@
it.port = TEST_PORT
it.publicKey = publicKey
}
- val packetReader = PollPacketReader(
- 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 {
@@ -2394,8 +2287,11 @@
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<DiscoveryStopped>()
- } cleanup {
+ } cleanupStep {
nsdManager.unregisterService(registrationRecord)
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
}
@@ -2410,14 +2306,7 @@
parseNumericAddress("2001:db8::2"))
it.publicKey = publicKey
}
- val packetReader = PollPacketReader(
- 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)
@@ -2439,8 +2328,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)
}
}
@@ -2467,14 +2359,7 @@
it.hostAddresses = listOf()
it.publicKey = publicKey
}
- val packetReader = PollPacketReader(
- 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 {
@@ -2508,9 +2393,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)
}
}
@@ -2582,13 +2470,7 @@
"test_nsd_avoid_advertising_empty_txt_records",
"1"
)
- val packetReader = PollPacketReader(
- Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor,
- 1500 /* maxPacketSize */
- )
- packetReader.startAsyncForTest()
- handlerThread.waitForIdle(TIMEOUT_MS)
+ 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.
@@ -2607,9 +2489,125 @@
assertEquals(1, txtRecords.size)
// The TXT record should contain as single zero
assertContentEquals(byteArrayOf(0), txtRecords[0].rr)
- } cleanup {
+ } 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)
+ }
+
+ @Test
+ fun testSkipProbing() {
+ val si = makeTestServiceInfo(testNetwork1.network)
+ val request = AdvertisingRequest.Builder(si)
+ .setFlags(FLAG_SKIP_PROBING)
+ .build()
+ assertEquals(FLAG_SKIP_PROBING, request.flags)
+ assertEquals(PROTOCOL_DNS_SD, request.protocolType)
+ assertEquals(si.serviceName, request.serviceInfo.serviceName)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ nsdManager.registerService(request, { it.run() }, registrationRecord)
+ registrationRecord.expectCallback<ServiceRegistered>()
+ val packetReader = makePacketReader()
+
+ tryTest {
+ val srvRecordName = "$serviceName.$serviceType.local"
+ // Look for either announcements or probes
+ val packet = packetReader.pollForMdnsPacket {
+ it.isProbeFor(srvRecordName) || it.isReplyFor(srvRecordName)
+ }
+ assertNotNull(packet, "Probe or announcement not received within timeout")
+ // The first packet should be an announcement, not a probe.
+ assertTrue("Found initial probes with NSD_ADVERTISING_SKIP_PROBING enabled",
+ packet.isReplyFor(srvRecordName))
+
+ // Force a conflict now that the service is getting announced
+ val conflictingAnnouncement = buildConflictingAnnouncement()
+ packetReader.sendResponse(conflictingAnnouncement)
+
+ // Expect to see probes now (RFC6762 9., service is reset to probing state)
+ assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+ "Probe not received within timeout after conflict")
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
}
}
diff --git a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
index bd9e03c..f5198e3 100755
--- a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
+++ b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java
@@ -24,6 +24,8 @@
import android.util.Log;
import android.util.Range;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -94,11 +96,12 @@
long tcpPacketToIpBytes(long packetCount, long bytes) {
// ip header + tcp header + data.
- // Tcp header is mostly 32. Syn has different tcp options -> 40. Don't care.
+ // Tcp header is mostly 32. Syn has different tcp options -> 40.
return packetCount * (20 + 32 + bytes);
}
@AppModeFull(reason = "Socket cannot bind in instant app mode")
+ @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
public void testTrafficStatsForLocalhost() throws IOException {
final long mobileTxPacketsBefore = TrafficStats.getMobileTxPackets();
final long mobileRxPacketsBefore = TrafficStats.getMobileRxPackets();
@@ -224,9 +227,15 @@
- uidTxDeltaPackets;
final long deltaRxOtherPackets = (totalRxPacketsAfter - totalRxPacketsBefore)
- uidRxDeltaPackets;
- if (deltaTxOtherPackets > 0 || deltaRxOtherPackets > 0) {
+ final long deltaTxOtherPktBytes = (totalTxBytesAfter - totalTxBytesBefore)
+ - uidTxDeltaBytes;
+ final long deltaRxOtherPktBytes = (totalRxBytesAfter - totalRxBytesBefore)
+ - uidRxDeltaBytes;
+ if (deltaTxOtherPackets != 0 || deltaRxOtherPackets != 0
+ || deltaTxOtherPktBytes != 0 || deltaRxOtherPktBytes != 0) {
Log.i(LOG_TAG, "lingering traffic data: " + deltaTxOtherPackets + "/"
- + deltaRxOtherPackets);
+ + deltaRxOtherPackets + "/" + deltaTxOtherPktBytes
+ + "/" + deltaRxOtherPktBytes);
}
// Check that the per-uid stats obtained from data profiling contain the expected values.
@@ -237,9 +246,9 @@
final long pktBytes = tcpPacketToIpBytes(packetCount, byteCount);
final long pktWithNoDataBytes = tcpPacketToIpBytes(packetCount, 0);
final long minExpExtraPktBytes = tcpPacketToIpBytes(minExpectedExtraPackets, 0);
- final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 0);
- final long deltaTxOtherPktBytes = tcpPacketToIpBytes(deltaTxOtherPackets, 0);
- final long deltaRxOtherPktBytes = tcpPacketToIpBytes(deltaRxOtherPackets, 0);
+ // Syn/syn-ack has different tcp options, make tcp header 40 for upper bound estimation.
+ final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 8);
+
assertInRange("txPackets detail", entry.txPackets, packetCount + minExpectedExtraPackets,
uidTxDeltaPackets);
assertInRange("rxPackets detail", entry.rxPackets, packetCount + minExpectedExtraPackets,
@@ -257,32 +266,24 @@
assertInRange("uidrxb", uidRxDeltaBytes, pktBytes + minExpExtraPktBytes,
pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes);
assertInRange("iftxp", ifaceTxDeltaPackets, packetCount + minExpectedExtraPackets,
- packetCount + packetCount + maxExpectedExtraPackets);
+ packetCount + packetCount + maxExpectedExtraPackets + deltaTxOtherPackets);
assertInRange("ifrxp", ifaceRxDeltaPackets, packetCount + minExpectedExtraPackets,
- packetCount + packetCount + maxExpectedExtraPackets);
+ packetCount + packetCount + maxExpectedExtraPackets + deltaRxOtherPackets);
assertInRange("iftxb", ifaceTxDeltaBytes, pktBytes + minExpExtraPktBytes,
- pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes);
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaTxOtherPktBytes);
assertInRange("ifrxb", ifaceRxDeltaBytes, pktBytes + minExpExtraPktBytes,
- pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes);
+ pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes);
// Localhost traffic *does* count against total stats.
// Check the total stats increased after test data transfer over localhost has been made.
- assertTrue("ttxp: " + totalTxPacketsBefore + " -> " + totalTxPacketsAfter,
- totalTxPacketsAfter >= totalTxPacketsBefore + uidTxDeltaPackets);
- assertTrue("trxp: " + totalRxPacketsBefore + " -> " + totalRxPacketsAfter,
- totalRxPacketsAfter >= totalRxPacketsBefore + uidRxDeltaPackets);
- assertTrue("ttxb: " + totalTxBytesBefore + " -> " + totalTxBytesAfter,
- totalTxBytesAfter >= totalTxBytesBefore + uidTxDeltaBytes);
- assertTrue("trxb: " + totalRxBytesBefore + " -> " + totalRxBytesAfter,
- totalRxBytesAfter >= totalRxBytesBefore + uidRxDeltaBytes);
- assertTrue("iftxp: " + ifaceTxPacketsBefore + " -> " + ifaceTxPacketsAfter,
- totalTxPacketsAfter >= totalTxPacketsBefore + ifaceTxDeltaPackets);
- assertTrue("ifrxp: " + ifaceRxPacketsBefore + " -> " + ifaceRxPacketsAfter,
- totalRxPacketsAfter >= totalRxPacketsBefore + ifaceRxDeltaPackets);
- assertTrue("iftxb: " + ifaceTxBytesBefore + " -> " + ifaceTxBytesAfter,
- totalTxBytesAfter >= totalTxBytesBefore + ifaceTxDeltaBytes);
- assertTrue("ifrxb: " + ifaceRxBytesBefore + " -> " + ifaceRxBytesAfter,
- totalRxBytesAfter >= totalRxBytesBefore + ifaceRxDeltaBytes);
+ assertInRange("ttxp", totalTxPacketsAfter,
+ totalTxPacketsBefore + packetCount + minExpectedExtraPackets, Long.MAX_VALUE);
+ assertInRange("trxp", totalRxPacketsAfter,
+ totalRxPacketsBefore + packetCount + minExpectedExtraPackets, Long.MAX_VALUE);
+ assertInRange("ttxb", totalTxBytesAfter,
+ totalTxBytesBefore + pktBytes + minExpExtraPktBytes, Long.MAX_VALUE);
+ assertInRange("trxb", totalRxBytesAfter,
+ totalRxBytesBefore + pktBytes + minExpExtraPktBytes, Long.MAX_VALUE);
// Localhost traffic should *not* count against mobile stats,
// There might be some other traffic, but nowhere near 1MB.
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 0dd2a23..173d13f 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -53,6 +53,7 @@
import android.os.Build;
import android.os.ConditionVariable;
import android.os.IBinder;
+import android.os.UserHandle;
import android.system.Os;
import android.system.OsConstants;
import android.telephony.SubscriptionManager;
@@ -145,7 +146,8 @@
for (final String pkg : new String[] {"com.android.shell", mContext.getPackageName()}) {
final String cmd =
String.format(
- "appops set %s %s %s",
+ "appops set --user %d %s %s %s",
+ UserHandle.myUserId(), // user id
pkg, // Package name
opName, // Appop
(allow ? "allow" : "deny")); // Action
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..75b2814 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
@@ -22,6 +22,7 @@
import static android.Manifest.permission.TETHER_PRIVILEGED;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_REQUEST;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
@@ -46,6 +47,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;
@@ -125,6 +127,67 @@
}
}
+ public static class StopTetheringCallback implements TetheringManager.StopTetheringCallback {
+ private static final int TIMEOUT_MS = 30_000;
+ public static class CallbackValue {
+ public final int error;
+
+ private CallbackValue(final int e) {
+ error = e;
+ }
+
+ public static class OnStopTetheringSucceeded extends CallbackValue {
+ OnStopTetheringSucceeded() {
+ super(TETHER_ERROR_NO_ERROR);
+ }
+ }
+
+ public static class OnStopTetheringFailed extends CallbackValue {
+ OnStopTetheringFailed(final int error) {
+ super(error);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s(%d)", getClass().getSimpleName(), error);
+ }
+ }
+
+ private final ArrayTrackRecord<CallbackValue>.ReadHead mHistory =
+ new ArrayTrackRecord<CallbackValue>().newReadHead();
+
+ @Override
+ public void onStopTetheringSucceeded() {
+ mHistory.add(new CallbackValue.OnStopTetheringSucceeded());
+ }
+
+ @Override
+ public void onStopTetheringFailed(final int error) {
+ mHistory.add(new CallbackValue.OnStopTetheringFailed(error));
+ }
+
+ /**
+ * Verifies that {@link #onStopTetheringSucceeded()} was called
+ */
+ public void verifyStopTetheringSucceeded() {
+ final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+ assertNotNull("No onStopTetheringSucceeded after " + TIMEOUT_MS + " ms", cv);
+ assertTrue("Fail stop tethering:" + cv,
+ cv instanceof CallbackValue.OnStopTetheringSucceeded);
+ }
+
+ /**
+ * Verifies that {@link #onStopTetheringFailed(int)} was called
+ */
+ public void expectStopTetheringFailed(final int expected) {
+ final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+ assertNotNull("No onStopTetheringFailed after " + TIMEOUT_MS + " ms", cv);
+ assertTrue("Expect fail with error code " + expected + ", but received: " + cv,
+ (cv instanceof CallbackValue.OnStopTetheringFailed) && (cv.error == expected));
+ }
+ }
+
private static boolean isRegexMatch(final String[] ifaceRegexs, String iface) {
if (ifaceRegexs == null) fail("ifaceRegexs should not be null");
@@ -491,13 +554,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);
@@ -557,6 +636,22 @@
expectSoftApDisabled();
}
+ /**
+ * Calls {@link TetheringManager#stopTethering(TetheringRequest, Executor,
+ * TetheringManager.StopTetheringCallback)} and verifies if it succeeded or failed.
+ */
+ public void stopTethering(final TetheringRequest request, boolean expectSuccess) {
+ final StopTetheringCallback stopTetheringCallback = new StopTetheringCallback();
+ runAsShell(TETHER_PRIVILEGED, () -> {
+ mTm.stopTethering(request, c -> c.run() /* executor */, stopTetheringCallback);
+ if (expectSuccess) {
+ stopTetheringCallback.verifyStopTetheringSucceeded();
+ } else {
+ stopTetheringCallback.expectStopTetheringFailed(TETHER_ERROR_UNKNOWN_REQUEST);
+ }
+ });
+ }
+
public void stopAllTethering() {
final TestTetheringEventCallback callback = registerTetheringEventCallback();
try {
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
index e0424ac..71d2b6e 100644
--- a/tests/cts/netpermission/internetpermission/Android.bp
+++ b/tests/cts/netpermission/internetpermission/Android.bp
@@ -32,4 +32,7 @@
],
host_required: ["net-tests-utils-host-common"],
sdk_version: "test_current",
+ data: [
+ ":ConnectivityTestPreparer",
+ ],
}
diff --git a/tests/cts/netpermission/internetpermission/AndroidTest.xml b/tests/cts/netpermission/internetpermission/AndroidTest.xml
index ad9a731..13deb82 100644
--- a/tests/cts/netpermission/internetpermission/AndroidTest.xml
+++ b/tests/cts/netpermission/internetpermission/AndroidTest.xml
@@ -20,6 +20,7 @@
<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="parameter" value="secondary_user_on_secondary_display" />
<option name="not-shardable" value="true" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index 689ce74..0ff98e7 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -33,8 +33,10 @@
// Tag this module as a cts test artifact
test_suites: [
+ "automotive-general-tests",
"cts",
"general-tests",
],
+ data: [":ConnectivityTestPreparer"],
host_required: ["net-tests-utils-host-common"],
}
diff --git a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
index fb6c814..82994c4 100644
--- a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
+++ b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
@@ -20,6 +20,7 @@
<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="parameter" value="secondary_user_on_secondary_display" />
<option name="not-shardable" value="true" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index d9bc7f7..a1e0797 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -34,6 +34,7 @@
static_libs: [
"TetheringCommonTests",
+ "com.android.net.flags-aconfig-java",
"compatibility-device-util-axt",
"cts-net-utils",
"net-tests-utils",
@@ -54,34 +55,6 @@
host_required: ["net-tests-utils-host-common"],
}
-// Tethering CTS tests that target the latest released SDK. These tests can be installed on release
-// devices which has equal or lowner sdk version than target sdk and are useful for qualifying
-// mainline modules on release devices.
-android_test {
- name: "CtsTetheringTestLatestSdk",
- defaults: [
- "ConnectivityTestsLatestSdkDefaults",
- "CtsTetheringTestDefaults",
- ],
-
- min_sdk_version: "30",
-
- static_libs: [
- "TetheringIntegrationTestsLatestSdkLib",
- ],
-
- test_suites: [
- "general-tests",
- "mts-tethering",
- ],
-
- test_config_template: "AndroidTestTemplate.xml",
-
- // Include both the 32 and 64 bit versions
- compile_multilib: "both",
- jarjar_rules: ":NetworkStackJarJarRules",
-}
-
// Tethering CTS tests for development and release. These tests always target the platform SDK
// version, and are subject to all the restrictions appropriate to that version. Before SDK
// finalization, these tests have a min_sdk_version of 10000, but they can still be installed on
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index a07c9ea..9e49926 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -17,15 +17,19 @@
import static android.Manifest.permission.MODIFY_PHONE_STATE;
import static android.Manifest.permission.TETHER_PRIVILEGED;
+import static android.Manifest.permission.WRITE_SETTINGS;
import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
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_VIRTUAL;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
@@ -41,6 +45,7 @@
import static org.junit.Assert.assertNotEquals;
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.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
@@ -68,6 +73,7 @@
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiSsid;
+import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.ResultReceiver;
@@ -75,14 +81,18 @@
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.net.flags.Flags;
import com.android.testutils.ParcelUtils;
+import com.android.testutils.com.android.testutils.CarrierConfigRule;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -97,6 +107,8 @@
@RunWith(AndroidJUnit4.class)
public class TetheringManagerTest {
+ @Rule
+ public final CarrierConfigRule mCarrierConfigRule = new CarrierConfigRule();
private Context mContext;
@@ -223,19 +235,23 @@
}
- @Test
- public void testTetheringRequest() {
- SoftApConfiguration softApConfiguration;
+ private SoftApConfiguration createSoftApConfiguration(@NonNull String ssid) {
+ SoftApConfiguration config;
if (SdkLevel.isAtLeastT()) {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setWifiSsid(WifiSsid.fromBytes(
- "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+ config = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(ssid.getBytes(StandardCharsets.UTF_8)))
.build();
} else {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setSsid("This is an SSID!")
+ config = new SoftApConfiguration.Builder()
+ .setSsid(ssid)
.build();
}
+ return config;
+ }
+
+ @Test
+ public void testTetheringRequest() {
+ SoftApConfiguration softApConfiguration = createSoftApConfiguration("SSID");
final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI)
.setSoftApConfiguration(softApConfiguration)
.build();
@@ -244,9 +260,16 @@
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");
@@ -254,6 +277,7 @@
.setStaticIpv4Addresses(localAddr, clientAddr)
.setExemptFromEntitlementCheck(true)
.setShouldShowEntitlementUi(false)
+ .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL)
.build();
int uid = 1000;
String packageName = "package";
@@ -265,31 +289,41 @@
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);
+
+ final String interfaceName = "test_iface";
+ final TetheringRequest tr4 = new TetheringRequest.Builder(TETHERING_VIRTUAL)
+ .setInterfaceName(interfaceName)
+ .setConnectivityScope(CONNECTIVITY_SCOPE_GLOBAL).build();
+ assertEquals(interfaceName, tr4.getInterfaceName());
+ assertEquals(CONNECTIVITY_SCOPE_GLOBAL, tr4.getConnectivityScope());
}
@Test
public void testTetheringRequestSetSoftApConfigurationFailsWhenNotWifi() {
- final SoftApConfiguration softApConfiguration;
- if (SdkLevel.isAtLeastT()) {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setWifiSsid(WifiSsid.fromBytes(
- "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
- .build();
- } else {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setSsid("This is an SSID!")
- .build();
- }
+ final SoftApConfiguration softApConfiguration = createSoftApConfiguration("SSID");
for (int type : List.of(TETHERING_USB, TETHERING_BLUETOOTH, TETHERING_WIFI_P2P,
TETHERING_NCM, TETHERING_ETHERNET)) {
try {
@@ -302,33 +336,40 @@
}
@Test
- public void testTetheringRequestParcelable() {
- final SoftApConfiguration softApConfiguration;
- if (SdkLevel.isAtLeastT()) {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setWifiSsid(WifiSsid.fromBytes(
- "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
- .build();
- } else {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setSsid("This is an SSID!")
- .build();
+ public void testTetheringRequestSetInterfaceNameFailsExceptTetheringVirtual() {
+ for (int type : List.of(TETHERING_USB, TETHERING_BLUETOOTH, TETHERING_NCM,
+ TETHERING_ETHERNET)) {
+ try {
+ new TetheringRequest.Builder(type).setInterfaceName("test_iface");
+ fail("Was able to set interface name 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 withConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+ final TetheringRequest withApConfig = new TetheringRequest.Builder(TETHERING_WIFI)
.setSoftApConfiguration(softApConfiguration)
.setStaticIpv4Addresses(localAddr, clientAddr)
.setExemptFromEntitlementCheck(true)
.setShouldShowEntitlementUi(false).build();
- final TetheringRequest withoutConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+ final TetheringRequest withoutApConfig = 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));
+ assertEquals(withApConfig, ParcelUtils.parcelingRoundTrip(withApConfig));
+ assertEquals(withoutApConfig, ParcelUtils.parcelingRoundTrip(withoutApConfig));
+ assertNotEquals(withApConfig, ParcelUtils.parcelingRoundTrip(withoutApConfig));
+ assertNotEquals(withoutApConfig, ParcelUtils.parcelingRoundTrip(withApConfig));
+
+ final TetheringRequest withIfaceName = new TetheringRequest.Builder(TETHERING_VIRTUAL)
+ .setInterfaceName("test_iface").build();
+ assertEquals(withIfaceName, ParcelUtils.parcelingRoundTrip(withIfaceName));
}
@Test
@@ -340,26 +381,31 @@
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);
- try {
- final int ret = runAsShell(TETHER_PRIVILEGED, () -> mTM.tether(wifiTetheringIface));
- // There is no guarantee that the wifi interface will be available after disabling
- // the hotspot, so don't fail the test if the call to tether() fails.
- if (ret == TETHER_ERROR_NO_ERROR) {
- // If calling #tether successful, there is a callback to tell the result of
- // tethering setup.
- tetherEventCallback.expectErrorOrTethered(
- new TetheringInterface(TETHERING_WIFI, wifiTetheringIface));
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ try {
+ final int ret = runAsShell(TETHER_PRIVILEGED,
+ () -> mTM.tether(wifiTetheringIface));
+ // There is no guarantee that the wifi interface will be available after
+ // disabling the hotspot, so don't fail the test if the call to tether() fails.
+ if (ret == TETHER_ERROR_NO_ERROR) {
+ // If calling #tether successful, there is a callback to tell the result of
+ // tethering setup.
+ tetherEventCallback.expectErrorOrTethered(
+ new TetheringInterface(TETHERING_WIFI, wifiTetheringIface));
+ }
+ } finally {
+ runAsShell(TETHER_PRIVILEGED, () -> mTM.untether(wifiTetheringIface));
}
- } finally {
- runAsShell(TETHER_PRIVILEGED, () -> mTM.untether(wifiTetheringIface));
}
} finally {
mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
@@ -410,11 +456,52 @@
}
@Test
- public void testEnableTetheringPermission() throws Exception {
+ public void testStopTetheringRequest() throws Exception {
+ assumeTrue(isTetheringWithSoftApConfigEnabled());
+ final TestTetheringEventCallback tetherEventCallback =
+ mCtsTetheringUtils.registerTetheringEventCallback();
+ try {
+ tetherEventCallback.assumeWifiTetheringSupported(mContext);
+
+ // stopTethering without any tethering active should fail.
+ TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ mCtsTetheringUtils.stopTethering(request, false /* expectSuccess */);
+
+ // Start wifi tethering
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+ // stopTethering should succeed now that there's a request.
+ mCtsTetheringUtils.stopTethering(request, true /* expectSuccess */);
+ tetherEventCallback.expectNoTetheringActive();
+ } finally {
+ mCtsTetheringUtils.stopAllTethering();
+ mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
+ }
+ }
+
+ private boolean isTetheringWithSoftApConfigEnabled() {
+ return Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM
+ && Flags.tetheringWithSoftApConfig();
+ }
+
+ @Test
+ public void testStartTetheringNoPermission() throws Exception {
final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+
+ // No permission
mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
c -> c.run() /* executor */, startTetheringCallback);
startTetheringCallback.expectTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+
+ // WRITE_SETTINGS not sufficient
+ if (isTetheringWithSoftApConfigEnabled()) {
+ runAsShell(WRITE_SETTINGS, () -> {
+ mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
+ c -> c.run() /* executor */, startTetheringCallback);
+ startTetheringCallback.expectTetheringFailed(
+ TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ });
+ }
}
private class EntitlementResultListener implements OnTetheringEntitlementResultListener {
@@ -479,22 +566,13 @@
// Override carrier config to ignore entitlement check.
final PersistableBundle bundle = new PersistableBundle();
bundle.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
- overrideCarrierConfig(bundle);
+ mCarrierConfigRule.addConfigOverrides(
+ SubscriptionManager.getDefaultSubscriptionId(), bundle);
// Verify that requestLatestTetheringEntitlementResult() can get entitlement
// result TETHER_ERROR_NO_ERROR due to provisioning bypassed.
assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
TETHERING_WIFI, false, c -> c.run(), listener), TETHER_ERROR_NO_ERROR);
-
- // Reset carrier config.
- overrideCarrierConfig(null);
- }
-
- private void overrideCarrierConfig(PersistableBundle bundle) {
- final CarrierConfigManager configManager = (CarrierConfigManager) mContext
- .getSystemService(Context.CARRIER_CONFIG_SERVICE);
- final int subId = SubscriptionManager.getDefaultSubscriptionId();
- runAsShell(MODIFY_PHONE_STATE, () -> configManager.overrideConfig(subId, bundle));
}
private boolean isTetheringApnRequired() {
@@ -555,4 +633,11 @@
}
}
}
+
+ @Test
+ public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ assertThrows(UnsupportedOperationException.class, () -> mTM.tether("iface"));
+ assertThrows(UnsupportedOperationException.class, () -> mTM.untether("iface"));
+ }
}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 06bdca6..437eb81 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -16,6 +16,7 @@
package com.android.server.net.integrationtests
+import android.Manifest.permission
import android.app.usage.NetworkStatsManager
import android.content.ComponentName
import android.content.Context
@@ -54,18 +55,21 @@
import com.android.networkstack.apishim.TelephonyManagerShimImpl
import com.android.server.BpfNetMaps
import com.android.server.ConnectivityService
+import com.android.server.L2capNetworkProvider
import com.android.server.NetworkAgentWrapper
import com.android.server.TestNetIdManager
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
import com.android.server.connectivity.ConnectivityResources
import com.android.server.connectivity.MockableSystemProperties
import com.android.server.connectivity.MultinetworkPolicyTracker
+import com.android.server.connectivity.PermissionMonitor
import com.android.server.connectivity.ProxyTracker
import com.android.server.connectivity.SatelliteAccessController
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.DeviceInfoUtils
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
import com.android.testutils.tryTest
import java.util.function.BiConsumer
import java.util.function.Consumer
@@ -208,7 +212,9 @@
networkStackClient = TestNetworkStackClient(realContext)
networkStackClient.start()
- service = TestConnectivityService(TestDependencies())
+ service = runAsShell(permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) {
+ TestConnectivityService(TestDependencies())
+ }
cm = ConnectivityManager(context, service)
context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm)
context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager)
@@ -217,7 +223,7 @@
}
private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
- context, dnsResolver, log, netd, deps)
+ context, dnsResolver, log, netd, deps, PermissionMonitorDependencies())
private inner class TestDependencies : ConnectivityService.Dependencies() {
override fun getNetworkStack() = networkStackClient
@@ -268,6 +274,12 @@
connectivityServiceInternalHandler: Handler
): SatelliteAccessController? = mock(
SatelliteAccessController::class.java)
+
+ override fun makeL2capNetworkProvider(context: Context) = null
+ }
+
+ private inner class PermissionMonitorDependencies : PermissionMonitor.Dependencies() {
+ override fun shouldEnforceLocalNetRestrictions(uid: Int) = false
}
@After
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index 39a08fa..02ac3c5 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -24,7 +24,6 @@
"libcom.android.tethering.connectivity_native",
"liblog",
"libnetutils",
- "libprocessgroup",
],
static_libs: [
"connectivity_native_aidl_interface-lateststable-ndk",
diff --git a/tests/unit/java/android/net/TrafficStatsTest.kt b/tests/unit/java/android/net/TrafficStatsTest.kt
index c61541e..0f85daf 100644
--- a/tests/unit/java/android/net/TrafficStatsTest.kt
+++ b/tests/unit/java/android/net/TrafficStatsTest.kt
@@ -1,46 +1,251 @@
/*
-* 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.
-*/
+ * 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
-import android.net.TrafficStats.getValueForTypeFromFirstEntry
-import android.net.TrafficStats.TYPE_RX_BYTES
import android.net.TrafficStats.UNSUPPORTED
+import android.net.netstats.StatsResult
+import android.net.netstats.TrafficStatsRateLimitCacheConfig
import android.os.Build
+import com.android.server.net.NetworkStatsService.TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import kotlin.test.assertEquals
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.HashMap
+import java.util.function.LongSupplier
-private const val TEST_IFACE1 = "test_iface1"
+const val TEST_EXPIRY_DURATION_MS = 1000
+const val TEST_IFACE = "wlan0"
@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class TrafficStatsTest {
+ private val binder = mock(INetworkStatsService::class.java)
+ private val myUid = android.os.Process.myUid()
+ private val mockMyUidStatsResult = StatsResult(5L, 6L, 7L, 8L)
+ private val mockIfaceStatsResult = StatsResult(7L, 3L, 10L, 21L)
+ private val mockTotalStatsResult = StatsResult(8L, 1L, 5L, 2L)
+ private val secondUidStatsResult = StatsResult(3L, 7L, 10L, 5L)
+ private val secondIfaceStatsResult = StatsResult(9L, 8L, 7L, 6L)
+ private val secondTotalStatsResult = StatsResult(4L, 3L, 2L, 1L)
+ private val emptyStatsResult = StatsResult(0L, 0L, 0L, 0L)
+ private val unsupportedStatsResult =
+ StatsResult(UNSUPPORTED.toLong(), UNSUPPORTED.toLong(),
+ UNSUPPORTED.toLong(), UNSUPPORTED.toLong())
- @Test
- fun testGetValueForTypeFromFirstEntry() {
- var stats: NetworkStats = NetworkStats(0, 0)
- // empty stats
- assertEquals(getValueForTypeFromFirstEntry(stats, TYPE_RX_BYTES), UNSUPPORTED.toLong())
- // invalid type
- stats.insertEntry(TEST_IFACE1, 1, 2, 3, 4)
- assertEquals(getValueForTypeFromFirstEntry(stats, 1000), UNSUPPORTED.toLong())
- // valid type
- assertEquals(getValueForTypeFromFirstEntry(stats, TYPE_RX_BYTES), 1)
+ private val cacheDisabledConfig = TrafficStatsRateLimitCacheConfig.Builder()
+ .setIsCacheEnabled(false)
+ .setExpiryDurationMs(0)
+ .setMaxEntries(0)
+ .build()
+ private val cacheEnabledConfig = TrafficStatsRateLimitCacheConfig.Builder()
+ .setIsCacheEnabled(true)
+ .setExpiryDurationMs(TEST_EXPIRY_DURATION_MS)
+ .setMaxEntries(100)
+ .build()
+ private val mTestTimeSupplier = TestTimeSupplier()
+
+ private val featureFlags = HashMap<String, Boolean>()
+
+ // This will set feature flags from @FeatureFlag annotations
+ // into the map before setUp() runs.
+ @get:Rule
+ val setFeatureFlagsRule = SetFeatureFlagsRule(
+ { name, enabled -> featureFlags.put(name, enabled == true) },
+ { name -> featureFlags.getOrDefault(name, false) }
+ )
+
+ class TestTimeSupplier : LongSupplier {
+ private var currentTimeMillis = 0L
+
+ override fun getAsLong() = currentTimeMillis
+
+ fun advanceTime(millis: Int) {
+ currentTimeMillis += millis
+ }
}
-}
\ No newline at end of file
+
+ @Before
+ fun setUp() {
+ TrafficStats.setServiceForTest(binder)
+ TrafficStats.setTimeSupplierForTest(mTestTimeSupplier)
+ mockStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ if (featureFlags.getOrDefault(TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, false)) {
+ doReturn(cacheEnabledConfig).`when`(binder).getRateLimitCacheConfig()
+ } else {
+ doReturn(cacheDisabledConfig).`when`(binder).getRateLimitCacheConfig()
+ }
+ TrafficStats.reinitRateLimitCacheForTest()
+ }
+
+ @After
+ fun tearDown() {
+ TrafficStats.setServiceForTest(null)
+ TrafficStats.setTimeSupplierForTest(null)
+ TrafficStats.reinitRateLimitCacheForTest()
+ }
+
+ private fun assertUidStats(uid: Int, stats: StatsResult) {
+ assertEquals(stats.rxBytes, TrafficStats.getUidRxBytes(uid))
+ assertEquals(stats.rxPackets, TrafficStats.getUidRxPackets(uid))
+ assertEquals(stats.txBytes, TrafficStats.getUidTxBytes(uid))
+ assertEquals(stats.txPackets, TrafficStats.getUidTxPackets(uid))
+ }
+
+ private fun assertIfaceStats(iface: String, stats: StatsResult) {
+ assertEquals(stats.rxBytes, TrafficStats.getRxBytes(iface))
+ assertEquals(stats.rxPackets, TrafficStats.getRxPackets(iface))
+ assertEquals(stats.txBytes, TrafficStats.getTxBytes(iface))
+ assertEquals(stats.txPackets, TrafficStats.getTxPackets(iface))
+ }
+
+ private fun assertTotalStats(stats: StatsResult) {
+ assertEquals(stats.rxBytes, TrafficStats.getTotalRxBytes())
+ assertEquals(stats.rxPackets, TrafficStats.getTotalRxPackets())
+ assertEquals(stats.txBytes, TrafficStats.getTotalTxBytes())
+ assertEquals(stats.txPackets, TrafficStats.getTotalTxPackets())
+ }
+
+ private fun mockStats(uidStats: StatsResult?, ifaceStats: StatsResult?,
+ totalStats: StatsResult?) {
+ doReturn(uidStats).`when`(binder).getUidStats(myUid)
+ doReturn(ifaceStats).`when`(binder).getIfaceStats(TEST_IFACE)
+ doReturn(totalStats).`when`(binder).getTotalStats()
+ }
+
+ private fun assertStats(uidStats: StatsResult, ifaceStats: StatsResult,
+ totalStats: StatsResult) {
+ assertUidStats(myUid, uidStats)
+ assertIfaceStats(TEST_IFACE, ifaceStats)
+ assertTotalStats(totalStats)
+ }
+
+ private fun assertStatsFetchInvocations(wantedInvocations: Int) {
+ verify(binder, times(wantedInvocations)).getUidStats(myUid)
+ verify(binder, times(wantedInvocations)).getIfaceStats(TEST_IFACE)
+ verify(binder, times(wantedInvocations)).getTotalStats()
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @Test
+ fun testRateLimitCacheExpiry_cacheEnabled() {
+ // Initial fetch, verify binder calls.
+ assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ assertStatsFetchInvocations(1)
+
+ // Advance time within expiry, verify cached values used.
+ clearInvocations(binder)
+ mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ mTestTimeSupplier.advanceTime(1)
+ assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ assertStatsFetchInvocations(0)
+
+ // Advance time to expire cache, verify new values fetched.
+ clearInvocations(binder)
+ mTestTimeSupplier.advanceTime(TEST_EXPIRY_DURATION_MS)
+ assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ assertStatsFetchInvocations(1)
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @Test
+ fun testRateLimitCacheExpiry_cacheDisabled() {
+ // Initial fetch, verify binder calls.
+ assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ assertStatsFetchInvocations(4)
+
+ // Advance time within expiry, verify new values fetched.
+ clearInvocations(binder)
+ mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ mTestTimeSupplier.advanceTime(1)
+ assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ assertStatsFetchInvocations(4)
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @Test
+ fun testInvalidStatsNotCached_cacheEnabled() {
+ doTestInvalidStatsNotCached()
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @Test
+ fun testInvalidStatsNotCached_cacheDisabled() {
+ doTestInvalidStatsNotCached()
+ }
+
+ private fun doTestInvalidStatsNotCached() {
+ // Mock null stats, this usually happens when the query is not valid,
+ // e.g. query uid stats of other application.
+ mockStats(null, null, null)
+ assertStats(unsupportedStatsResult, unsupportedStatsResult, unsupportedStatsResult)
+ assertStatsFetchInvocations(4)
+
+ // Verify null stats is not cached, and mock empty stats. This usually
+ // happens when queries with non-existent interface names.
+ clearInvocations(binder)
+ mockStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+ assertStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+ assertStatsFetchInvocations(4)
+
+ // Verify empty result is also not cached.
+ clearInvocations(binder)
+ assertStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+ assertStatsFetchInvocations(4)
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @Test
+ fun testClearRateLimitCaches_cacheEnabled() {
+ doTestClearRateLimitCaches(true)
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @Test
+ fun testClearRateLimitCaches_cacheDisabled() {
+ doTestClearRateLimitCaches(false)
+ }
+
+ private fun doTestClearRateLimitCaches(cacheEnabled: Boolean) {
+ // Initial fetch, verify binder calls.
+ assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ assertStatsFetchInvocations(if (cacheEnabled) 1 else 4)
+
+ // Verify cached values are used.
+ clearInvocations(binder)
+ assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ assertStatsFetchInvocations(if (cacheEnabled) 0 else 4)
+
+ // Clear caches, verify fetching from the service.
+ clearInvocations(binder)
+ TrafficStats.clearRateLimitCaches()
+ mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ assertStatsFetchInvocations(if (cacheEnabled) 1 else 4)
+ }
+}
diff --git a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
index c491f37..8117431 100644
--- a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -44,14 +44,14 @@
serviceType = "_ipp._tcp"
}
val beforeParcel = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
- .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
.setTtl(Duration.ofSeconds(30L))
.build()
val afterParcel = parcelingRoundTrip(beforeParcel)
assertEquals(beforeParcel.serviceInfo.serviceType, afterParcel.serviceInfo.serviceType)
- assertEquals(beforeParcel.advertisingConfig, afterParcel.advertisingConfig)
+ assertEquals(beforeParcel.flags, afterParcel.flags)
}
@Test
@@ -72,13 +72,13 @@
serviceType = "_ipp._tcp"
}
val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
- .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
.setTtl(Duration.ofSeconds(100L))
.build()
assertEquals("_ipp._tcp", request.serviceInfo.serviceType)
assertEquals(PROTOCOL_DNS_SD, request.protocolType)
- assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.advertisingConfig)
+ assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.flags)
assertEquals(Duration.ofSeconds(100L), request.ttl)
}
@@ -90,11 +90,11 @@
val request1 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
val request2 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
val request3 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
- .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
.setTtl(Duration.ofSeconds(120L))
.build()
val request4 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
- .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
.setTtl(Duration.ofSeconds(120L))
.build()
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index c1c15ca..caf1765 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -71,6 +71,7 @@
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;
@@ -99,6 +100,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.Bool;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U32;
import com.android.net.module.util.Struct.U8;
@@ -106,6 +108,7 @@
import com.android.net.module.util.bpf.CookieTagMapValue;
import com.android.net.module.util.bpf.IngressDiscardKey;
import com.android.net.module.util.bpf.IngressDiscardValue;
+import com.android.net.module.util.bpf.LocalNetAccessKey;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -171,6 +174,10 @@
private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap =
new TestBpfMap<>(S32.class, UidOwnerValue.class);
private final IBpfMap<S32, U8> mUidPermissionMap = new TestBpfMap<>(S32.class, U8.class);
+ private final IBpfMap<U32, Bool> mLocalNetBlockedUidMap =
+ new TestBpfMap<>(U32.class, Bool.class);
+ private final IBpfMap<LocalNetAccessKey, Bool> mLocalNetAccessMap =
+ new TestBpfMap<>(LocalNetAccessKey.class, Bool.class);
private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap =
spy(new TestBpfMap<>(CookieTagMapKey.class, CookieTagMapValue.class));
private final IBpfMap<S32, U8> mDataSaverEnabledMap = new TestBpfMap<>(S32.class, U8.class);
@@ -189,6 +196,8 @@
CURRENT_STATS_MAP_CONFIGURATION_KEY, new U32(STATS_SELECT_MAP_A));
BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
BpfNetMaps.setUidPermissionMapForTest(mUidPermissionMap);
+ BpfNetMaps.setLocalNetAccessMapForTest(mLocalNetAccessMap);
+ BpfNetMaps.setLocalNetBlockedUidMapForTest(mLocalNetBlockedUidMap);
BpfNetMaps.setCookieTagMapForTest(mCookieTagMap);
BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
@@ -235,6 +244,225 @@
}
@Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.addLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0, true));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessWithNullInterfaceAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, null,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ // As we tried to add null interface, it would be skipped and map should be empty.
+ assertTrue(mLocalNetAccessMap.isEmpty());
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessAfterVWithIncorrectInterface() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ // wlan2 is an incorrect interface
+ mBpfNetMaps.addLocalNetAccess(160, "wlan2",
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ // As we tried to add incorrect interface, it would be skipped and map should be empty.
+ assertTrue(mLocalNetAccessMap.isEmpty());
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testGetLocalNetAccessBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.getLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testGetLocalNetAccessAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetAccessMap.updateEntry(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0),
+ new Bool(false));
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+
+ assertFalse(mBpfNetMaps.getLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0));
+ assertTrue(mBpfNetMaps.getLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("100.68.0.0"), 0, 0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testGetLocalNetAccessWithNullInterfaceAfterV() throws Exception {
+ assertTrue(mBpfNetMaps.getLocalNetAccess(160, null,
+ Inet4Address.getByName("100.68.0.0"), 0, 0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveLocalNetAccessBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.removeLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveLocalNetAccessAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+ mBpfNetMaps.removeLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0);
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveLocalNetAccessAfterVWithIncorrectInterface() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+ mBpfNetMaps.removeLocalNetAccess(160, "wlan2",
+ Inet4Address.getByName("196.68.0.0"), 0, 0);
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveLocalNetAccessAfterVWithNullInterface() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+ mBpfNetMaps.removeLocalNetAccess(160, null,
+ Inet4Address.getByName("196.68.0.0"), 0, 0);
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddUidToLocalNetBlockMapBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.addUidToLocalNetBlockMap(0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testIsUidBlockedFromUsingLocalNetworkBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveUidFromLocalNetBlockMapBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddUidFromLocalNetBlockMapAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addUidToLocalNetBlockMap(uid0);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid0)).val);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid1)));
+
+ mBpfNetMaps.addUidToLocalNetBlockMap(uid1);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testIsUidBlockedFromUsingLocalNetworkAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
+ assertTrue(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid0));
+ assertFalse(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid1));
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
+ assertTrue(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid1));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveUidFromLocalNetBlockMapAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
+
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid0)).val);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid0);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid0)));
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid1);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid1)));
+ }
+
+ @Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testIsChainEnabled() throws Exception {
doTestIsChainEnabled(FIREWALL_CHAIN_DOZABLE);
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 999d17d..19a41d8 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -163,10 +163,7 @@
import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
-import static com.android.server.ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK;
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
-import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
-import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_OEM;
@@ -177,9 +174,6 @@
import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
-import static com.android.server.connectivity.ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN;
-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.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.ConcurrentUtils.await;
import static com.android.testutils.ConcurrentUtils.durationOf;
@@ -412,6 +406,7 @@
import com.android.server.ConnectivityService.NetworkRequestInfo;
import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.DestroySocketsWrapper;
import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
+import com.android.server.L2capNetworkProvider;
import com.android.server.connectivity.ApplicationSelfCertifiedNetworkCapabilities;
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker;
import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
@@ -425,6 +420,7 @@
import com.android.server.connectivity.NetworkAgentInfo;
import com.android.server.connectivity.NetworkNotificationManager;
import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import com.android.server.connectivity.PermissionMonitor;
import com.android.server.connectivity.ProxyTracker;
import com.android.server.connectivity.QosCallbackTracker;
import com.android.server.connectivity.SatelliteAccessController;
@@ -593,6 +589,7 @@
private MockContext mServiceContext;
private HandlerThread mCsHandlerThread;
private ConnectivityServiceDependencies mDeps;
+ private PermissionMonitorDependencies mPermDeps;
private AutomaticOnOffKeepaliveTrackerDependencies mAutoOnOffKeepaliveDependencies;
private ConnectivityService mService;
private WrappedConnectivityManager mCm;
@@ -1920,6 +1917,7 @@
doReturn(mResources).when(mockResContext).getResources();
ConnectivityResources.setResourcesContextForTest(mockResContext);
mDeps = new ConnectivityServiceDependencies(mockResContext);
+ mPermDeps = new PermissionMonitorDependencies();
doReturn(true).when(mMockKeepaliveTrackerDependencies)
.isAddressTranslationEnabled(mServiceContext);
doReturn(new ConnectivityResources(mockResContext)).when(mMockKeepaliveTrackerDependencies)
@@ -1932,7 +1930,7 @@
mMockDnsResolver,
mock(IpConnectivityLog.class),
mMockNetd,
- mDeps);
+ mDeps, mPermDeps);
mService.mLingerDelayMs = TEST_LINGER_DELAY_MS;
mService.mNascentDelayMs = TEST_NASCENT_DELAY_MS;
@@ -2181,28 +2179,30 @@
case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
- case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
+ case ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN:
+ case ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION:
return true;
default:
- return super.isFeatureEnabled(context, name);
+ // This is a unit test and must never depend on actual device flag values.
+ throw new UnsupportedOperationException("Unknown flag " + name
+ + ", update this test");
}
}
@Override
public boolean isFeatureNotChickenedOut(Context context, String name) {
switch (name) {
- case ALLOW_SYSUI_CONNECTIVITY_REPORTS:
- return true;
- case ALLOW_SATALLITE_NETWORK_FALLBACK:
- return true;
- case INGRESS_TO_VPN_ADDRESS_FILTERING:
- return true;
- case BACKGROUND_FIREWALL_CHAIN:
- return true;
- case DELAY_DESTROY_SOCKETS:
+ case ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS:
+ case ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK:
+ case ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING:
+ case ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN:
+ case ConnectivityFlags.DELAY_DESTROY_SOCKETS:
+ case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
+ case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
return true;
default:
- return super.isFeatureNotChickenedOut(context, name);
+ throw new UnsupportedOperationException("Unknown flag " + name
+ + ", update this test");
}
}
@@ -2369,6 +2369,30 @@
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;
+ }
+
+ @Override
+ public L2capNetworkProvider makeL2capNetworkProvider(Context context) {
+ return null;
+ }
+ }
+
+ static class PermissionMonitorDependencies extends PermissionMonitor.Dependencies {
+ @Override
+ public boolean shouldEnforceLocalNetRestrictions(int uid) {
+ return false;
+ }
}
private class AutomaticOnOffKeepaliveTrackerDependencies
@@ -2419,6 +2443,10 @@
@After
public void tearDown() throws Exception {
+ // Don't attempt to tear down if setUp didn't even get as far as creating the service.
+ // Otherwise, exceptions here will mask the actual exception in setUp, making failures
+ // harder to diagnose.
+ if (mService == null) return;
unregisterDefaultNetworkCallbacks();
maybeTearDownEnterpriseNetwork();
setAlwaysOnNetworks(false);
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkPermissionsTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkPermissionsTest.kt
new file mode 100644
index 0000000..8a9d288
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkPermissionsTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2025 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.connectivity
+
+import android.net.INetd
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class NetworkPermissionsTest {
+ @Test
+ fun test_networkTrafficPerms_correctValues() {
+ assertEquals(NetworkPermissions.PERMISSION_NONE, INetd.PERMISSION_NONE) /* 0 */
+ assertEquals(NetworkPermissions.PERMISSION_NETWORK, INetd.PERMISSION_NETWORK) /* 1 */
+ assertEquals(NetworkPermissions.PERMISSION_SYSTEM, INetd.PERMISSION_SYSTEM) /* 2 */
+ assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_INTERNET, 4)
+ assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS, 8)
+ assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED, -1)
+ assertEquals(NetworkPermissions.TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST, 16)
+ }
+
+ @Test
+ fun test_noOverridesInFlags() {
+ val permsList = listOf(
+ NetworkPermissions.PERMISSION_NONE,
+ NetworkPermissions.PERMISSION_NETWORK,
+ NetworkPermissions.PERMISSION_SYSTEM,
+ NetworkPermissions.TRAFFIC_PERMISSION_INTERNET,
+ NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS,
+ NetworkPermissions.TRAFFIC_PERMISSION_SDKSANDBOX_LOCALHOST,
+ NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED
+ )
+ assertFalse(hasDuplicates(permsList))
+ }
+
+ fun hasDuplicates(list: List<Int>): Boolean {
+ return list.distinct().size != list.size
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 5bde31a..ec9c6b0 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -21,7 +21,9 @@
import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.INTERNET;
+import static android.Manifest.permission.NEARBY_WIFI_DEVICES;
import static android.Manifest.permission.NETWORK_STACK;
+import static android.Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_OEM;
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRODUCT;
@@ -30,6 +32,7 @@
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
import static android.net.INetd.PERMISSION_INTERNET;
import static android.net.INetd.PERMISSION_NETWORK;
@@ -38,15 +41,20 @@
import static android.net.INetd.PERMISSION_UNINSTALLED;
import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.connectivity.ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK;
import static android.os.Process.SYSTEM_UID;
+import static android.permission.PermissionManager.PERMISSION_GRANTED;
import static com.android.server.connectivity.PermissionMonitor.isHigherNetworkPermission;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -66,6 +74,8 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -83,7 +93,9 @@
import android.os.SystemConfigManager;
import android.os.UserHandle;
import android.os.UserManager;
+import android.permission.PermissionManager;
import android.provider.Settings;
+import android.util.ArraySet;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
@@ -100,9 +112,13 @@
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.AdditionalAnswers;
import org.mockito.ArgumentCaptor;
@@ -119,6 +135,8 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public class PermissionMonitorTest {
+ @Rule
+ public TestRule compatChangeRule = new PlatformCompatChangeRule();
private static final int MOCK_USER_ID1 = 0;
private static final int MOCK_USER_ID2 = 1;
private static final int MOCK_USER_ID3 = 2;
@@ -160,9 +178,14 @@
private static final int PERMISSION_TRAFFIC_ALL =
PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS;
private static final int TIMEOUT_MS = 2_000;
+ // The ACCESS_LOCAL_NETWORK permission is not available yet. For the time being, use
+ // NEARBY_WIFI_DEVICES as a means to develop, for expediency.
+ // TODO(b/375236298): remove this constant when the ACCESS_LOCAL_NETWORK permission is defined.
+ private static final String ACCESS_LOCAL_NETWORK = NEARBY_WIFI_DEVICES;
@Mock private Context mContext;
@Mock private PackageManager mPackageManager;
+ @Mock private PermissionManager mPermissionManager;
@Mock private INetd mNetdService;
@Mock private UserManager mUserManager;
@Mock private PermissionMonitor.Dependencies mDeps;
@@ -181,6 +204,7 @@
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager);
doReturn(List.of(MOCK_USER1)).when(mUserManager).getUserHandles(eq(true));
+ when(mContext.getSystemService(PermissionManager.class)).thenReturn(mPermissionManager);
when(mContext.getSystemServiceName(SystemConfigManager.class))
.thenReturn(Context.SYSTEM_CONFIG_SERVICE);
when(mContext.getSystemService(Context.SYSTEM_CONFIG_SERVICE))
@@ -293,19 +317,28 @@
return result;
}
- private void buildAndMockPackageInfoWithPermissions(String packageName, int uid,
+ private PackageInfo buildAndMockPackageInfoWithPermissions(String packageName, int uid,
String... permissions) throws Exception {
final PackageInfo packageInfo = buildPackageInfo(packageName, uid, permissions);
// This will return the wrong UID for the package when queried with other users.
doReturn(packageInfo).when(mPackageManager)
.getPackageInfo(eq(packageName), anyInt() /* flag */);
+ if (BpfNetMaps.isAtLeast25Q2()) {
+ // Runtime permission checks for local net restrictions were introduced in 25Q2
+ for (String permission : permissions) {
+ doReturn(PERMISSION_GRANTED).when(mPermissionManager).checkPermissionForPreflight(
+ eq(permission),
+ argThat(attributionSource -> attributionSource.getUid() == uid));
+ }
+ }
final String[] oldPackages = mPackageManager.getPackagesForUid(uid);
// If it's duplicated package, no need to set it again.
- if (CollectionUtils.contains(oldPackages, packageName)) return;
+ if (CollectionUtils.contains(oldPackages, packageName)) return packageInfo;
// Combine the package if this uid is shared with other packages.
final String[] newPackages = appendElement(String.class, oldPackages, packageName);
doReturn(newPackages).when(mPackageManager).getPackagesForUid(eq(uid));
+ return packageInfo;
}
private void startMonitoring() {
@@ -340,7 +373,7 @@
private void addPackage(String packageName, int uid, String... permissions) throws Exception {
buildAndMockPackageInfoWithPermissions(packageName, uid, permissions);
- processOnHandlerThread(() -> mPermissionMonitor.onPackageAdded(packageName, uid));
+ onPackageAdded(packageName, uid);
}
private void removePackage(String packageName, int uid) {
@@ -352,7 +385,12 @@
final String[] newPackages = Arrays.stream(oldPackages).filter(e -> !e.equals(packageName))
.toArray(String[]::new);
doReturn(newPackages).when(mPackageManager).getPackagesForUid(eq(uid));
- processOnHandlerThread(() -> mPermissionMonitor.onPackageRemoved(packageName, uid));
+ if (BpfNetMaps.isAtLeast25Q2()){
+ // Runtime permission checks for local net restrictions were introduced in 25Q2
+ doReturn(PERMISSION_DENIED).when(mPermissionManager).checkPermissionForPreflight(
+ anyString(), argThat(as -> as.getUid() == uid));
+ }
+ onPackageRemoved(packageName, uid);
}
@Test
@@ -583,6 +621,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testHasUseBackgroundNetworksPermission() throws Exception {
assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(SYSTEM_UID));
assertBackgroundPermission(false, SYSTEM_PACKAGE1, SYSTEM_UID);
@@ -604,6 +643,7 @@
private class BpfMapMonitor {
private final SparseIntArray mAppIdsTrafficPermission = new SparseIntArray();
+ private final ArraySet<Integer> mLocalNetBlockedUids = new ArraySet<>();
private static final int DOES_NOT_EXIST = -2;
BpfMapMonitor(BpfNetMaps mockBpfmap) throws Exception {
@@ -616,6 +656,18 @@
}
return null;
}).when(mockBpfmap).setNetPermForUids(anyInt(), any(int[].class));
+ doAnswer((InvocationOnMock invocation) -> {
+ final Object[] args = invocation.getArguments();
+ final int uid = (int) args[0];
+ mLocalNetBlockedUids.add(uid);
+ return null;
+ }).when(mockBpfmap).addUidToLocalNetBlockMap(anyInt());
+ doAnswer((InvocationOnMock invocation) -> {
+ final Object[] args = invocation.getArguments();
+ final int uid = (int) args[0];
+ mLocalNetBlockedUids.remove(uid);
+ return null;
+ }).when(mockBpfmap).removeUidFromLocalNetBlockMap(anyInt());
}
public void expectTrafficPerm(int permission, Integer... appIds) {
@@ -640,6 +692,18 @@
}
}
}
+
+ public boolean hasLocalNetPermissions(int uid) {
+ return !mLocalNetBlockedUids.contains(uid);
+ }
+
+ public boolean isUidPresentInLocalNetBlockMap(int uid) {
+ return mLocalNetBlockedUids.contains(uid);
+ }
+
+ public boolean hasBlockedLocalNetForSandboxUid(int sandboxUid) {
+ return mLocalNetBlockedUids.contains(sandboxUid);
+ }
}
private class NetdMonitor {
@@ -723,6 +787,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUserAndPackageAddRemove() throws Exception {
// MOCK_UID11: MOCK_PACKAGE1 only has network permission.
// SYSTEM_APP_UID11: SYSTEM_PACKAGE1 has system permission.
@@ -812,6 +877,48 @@
MOCK_APPID1);
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_onUserAdded() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ final PackageInfo packageInfo = buildAndMockPackageInfoWithPermissions(
+ MOCK_PACKAGE1, MOCK_UID11, CHANGE_NETWORK_STATE);
+ // Set package for all users on devices
+ doReturn(List.of(packageInfo)).when(mPackageManager)
+ .getInstalledPackagesAsUser(anyInt(), eq(MOCK_USER1.getIdentifier()));
+ onUserAdded(MOCK_USER1);
+
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+ if (hasSdkSandbox(MOCK_UID11)) {
+ assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+ }
+ }
+
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_onUserRemoved() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ final PackageInfo packageInfo = buildAndMockPackageInfoWithPermissions(
+ MOCK_PACKAGE1, MOCK_UID11, CHANGE_NETWORK_STATE);
+ // Set package for all users on devices
+ doReturn(List.of(packageInfo)).when(mPackageManager)
+ .getInstalledPackagesAsUser(anyInt(), eq(MOCK_USER1.getIdentifier()));
+ onUserAdded(MOCK_USER1);
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+ onUserRemoved(MOCK_USER1);
+ assertFalse(mBpfMapMonitor.isUidPresentInLocalNetBlockMap(MOCK_UID11));
+ }
+
private void doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
throws Exception {
doReturn(List.of(
@@ -858,11 +965,13 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
throws Exception {
doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
@@ -895,16 +1004,19 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
doTestUidFilteringDuringPackageInstallAndUninstall("tun0");
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidFilteringDuringPackageInstallAndUninstallWithWildcard() throws Exception {
doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithLockdownEnableDisable() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -936,6 +1048,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -977,6 +1090,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAddAndOverlap() {
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
@@ -1037,6 +1151,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -1071,6 +1186,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testLockdownUidFilteringWithInstallAndUnInstall() {
doReturn(List.of(
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
@@ -1107,15 +1223,13 @@
// called multiple times with the uid corresponding to each user.
private void addPackageForUsers(UserHandle[] users, String packageName, int appId) {
for (final UserHandle user : users) {
- processOnHandlerThread(() ->
- mPermissionMonitor.onPackageAdded(packageName, user.getUid(appId)));
+ onPackageAdded(packageName, user.getUid(appId));
}
}
private void removePackageForUsers(UserHandle[] users, String packageName, int appId) {
for (final UserHandle user : users) {
- processOnHandlerThread(() ->
- mPermissionMonitor.onPackageRemoved(packageName, user.getUid(appId)));
+ onPackageRemoved(packageName, user.getUid(appId));
}
}
@@ -1163,6 +1277,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageInstall() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1171,7 +1286,25 @@
mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID2);
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_onPackageInstall() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET);
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+ addPackage(MOCK_PACKAGE2, MOCK_UID12, ACCESS_LOCAL_NETWORK);
+ assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID12));
+ if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID12)));
+ }
+
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageInstallSharedUid() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1183,6 +1316,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageUninstallBasic() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1192,7 +1326,24 @@
mBpfMapMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, MOCK_APPID1);
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_onPackageUninstall() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ addPackage(MOCK_PACKAGE1, MOCK_UID11, ACCESS_LOCAL_NETWORK);
+ assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+ when(mPackageManager.getPackagesForUid(MOCK_UID11)).thenReturn(new String[]{});
+ onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
+ assertFalse(mBpfMapMonitor.isUidPresentInLocalNetBlockMap(MOCK_UID11));
+ }
+
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageRemoveThenAdd() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1205,7 +1356,30 @@
mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1);
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_onPackageRemoveThenAdd() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ addPackage(MOCK_PACKAGE1, MOCK_UID11, ACCESS_LOCAL_NETWORK);
+ assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+ if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+
+ removePackage(MOCK_PACKAGE1, MOCK_UID11);
+ assertFalse(mBpfMapMonitor.isUidPresentInLocalNetBlockMap(MOCK_UID11));
+
+ addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET);
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+ if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+ }
+
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageUpdate() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1);
@@ -1215,6 +1389,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testPackageUninstallWithMultiplePackages() throws Exception {
addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS);
mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);
@@ -1235,8 +1410,10 @@
// Use the real context as this test must ensure the *real* system package holds the
// necessary permission.
final Context realContext = InstrumentationRegistry.getContext();
- final PermissionMonitor monitor = new PermissionMonitor(
- realContext, mNetdService, mBpfNetMaps, mHandlerThread);
+ final PermissionMonitor monitor = runAsShell(
+ OBSERVE_GRANT_REVOKE_PERMISSIONS,
+ () -> new PermissionMonitor(realContext, mNetdService, mBpfNetMaps, mHandlerThread)
+ );
final PackageManager manager = realContext.getPackageManager();
final PackageInfo systemInfo = manager.getPackageInfo(REAL_SYSTEM_PACKAGE_NAME,
GET_PERMISSIONS | MATCH_ANY_USER);
@@ -1244,6 +1421,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUpdateUidPermissionsFromSystemConfig() throws Exception {
when(mSystemConfigManager.getSystemPermissionUids(eq(INTERNET)))
.thenReturn(new int[]{ MOCK_UID11, MOCK_UID12 });
@@ -1283,6 +1461,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testIntentReceiver() throws Exception {
startMonitoring();
final BroadcastReceiver receiver = expectBroadcastReceiver(
@@ -1321,6 +1500,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidsAllowedOnRestrictedNetworksChanged() throws Exception {
startMonitoring();
final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1353,6 +1533,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidsAllowedOnRestrictedNetworksChangedWithSharedUid() throws Exception {
startMonitoring();
final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1386,6 +1567,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testUidsAllowedOnRestrictedNetworksChangedWithMultipleUsers() throws Exception {
startMonitoring();
final ContentObserver contentObserver = expectRegisterContentObserver(
@@ -1440,6 +1622,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testOnExternalApplicationsAvailable() throws Exception {
// Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
// and have different uids. There has no permission for both uids.
@@ -1471,6 +1654,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testOnExternalApplicationsAvailable_AppsNotRegisteredOnStartMonitoring()
throws Exception {
startMonitoring();
@@ -1498,6 +1682,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testOnExternalApplicationsAvailableWithSharedUid()
throws Exception {
// Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
@@ -1524,6 +1709,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testOnExternalApplicationsAvailableWithSharedUid_DifferentStorage()
throws Exception {
// Initial the permission state. MOCK_PACKAGE1 is installed on external storage and
@@ -1566,6 +1752,38 @@
assertFalse(isHigherNetworkPermission(PERMISSION_SYSTEM, PERMISSION_SYSTEM));
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
+ public void testLocalNetRestrictions_setPermChanges() throws Exception {
+ assumeTrue(BpfNetMaps.isAtLeast25Q2());
+ doReturn(true).when(mDeps).shouldEnforceLocalNetRestrictions(anyInt());
+ when(mPermissionManager.checkPermissionForPreflight(
+ anyString(), any(AttributionSource.class))).thenReturn(PERMISSION_DENIED);
+ addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET);
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+
+ // Mock permission grant
+ when(mPermissionManager.checkPermissionForPreflight(
+ eq(ACCESS_LOCAL_NETWORK),
+ argThat(attributionSource -> attributionSource.getUid() == MOCK_UID11)))
+ .thenReturn(PERMISSION_GRANTED);
+ mPermissionMonitor.setLocalNetworkPermissions(MOCK_UID11, null);
+ assertTrue(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+ if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+
+ // Mock permission denied
+ when(mPermissionManager.checkPermissionForPreflight(
+ eq(ACCESS_LOCAL_NETWORK),
+ argThat(attributionSource -> attributionSource.getUid() == MOCK_UID11)))
+ .thenReturn(PERMISSION_DENIED);
+ mPermissionMonitor.setLocalNetworkPermissions(MOCK_UID11, null);
+ assertFalse(mBpfMapMonitor.hasLocalNetPermissions(MOCK_UID11));
+ if (hasSdkSandbox(MOCK_UID12)) assertTrue(mBpfMapMonitor.hasBlockedLocalNetForSandboxUid(
+ mProcessShim.toSdkSandboxUid(MOCK_UID11)));
+ }
+
private void prepareMultiUserPackages() {
// MOCK_USER1 has installed 3 packages
// mockApp1 has no permission and share MOCK_APPID1.
@@ -1598,7 +1816,7 @@
private void addUserAndVerifyAppIdsPermissions(UserHandle user, int appId1Perm,
int appId2Perm, int appId3Perm) {
- processOnHandlerThread(() -> mPermissionMonitor.onUserAdded(user));
+ onUserAdded(user);
mBpfMapMonitor.expectTrafficPerm(appId1Perm, MOCK_APPID1);
mBpfMapMonitor.expectTrafficPerm(appId2Perm, MOCK_APPID2);
mBpfMapMonitor.expectTrafficPerm(appId3Perm, MOCK_APPID3);
@@ -1606,13 +1824,14 @@
private void removeUserAndVerifyAppIdsPermissions(UserHandle user, int appId1Perm,
int appId2Perm, int appId3Perm) {
- processOnHandlerThread(() -> mPermissionMonitor.onUserRemoved(user));
+ onUserRemoved(user);
mBpfMapMonitor.expectTrafficPerm(appId1Perm, MOCK_APPID1);
mBpfMapMonitor.expectTrafficPerm(appId2Perm, MOCK_APPID2);
mBpfMapMonitor.expectTrafficPerm(appId3Perm, MOCK_APPID3);
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testAppIdsTrafficPermission_UserAddedRemoved() {
prepareMultiUserPackages();
@@ -1646,6 +1865,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testAppIdsTrafficPermission_Multiuser_PackageAdded() throws Exception {
// Add two users with empty package list.
onUserAdded(MOCK_USER1);
@@ -1716,6 +1936,7 @@
}
@Test
+ @EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
public void testAppIdsTrafficPermission_Multiuser_PackageRemoved() throws Exception {
// Add two users with empty package list.
onUserAdded(MOCK_USER1);
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index df48f6c..e6e6ecc 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -42,17 +42,20 @@
import java.util.Objects
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
+import kotlin.test.assertTrue
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.argThat
import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.doCallRealMethod
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
@@ -89,6 +92,13 @@
network = TEST_NETWORK_1
}
+private val GOOGLECAST_SERVICE = NsdServiceInfo("TestServiceName", "_googlecast._tcp").apply {
+ subtypes = setOf(TEST_SUBTYPE)
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = TEST_NETWORK_1
+}
+
private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
subtypes = setOf(TEST_SUBTYPE)
port = 12345
@@ -140,6 +150,15 @@
OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
)
+private val OFFLOAD_SERVICE_INFO_GOOGLECAST = OffloadServiceInfo(
+ OffloadServiceInfo.Key("TestServiceName", "_googlecast._tcp"),
+ listOf(),
+ "Android_test.local",
+ TEST_OFFLOAD_PACKET1,
+ Int.MAX_VALUE,
+ OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+)
+
private val OFFLOAD_SERVICEINFO_NO_SUBTYPE = OffloadServiceInfo(
OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
listOf(),
@@ -185,12 +204,26 @@
@Before
fun setUp() {
thread.start()
- doReturn(TEST_HOSTNAME).`when`(mockDeps).generateHostname()
- doReturn(mockInterfaceAdvertiser1).`when`(mockDeps).makeAdvertiser(eq(mockSocket1),
- any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
+ doReturn(TEST_HOSTNAME).`when`(mockDeps).generateHostname(anyBoolean())
+ doReturn(mockInterfaceAdvertiser1).`when`(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any()
)
- doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(eq(mockSocket2),
- any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
+ doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(
+ eq(mockSocket2),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any()
)
doReturn(true).`when`(mockInterfaceAdvertiser1).isProbing(anyInt())
doReturn(true).`when`(mockInterfaceAdvertiser2).isProbing(anyInt())
@@ -199,16 +232,21 @@
doReturn(TEST_INTERFACE1).`when`(mockInterfaceAdvertiser1).socketInterfaceName
doReturn(TEST_INTERFACE2).`when`(mockInterfaceAdvertiser2).socketInterfaceName
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
- SERVICE_ID_1)
+ SERVICE_ID_1
+ )
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
- SERVICE_ID_2)
+ SERVICE_ID_2
+ )
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
- SERVICE_ID_3)
+ SERVICE_ID_3
+ )
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser2).getRawOffloadPayload(
- SERVICE_ID_1)
+ SERVICE_ID_1
+ )
doReturn(resources).`when`(context).getResources()
doReturn(SERVICES_PRIORITY_LIST).`when`(resources).getStringArray(
- R.array.config_nsdOffloadServicesPriority)
+ R.array.config_nsdOffloadServicesPriority
+ )
ConnectivityResources.setResourcesContextForTest(context)
}
@@ -229,8 +267,12 @@
fun testAddService_OneNetwork() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -252,7 +294,9 @@
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
- mockInterfaceAdvertiser1, SERVICE_ID_1) }
+ mockInterfaceAdvertiser1,
+ SERVICE_ID_1
+ ) }
verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
@@ -293,12 +337,18 @@
fun testAddService_AllNetworksWithSubType() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ ALL_NETWORKS_SERVICE_SUBTYPE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
- verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
- socketCbCaptor.capture())
+ verify(socketProvider).requestSocket(
+ eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
+ socketCbCaptor.capture()
+ )
val socketCb = socketCbCaptor.value
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
@@ -306,30 +356,56 @@
val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
val intAdvCbCaptor2 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
- verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor1.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
)
- verify(mockDeps).makeAdvertiser(eq(mockSocket2), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket2),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor2.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
)
verify(mockInterfaceAdvertiser1).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
+ anyInt(),
+ eq(ALL_NETWORKS_SERVICE_SUBTYPE),
+ any()
+ )
verify(mockInterfaceAdvertiser2).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
+ anyInt(),
+ eq(ALL_NETWORKS_SERVICE_SUBTYPE),
+ any()
+ )
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
- mockInterfaceAdvertiser1, SERVICE_ID_1) }
+ mockInterfaceAdvertiser1,
+ SERVICE_ID_1
+ ) }
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO))
// Need both advertisers to finish probing and call onRegisterServiceSucceeded
verify(cb, never()).onRegisterServiceSucceeded(anyInt(), any())
doReturn(false).`when`(mockInterfaceAdvertiser2).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor2.value.onServiceProbingSucceeded(
- mockInterfaceAdvertiser2, SERVICE_ID_1) }
+ mockInterfaceAdvertiser2,
+ SERVICE_ID_1
+ ) }
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
- verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
+ verify(cb).onRegisterServiceSucceeded(
+ eq(SERVICE_ID_1),
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }
+ )
// Services are conflicted.
postSync {
@@ -375,19 +451,30 @@
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
postSync {
- advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, DEFAULT_ADVERTISING_OPTION,
- TEST_CLIENT_UID_1)
- advertiser.addOrUpdateService(SERVICE_ID_2,
+ advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ )
+ advertiser.addOrUpdateService(
+ SERVICE_ID_2,
NsdServiceInfo("TestService2", "_PRIORITYTEST._udp").apply {
port = 12345
hostAddresses = listOf(TEST_ADDR)
- }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+ },
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ )
advertiser.addOrUpdateService(
SERVICE_ID_3,
NsdServiceInfo("TestService3", "_notprioritized._tcp").apply {
port = 12345
hostAddresses = listOf(TEST_ADDR)
- }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+ },
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ )
}
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
@@ -397,8 +484,15 @@
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
- verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor1.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
)
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
@@ -430,30 +524,88 @@
}
@Test
+ fun testAddService_NoSubtypeForGoogleCastOffload() {
+ val advertiser =
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ postSync {
+ advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ GOOGLECAST_SERVICE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ )
+ }
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(SERVICE_1.network), socketCbCaptor.capture())
+
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+ val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor1.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
+ )
+
+ doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
+ postSync {
+ intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_1)
+ }
+
+ verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICE_INFO_GOOGLECAST))
+ }
+
+ @Test
fun testAddService_Conflicts() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
val oneNetSocketCb = oneNetSocketCbCaptor.value
// Register a service with the same name on all networks (name conflict)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_2,
+ ALL_NETWORKS_SERVICE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
val allNetSocketCb = allNetSocketCbCaptor.value
- postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
- postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ LONG_SERVICE_ID_1,
+ LONG_SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
+ postSync { advertiser.addOrUpdateService(
+ LONG_SERVICE_ID_2,
+ LONG_ALL_NETWORKS_SERVICE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
- postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
- ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ CASE_INSENSITIVE_TEST_SERVICE_ID,
+ ALL_NETWORKS_SERVICE_2,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
// Callbacks for matching network and all networks both get the socket
postSync {
@@ -462,7 +614,9 @@
}
val expectedRenamed = NsdServiceInfo(
- "${ALL_NETWORKS_SERVICE.serviceName} (2)", ALL_NETWORKS_SERVICE.serviceType).apply {
+ "${ALL_NETWORKS_SERVICE.serviceName} (2)",
+ ALL_NETWORKS_SERVICE.serviceType
+ ).apply {
port = ALL_NETWORKS_SERVICE.port
hostAddresses = ALL_NETWORKS_SERVICE.hostAddresses
network = ALL_NETWORKS_SERVICE.network
@@ -470,14 +624,16 @@
val expectedLongRenamed = NsdServiceInfo(
"${LONG_ALL_NETWORKS_SERVICE.serviceName.dropLast(4)} (2)",
- LONG_ALL_NETWORKS_SERVICE.serviceType).apply {
+ LONG_ALL_NETWORKS_SERVICE.serviceType
+ ).apply {
port = LONG_ALL_NETWORKS_SERVICE.port
hostAddresses = LONG_ALL_NETWORKS_SERVICE.hostAddresses
network = LONG_ALL_NETWORKS_SERVICE.network
}
val expectedCaseInsensitiveRenamed = NsdServiceInfo(
- "${ALL_NETWORKS_SERVICE_2.serviceName} (3)", ALL_NETWORKS_SERVICE_2.serviceType
+ "${ALL_NETWORKS_SERVICE_2.serviceName} (3)",
+ ALL_NETWORKS_SERVICE_2.serviceType
).apply {
port = ALL_NETWORKS_SERVICE_2.port
hostAddresses = ALL_NETWORKS_SERVICE_2.hostAddresses
@@ -485,30 +641,58 @@
}
val intAdvCbCaptor = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
- verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
)
- verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(SERVICE_1) }, any())
- verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) }, any())
- verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
- argThat { it.matches(LONG_SERVICE_1) }, any())
- verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
- argThat { it.matches(expectedLongRenamed) }, any())
- verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
- argThat { it.matches(expectedCaseInsensitiveRenamed) }, any())
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(SERVICE_ID_1),
+ argThat { it.matches(SERVICE_1) },
+ any()
+ )
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(SERVICE_ID_2),
+ argThat { it.matches(expectedRenamed) },
+ any()
+ )
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(LONG_SERVICE_ID_1),
+ argThat { it.matches(LONG_SERVICE_1) },
+ any()
+ )
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(LONG_SERVICE_ID_2),
+ argThat { it.matches(expectedLongRenamed) },
+ any()
+ )
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
+ argThat { it.matches(expectedCaseInsensitiveRenamed) },
+ any()
+ )
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
- mockInterfaceAdvertiser1, SERVICE_ID_1) }
+ mockInterfaceAdvertiser1,
+ SERVICE_ID_1
+ ) }
verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1), argThat { it.matches(SERVICE_1) })
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_2)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
- mockInterfaceAdvertiser1, SERVICE_ID_2) }
- verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) })
+ mockInterfaceAdvertiser1,
+ SERVICE_ID_2
+ ) }
+ verify(cb).onRegisterServiceSucceeded(
+ eq(SERVICE_ID_2),
+ argThat { it.matches(expectedRenamed) }
+ )
postSync { oneNetSocketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
postSync { allNetSocketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
@@ -520,10 +704,21 @@
@Test
fun testAddOrUpdateService_Updates() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags,
- context)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ MdnsAdvertiser(
+ thread.looper,
+ socketProvider,
+ cb,
+ mockDeps,
+ sharedlog,
+ flags,
+ context
+ )
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ ALL_NETWORKS_SERVICE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -531,41 +726,70 @@
val socketCb = socketCbCaptor.value
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
- verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) }, any())
+ verify(mockInterfaceAdvertiser1).addService(
+ eq(SERVICE_ID_1),
+ argThat { it.matches(ALL_NETWORKS_SERVICE) },
+ any()
+ )
val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
// Update with serviceId that is not registered yet should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
- updateOptions, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_2,
+ ALL_NETWORKS_SERVICE_SUBTYPE,
+ updateOptions,
+ TEST_CLIENT_UID_1
+ ) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with different NsdServiceInfo should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions,
- TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1_SUBTYPE,
+ updateOptions,
+ TEST_CLIENT_UID_1
+ ) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with same NsdServiceInfo but different subType should succeed
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
- updateOptions, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ ALL_NETWORKS_SERVICE_SUBTYPE,
+ updateOptions,
+ TEST_CLIENT_UID_1
+ ) }
verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
// Newly created MdnsInterfaceAdvertiser will get addService() call.
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
- verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }, any())
+ verify(mockInterfaceAdvertiser2).addService(
+ eq(SERVICE_ID_1),
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) },
+ any()
+ )
}
@Test
fun testAddOrUpdateService_customTtl_registeredSuccess() {
val advertiser = MdnsAdvertiser(
- thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ thread.looper,
+ socketProvider,
+ cb,
+ mockDeps,
+ sharedlog,
+ flags,
+ context
+ )
val updateOptions =
MdnsAdvertisingOptions.newBuilder().setTtl(Duration.ofSeconds(30)).build()
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- updateOptions, TEST_CLIENT_UID_1) }
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ ALL_NETWORKS_SERVICE,
+ updateOptions,
+ TEST_CLIENT_UID_1
+ ) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -578,11 +802,71 @@
fun testRemoveService_whenAllServiceRemoved_thenUpdateHostName() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
- verify(mockDeps, times(1)).generateHostname()
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+ verify(mockDeps, times(1)).generateHostname(anyBoolean())
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
postSync { advertiser.removeService(SERVICE_ID_1) }
- verify(mockDeps, times(2)).generateHostname()
+ verify(mockDeps, times(2)).generateHostname(anyBoolean())
+ }
+
+ private fun doHostnameGenerationTest(shortHostname: Boolean): Array<String> {
+ doCallRealMethod().`when`(mockDeps).generateHostname(anyBoolean())
+ val flags = MdnsFeatureFlags.newBuilder().setIsShortHostnamesEnabled(shortHostname).build()
+ val advertiser =
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ postSync { advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ ) }
+
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
+
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+ val hostnameCaptor = ArgumentCaptor.forClass(Array<String>::class.java)
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ any(),
+ hostnameCaptor.capture(),
+ any(),
+ any()
+ )
+ return hostnameCaptor.value
+ }
+
+ @Test
+ fun testShortHostnameGeneration() {
+ val hostname = doHostnameGenerationTest(shortHostname = true)
+ // Short hostnames are [8 uppercase letters or digits].local
+ assertEquals(2, hostname.size)
+ assertTrue(
+ Regex("Android_[A-Z0-9]{8}").matches(hostname[0]),
+ "Unexpected hostname: ${hostname.contentToString()}"
+ )
+ assertEquals("local", hostname[1])
+ }
+
+ @Test
+ fun testLongHostnameGeneration() {
+ val hostname = doHostnameGenerationTest(shortHostname = false)
+ // Long hostnames are Android_[32 lowercase hex characters].local
+ assertEquals(2, hostname.size)
+ assertTrue(
+ Regex("Android_[a-f0-9]{32}").matches(hostname[0]),
+ "Unexpected hostname: ${hostname.contentToString()}"
+ )
+ assertEquals("local", hostname[1])
}
private fun postSync(r: () -> Unit) {
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 ab2fb99..71a3274 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -442,14 +442,12 @@
// 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
@@ -469,7 +467,6 @@
// The function should be executed.
testableLooper.moveTimeForward(500L);
testableLooper.processAllMessages();
- assertTrue(future3.isDone());
assertTrue(future3.get(500L, TimeUnit.MILLISECONDS));
} finally {
testableLooper.destroy();
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 da0bc88..67f9d9c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -16,13 +16,13 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
import static com.android.server.connectivity.mdns.MdnsSearchOptions.ACTIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsServiceTypeClient.EVENT_START_QUERYTASK;
-import static com.android.server.connectivity.mdns.QueryTaskConfig.INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
-import static com.android.server.connectivity.mdns.QueryTaskConfig.MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS;
-import static com.android.server.connectivity.mdns.QueryTaskConfig.TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertArrayEquals;
@@ -566,21 +566,21 @@
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
- assertEquals(config.transactionId, 1);
+ assertEquals(config.getTransactionId(), 1);
// 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;
+ int oldTransactionId = config.getTransactionId();
config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
- assertEquals(config.transactionId, oldTransactionId + 1);
+ assertEquals(config.getTransactionId(), oldTransactionId + 1);
}
// This is the first query of a new burst. We will ask for unicast response.
- int oldTransactionId = config.transactionId;
+ int oldTransactionId = config.getTransactionId();
config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertTrue(config.expectUnicastResponse);
- assertEquals(config.transactionId, oldTransactionId + 1);
+ assertEquals(config.getTransactionId(), oldTransactionId + 1);
}
@Test
@@ -591,21 +591,21 @@
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
- assertEquals(config.transactionId, 1);
+ assertEquals(config.getTransactionId(), 1);
// 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;
+ int oldTransactionId = config.getTransactionId();
config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
- assertEquals(config.transactionId, oldTransactionId + 1);
+ assertEquals(config.getTransactionId(), oldTransactionId + 1);
}
// This is the first query of a new burst. We will NOT ask for unicast response.
- int oldTransactionId = config.transactionId;
+ int oldTransactionId = config.getTransactionId();
config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
- assertEquals(config.transactionId, oldTransactionId + 1);
+ assertEquals(config.getTransactionId(), oldTransactionId + 1);
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
index 1cc9985..f763bae 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
@@ -610,6 +610,7 @@
@Test
public void testSocketCreatedForMulticastInterface() throws Exception {
+ doReturn(true).when(mTestNetworkIfaceWrapper).isPointToPoint();
doReturn(true).when(mTestNetworkIfaceWrapper).supportsMulticast();
startMonitoringSockets();
@@ -621,18 +622,6 @@
}
@Test
- public void testNoSocketCreatedForPTPInterface() throws Exception {
- doReturn(true).when(mTestNetworkIfaceWrapper).isPointToPoint();
- startMonitoringSockets();
-
- final TestSocketCallback testCallback = new TestSocketCallback();
- runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
-
- postNetworkAvailable(TRANSPORT_BLUETOOTH);
- testCallback.expectedNoCallback();
- }
-
- @Test
public void testNoSocketCreatedForVPNInterface() throws Exception {
// VPN interfaces generally also have IFF_POINTOPOINT, but even if they don't, they should
// not be included even with TRANSPORT_WIFI.
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
index 8155fd0..06cb7ee 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -21,7 +21,10 @@
import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
+import android.net.InetAddresses
+import android.net.LinkProperties
import android.os.Build
+import android.os.Build.VERSION_CODES
import androidx.test.filters.SmallTest
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
@@ -33,11 +36,32 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+internal val LOCAL_DNS = InetAddresses.parseNumericAddress("224.0.1.2")
+internal val NON_LOCAL_DNS = InetAddresses.parseNumericAddress("76.76.75.75")
+
+private const val IFNAME_1 = "wlan1"
+private const val IFNAME_2 = "wlan2"
+private const val PORT_53 = 53
+private const val PROTOCOL_TCP = 6
+private const val PROTOCOL_UDP = 17
+
+private val lpWithNoLocalDns = LinkProperties().apply {
+ addDnsServer(NON_LOCAL_DNS)
+ interfaceName = IFNAME_1
+}
+
+private val lpWithLocalDns = LinkProperties().apply {
+ addDnsServer(LOCAL_DNS)
+ interfaceName = IFNAME_2
+}
+
@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@@ -69,6 +93,81 @@
}
}
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ fun testLocalPrefixesUpdatedInBpfMap() {
+ // Connect Wi-Fi network with non-local dns.
+ val wifiAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+ wifiAgent.connect()
+
+ // Verify that block rule is added to BpfMap for local prefixes.
+ verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(any(), eq(IFNAME_1),
+ any(), eq(0), eq(0), eq(false))
+
+ wifiAgent.disconnect()
+ val cellAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+ cellAgent.connect()
+
+ // Verify that block rule is removed from BpfMap for local prefixes.
+ verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(any(), eq(IFNAME_1),
+ any(), eq(0), eq(0))
+
+ cellAgent.disconnect()
+ }
+
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ fun testLocalDnsNotUpdatedInBpfMap() {
+ // Connect Wi-Fi network with non-local dns.
+ val wifiAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+ wifiAgent.connect()
+
+ // Verify that No allow rule is added to BpfMap since there is no local dns.
+ verify(bpfNetMaps, never()).addLocalNetAccess(any(), any(), any(), any(), any(),
+ eq(true))
+
+ wifiAgent.disconnect()
+ val cellAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+ cellAgent.connect()
+
+ // Verify that No allow rule from port 53 is removed on network change
+ // because no dns was added
+ verify(bpfNetMaps, never()).removeLocalNetAccess(eq(192), eq(IFNAME_1),
+ eq(NON_LOCAL_DNS), any(), eq(PORT_53))
+
+ cellAgent.disconnect()
+ }
+
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Test
+ fun testLocalDnsUpdatedInBpfMap() {
+ // Connect Wi-Fi network with one local Dns.
+ val wifiAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+ wifiAgent.connect()
+
+ // Verify that allow rule is added to BpfMap for local dns at port 53,
+ // for TCP(=6) protocol
+ verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(eq(192), eq(IFNAME_2),
+ eq(LOCAL_DNS), eq(PROTOCOL_TCP), eq(PORT_53), eq(true))
+ // And for UDP(=17) protocol
+ verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(eq(192), eq(IFNAME_2),
+ eq(LOCAL_DNS), eq(PROTOCOL_UDP), eq(PORT_53), eq(true))
+
+ wifiAgent.disconnect()
+ val cellAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+ cellAgent.connect()
+
+ // Verify that allow rule is removed for local dns on network change,
+ // for TCP(=6) protocol
+ verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(eq(192), eq(IFNAME_2),
+ eq(LOCAL_DNS), eq(PROTOCOL_TCP), eq(PORT_53))
+ // And for UDP(=17) protocol
+ verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(eq(192), eq(IFNAME_2),
+ eq(LOCAL_DNS), eq(PROTOCOL_UDP), eq(PORT_53))
+
+ cellAgent.disconnect()
+ }
+
private fun mockDataSaverStatus(status: Int) {
doReturn(status).`when`(context.networkPolicyManager).getRestrictBackgroundStatus(anyInt())
// While the production code dispatches the intent on the handler thread,
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
index a7083dc..b179aac 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
@@ -150,7 +150,7 @@
// EXPIRE_LEGACY_REQUEST (=8) is only used in ConnectivityManager and not included.
// CALLBACK_TRANSITIVE_CALLS_ONLY (=0) is not a callback so not included either.
assertEquals(
- "PRECHK|AVAIL|LOSING|LOST|UNAVAIL|NC|LP|SUSP|RESUME|BLK|LOCALINF|0x7fffe101",
+ "PRECHK|AVAIL|LOSING|LOST|UNAVAIL|NC|LP|SUSP|RESUME|BLK|LOCALINF|RES|0x7fffc101",
ConnectivityService.declaredMethodsFlagsToString(0x7fff_ffff)
)
// The toString method and the assertion above need to be updated if constants are added
@@ -158,7 +158,7 @@
Modifier.isStatic(it.modifiers) && Modifier.isFinal(it.modifiers) &&
it.name.startsWith("CALLBACK_")
}
- assertEquals(12, constants.size)
+ assertEquals(13, constants.size)
}
}
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/CSL2capProviderTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
new file mode 100644
index 0000000..babcba9
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothServerSocket
+import android.bluetooth.BluetoothSocket
+import android.net.INetworkMonitor
+import android.net.INetworkMonitorCallbacks
+import android.net.IpPrefix
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.ROLE_CLIENT
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.MacAddress
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkRequest
+import android.net.NetworkSpecifier
+import android.net.RouteInfo
+import android.os.Build
+import android.os.HandlerThread
+import android.os.ParcelFileDescriptor
+import com.android.server.net.L2capNetwork.L2capIpClient
+import com.android.server.net.L2capPacketForwarder
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.anyNetwork
+import com.android.testutils.waitForIdle
+import java.io.IOException
+import java.util.Optional
+import java.util.concurrent.LinkedBlockingQueue
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+
+private const val PSM = 0x85
+private val REMOTE_MAC = byteArrayOf(1, 2, 3, 4, 5, 6)
+private val REQUEST = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .build()
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+class CSL2capProviderTest : CSTest() {
+ private val networkMonitor = mock<INetworkMonitor>()
+
+ private val btAdapter = mock<BluetoothAdapter>()
+ private val btDevice = mock<BluetoothDevice>()
+ private val btServerSocket = mock<BluetoothServerSocket>()
+ private val btSocket = mock<BluetoothSocket>()
+ private val tunInterface = mock<ParcelFileDescriptor>()
+ private val l2capIpClient = mock<L2capIpClient>()
+ private val packetForwarder = mock<L2capPacketForwarder>()
+ private val providerDeps = mock<L2capNetworkProvider.Dependencies>()
+
+ // BlockingQueue does not support put(null) operations, as null is used as an internal sentinel
+ // value. Therefore, use Optional<BluetoothSocket> where an empty optional signals the
+ // BluetoothServerSocket#close() operation.
+ private val acceptQueue = LinkedBlockingQueue<Optional<BluetoothSocket>>()
+
+ private val handlerThread = HandlerThread("CSL2capProviderTest thread").apply { start() }
+ private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
+
+ // Requires Dependencies mock to be setup before creation.
+ private lateinit var provider: L2capNetworkProvider
+
+ @Before
+ fun innerSetUp() {
+ doReturn(btAdapter).`when`(bluetoothManager).getAdapter()
+ doReturn(btServerSocket).`when`(btAdapter).listenUsingInsecureL2capChannel()
+ doReturn(PSM).`when`(btServerSocket).getPsm()
+ doReturn(btDevice).`when`(btAdapter).getRemoteDevice(eq(REMOTE_MAC))
+ doReturn(btSocket).`when`(btDevice).createInsecureL2capChannel(eq(PSM))
+
+ doAnswer {
+ val sock = acceptQueue.take()
+ if (sock == null || !sock.isPresent()) throw IOException()
+ sock.get()
+ }.`when`(btServerSocket).accept()
+
+ doAnswer {
+ acceptQueue.put(Optional.empty())
+ }.`when`(btServerSocket).close()
+
+ doReturn(handlerThread).`when`(providerDeps).getHandlerThread()
+ doReturn(tunInterface).`when`(providerDeps).createTunInterface(any())
+ doReturn(packetForwarder).`when`(providerDeps)
+ .createL2capPacketForwarder(any(), any(), any(), any(), any())
+ doReturn(l2capIpClient).`when`(providerDeps).createL2capIpClient(any(), any(), any())
+
+ val lp = LinkProperties()
+ val ifname = "l2cap-tun0"
+ lp.setInterfaceName(ifname)
+ lp.addLinkAddress(LinkAddress("fe80::1/64"))
+ lp.addRoute(RouteInfo(IpPrefix("fe80::/64"), null /* nextHop */, ifname))
+ doReturn(lp).`when`(l2capIpClient).start()
+
+ // Note: In order to properly register a NetworkAgent, a NetworkMonitor must be created for
+ // the agent. CSAgentWrapper already does some of this, but requires adding additional
+ // Dependencies to the production code. Create a mocked NM inside this test instead.
+ doAnswer { i ->
+ val cb = i.arguments[2] as INetworkMonitorCallbacks
+ cb.onNetworkMonitorCreated(networkMonitor)
+ }.`when`(networkStack).makeNetworkMonitor(
+ any() /* network */,
+ isNull() /* name */,
+ any() /* callbacks */
+ )
+
+ provider = L2capNetworkProvider(providerDeps, context)
+ provider.start()
+ }
+
+ @After
+ fun innerTearDown() {
+ // Unregistering a callback which has previously been unregistered by virtue of receiving
+ // onUnavailable is a noop.
+ registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
+ // Wait for CS handler idle, meaning the unregisterNetworkCallback has been processed and
+ // L2capNetworkProvider has been notified.
+ waitForIdle()
+
+ // While quitSafely() effectively waits for idle, it is not enough, because the tear down
+ // path itself posts on the handler thread. This means that waitForIdle() needs to run
+ // twice. The first time, to ensure all active threads have been joined, and the second time
+ // to run all associated clean up actions.
+ handlerThread.waitForIdle(HANDLER_TIMEOUT_MS)
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ private fun reserveNetwork(nr: NetworkRequest) = TestableNetworkCallback().also {
+ cm.reserveNetwork(nr, csHandler, it)
+ registeredCallbacks.add(it)
+ }
+
+ private fun requestNetwork(nr: NetworkRequest) = TestableNetworkCallback().also {
+ cm.requestNetwork(nr, it, csHandler)
+ registeredCallbacks.add(it)
+ }
+
+ private fun NetworkRequest.copyWithSpecifier(specifier: NetworkSpecifier): NetworkRequest {
+ // Note: NetworkRequest.Builder(NetworkRequest) *does not* perform a defensive copy but
+ // changes the underlying request.
+ return NetworkRequest.Builder(NetworkRequest(this))
+ .setNetworkSpecifier(specifier)
+ .build()
+ }
+
+ @Test
+ fun testReservation() {
+ val l2capServerSpecifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ val l2capReservation = REQUEST.copyWithSpecifier(l2capServerSpecifier)
+ val reservationCb = reserveNetwork(l2capReservation)
+
+ val reservedCaps = reservationCb.expect<Reserved>().caps
+ val reservedSpec = reservedCaps.networkSpecifier as L2capNetworkSpecifier
+
+ assertEquals(PSM, reservedSpec.getPsm())
+ assertEquals(HEADER_COMPRESSION_6LOWPAN, reservedSpec.headerCompression)
+ assertNull(reservedSpec.remoteAddress)
+
+ reservationCb.assertNoCallback()
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithoutSpecifier() {
+ reserveNetwork(REQUEST).assertNoCallback()
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithCorrectSpecifier() {
+ var specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ var nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).expect<Reserved>()
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .build()
+ nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).expect<Reserved>()
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithIncorrectSpecifier() {
+ var specifier = L2capNetworkSpecifier.Builder().build()
+ var nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).assertNoCallback()
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .build()
+ nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).assertNoCallback()
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setPsm(0x81)
+ .build()
+ nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).assertNoCallback()
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .build()
+ nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).assertNoCallback()
+ }
+
+ @Test
+ fun testBluetoothException_listenUsingInsecureL2capChannelThrows() {
+ doThrow(IOException()).`when`(btAdapter).listenUsingInsecureL2capChannel()
+ var specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ var nr = REQUEST.copyWithSpecifier(specifier)
+ reserveNetwork(nr).expect<Unavailable>()
+
+ doReturn(btServerSocket).`when`(btAdapter).listenUsingInsecureL2capChannel()
+ reserveNetwork(nr).expect<Reserved>()
+ }
+
+ @Test
+ fun testBluetoothException_acceptThrows() {
+ doThrow(IOException()).`when`(btServerSocket).accept()
+ var specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ var nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = reserveNetwork(nr)
+ cb.expect<Reserved>()
+ cb.expect<Unavailable>()
+
+ // BluetoothServerSocket#close() puts Optional.empty() on the acceptQueue.
+ acceptQueue.clear()
+ doAnswer {
+ val sock = acceptQueue.take()
+ assertFalse(sock.isPresent())
+ throw IOException() // to indicate the socket was closed.
+ }.`when`(btServerSocket).accept()
+ val cb2 = reserveNetwork(nr)
+ cb2.expect<Reserved>()
+ cb2.assertNoCallback()
+ }
+
+ @Test
+ fun testServerNetwork() {
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ val nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = reserveNetwork(nr)
+ cb.expect<Reserved>()
+
+ // Unblock BluetoothServerSocket#accept()
+ doReturn(true).`when`(btSocket).isConnected()
+ acceptQueue.put(Optional.of(btSocket))
+
+ cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+ cb.assertNoCallback()
+ // Verify that packet forwarding was started.
+ // TODO: stop mocking L2capPacketForwarder.
+ verify(providerDeps).createL2capPacketForwarder(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun testBluetoothException_createInsecureL2capChannelThrows() {
+ doThrow(IOException()).`when`(btDevice).createInsecureL2capChannel(any())
+
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ val nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = requestNetwork(nr)
+
+ cb.expect<Unavailable>()
+ }
+
+ @Test
+ fun testBluetoothException_bluetoothSocketConnectThrows() {
+ doThrow(IOException()).`when`(btSocket).connect()
+
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ val nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = requestNetwork(nr)
+
+ cb.expect<Unavailable>()
+ }
+
+ @Test
+ fun testClientNetwork() {
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ val nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = requestNetwork(nr)
+ cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+ }
+
+ @Test
+ fun testClientNetwork_headerCompressionMismatch() {
+ var specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ var nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = requestNetwork(nr)
+ cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ nr = REQUEST.copyWithSpecifier(specifier)
+ val cb2 = requestNetwork(nr)
+ cb2.expect<Unavailable>()
+ }
+
+ @Test
+ fun testClientNetwork_multipleRequests() {
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+ .setPsm(PSM)
+ .build()
+ val nr = REQUEST.copyWithSpecifier(specifier)
+ val cb = requestNetwork(nr)
+ cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+
+ val cb2 = requestNetwork(nr)
+ cb2.expectAvailableCallbacks(anyNetwork(), validated = false)
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index cb98454..16a30aa 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -55,25 +55,28 @@
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.R)
-class CSLocalAgentCreationTests(
- private val sdkLevel: Int,
- private val isTv: Boolean,
- private val addLocalNetCapToRequest: Boolean
-) : CSTest() {
+class CSLocalAgentCreationTests : CSTest() {
+ @Parameterized.Parameter(0) lateinit var params: TestParams
+
+ data class TestParams(
+ val sdkLevel: Int,
+ val isTv: Boolean = false,
+ val addLocalNetCapToRequest: Boolean = true)
+
companion object {
@JvmStatic
@Parameterized.Parameters
fun arguments() = listOf(
- arrayOf(VERSION_V, false /* isTv */, true /* addLocalNetCapToRequest */),
- arrayOf(VERSION_V, false /* isTv */, false /* addLocalNetCapToRequest */),
- arrayOf(VERSION_V, true /* isTv */, true /* addLocalNetCapToRequest */),
- arrayOf(VERSION_V, true /* isTv */, false /* addLocalNetCapToRequest */),
- arrayOf(VERSION_U, false /* isTv */, true /* addLocalNetCapToRequest */),
- arrayOf(VERSION_U, false /* isTv */, false /* addLocalNetCapToRequest */),
- arrayOf(VERSION_U, true /* isTv */, true /* addLocalNetCapToRequest */),
- arrayOf(VERSION_U, true /* isTv */, false /* addLocalNetCapToRequest */),
- arrayOf(VERSION_T, false /* isTv */, false /* addLocalNetCapToRequest */),
- arrayOf(VERSION_T, true /* isTv */, false /* addLocalNetCapToRequest */),
+ TestParams(VERSION_V, isTv = false, addLocalNetCapToRequest = true),
+ TestParams(VERSION_V, isTv = false, addLocalNetCapToRequest = false),
+ TestParams(VERSION_V, isTv = true, addLocalNetCapToRequest = true),
+ TestParams(VERSION_V, isTv = true, addLocalNetCapToRequest = false),
+ TestParams(VERSION_U, isTv = false, addLocalNetCapToRequest = true),
+ TestParams(VERSION_U, isTv = false, addLocalNetCapToRequest = false),
+ TestParams(VERSION_U, isTv = true, addLocalNetCapToRequest = true),
+ TestParams(VERSION_U, isTv = true, addLocalNetCapToRequest = false),
+ TestParams(VERSION_T, isTv = false, addLocalNetCapToRequest = false),
+ TestParams(VERSION_T, isTv = true, addLocalNetCapToRequest = false),
)
}
@@ -84,11 +87,11 @@
@Test
fun testLocalAgents() {
val netdInOrder = inOrder(netd)
- deps.setBuildSdk(sdkLevel)
- doReturn(isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
+ deps.setBuildSdk(params.sdkLevel)
+ doReturn(params.isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
val allNetworksCb = TestableNetworkCallback()
val request = NetworkRequest.Builder()
- if (addLocalNetCapToRequest) {
+ if (params.addLocalNetCapToRequest) {
request.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
}
cm.registerNetworkCallback(request.build(), allNetworksCb)
@@ -96,7 +99,8 @@
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
addCapability(NET_CAPABILITY_LOCAL_NETWORK)
}.build()
- val localAgent = if (sdkLevel >= VERSION_V || sdkLevel == VERSION_U && isTv) {
+ val localAgent = if (params.sdkLevel >= VERSION_V
+ || params.sdkLevel == VERSION_U && params.isTv) {
Agent(nc = ncTemplate, score = keepConnectedScore(), lnc = defaultLnc())
} else {
assertFailsWith<IllegalArgumentException> { Agent(nc = ncTemplate, lnc = defaultLnc()) }
@@ -106,7 +110,7 @@
localAgent.connect()
netdInOrder.verify(netd).networkCreate(
makeNativeNetworkConfigLocal(localAgent.network.netId, INetd.PERMISSION_NONE))
- if (addLocalNetCapToRequest) {
+ if (params.addLocalNetCapToRequest) {
assertEquals(localAgent.network, allNetworksCb.expect<Available>().network)
} else {
allNetworksCb.assertNoCallback(NO_CALLBACK_TIMEOUT_MS)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index 83fff87..3583f84 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -54,9 +54,7 @@
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.timeout
-import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
private const val TIMEOUT_MS = 200L
private const val MEDIUM_TIMEOUT_MS = 1_000L
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
new file mode 100644
index 0000000..5bf6e04
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
@@ -0,0 +1,516 @@
+/*
+ * 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
+
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+private const val LONG_TIMEOUT_MS = 5_000
+private const val PREFIX_LENGTH_IPV4 = 32 + 96
+private const val PREFIX_LENGTH_IPV6 = 32
+private const val WIFI_IFNAME = "wlan0"
+private const val WIFI_IFNAME_2 = "wlan1"
+
+private val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+
+private fun lp(iface: String, vararg linkAddresses: LinkAddress) = LinkProperties().apply {
+ interfaceName = iface
+ for (linkAddress in linkAddresses) {
+ addLinkAddress(linkAddress)
+ }
+}
+
+private fun nr(transport: Int) = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(transport).apply {
+ if (transport != TRANSPORT_VPN) {
+ addCapability(NET_CAPABILITY_NOT_VPN)
+ }
+ }.build()
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+class CSLocalNetworkProtectionTest : CSTest() {
+ private val LOCAL_IPV6_IP_ADDRESS_PREFIX = IpPrefix("fe80::1cf1:35ff:fe8c:db87/64")
+ private val LOCAL_IPV6_LINK_ADDRESS = LinkAddress(
+ LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress(),
+ LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()
+ )
+
+ private val LOCAL_IPV4_IP_ADDRESS_PREFIX_1 = IpPrefix("10.0.0.184/24")
+ private val LOCAL_IPV4_LINK_ADDRRESS_1 =
+ LinkAddress(
+ LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getAddress(),
+ LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getPrefixLength()
+ )
+
+ private val LOCAL_IPV4_IP_ADDRESS_PREFIX_2 = IpPrefix("10.0.255.184/24")
+ private val LOCAL_IPV4_LINK_ADDRRESS_2 =
+ LinkAddress(
+ LOCAL_IPV4_IP_ADDRESS_PREFIX_2.getAddress(),
+ LOCAL_IPV4_IP_ADDRESS_PREFIX_2.getPrefixLength()
+ )
+
+ @Test
+ fun testNetworkWithIPv6LocalAddress_AddressAddedToBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ // Connecting to network with IPv6 local address in LinkProperties
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ }
+
+ @Test
+ fun testNetworkWithIPv4LocalAddress_AddressAddedToBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ }
+
+ @Test
+ fun testChangeLinkPropertiesWithDifferentLinkAddresses_AddressReplacedInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+
+ // Updating Link Property from IPv6 in Link Address to IPv4 in Link Address
+ val wifiLp2 = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+ wifiAgent.sendLinkProperties(wifiLp2)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // Verifying IPv6 address should be removed from local_net_access map
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0)
+ )
+ }
+
+ @Test
+ fun testStackedLinkPropertiesWithDifferentLinkAddresses_AddressAddedInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+ // Adding stacked link
+ wifiLp.addStackedLink(wifiLp2)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+
+ // Multicast and Broadcast address should always be populated on stacked link
+ // in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated as part of stacked link
+ // in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME_2),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // As both addresses are in stacked links, so no address should be removed from the map.
+ verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun testRemovingStackedLinkProperties_AddressRemovedInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+ // populating stacked link
+ wifiLp.addStackedLink(wifiLp2)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+
+ // Multicast and Broadcast address should always be populated on stacked link
+ // in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated as part of stacked link
+ // in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME_2),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // As both addresses are in stacked links, so no address should be removed from the map.
+ verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+
+ // replacing link properties without stacked links
+ val wifiLp_3 = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ wifiAgent.sendLinkProperties(wifiLp_3)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // As both stacked links is removed, 10.0.0.0/8 should be removed from local_net_access map.
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME_2),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0)
+ )
+ }
+
+ @Test
+ fun testChangeLinkPropertiesWithLinkAddressesInSameRange_AddressIntactInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+
+ // Updating Link Property from one IPv4 to another IPv4 within same range(10.0.0.0/8)
+ val wifiLp2 = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_2)
+ wifiAgent.sendLinkProperties(wifiLp2)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // As both addresses below to same range, so no address should be removed from the map.
+ verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun testChangeLinkPropertiesWithDifferentInterface_AddressReplacedInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+
+ // Updating Link Property by changing interface name which has IPv4 instead of IPv6
+ val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+ wifiAgent.sendLinkProperties(wifiLp2)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // Multicast and Broadcast address should be populated in local_net_access map for
+ // new interface
+ verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME_2),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // Multicast and Broadcast address should be removed in local_net_access map for
+ // old interface
+ verifyRemovalOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be removed from local_net_access map
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0)
+ )
+ }
+
+ @Test
+ fun testAddingAnotherNetwork_AllAddressesAddedInBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+
+ // Adding another network with LinkProperty having IPv4 in LinkAddress
+ val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+ val wifiAgent2 = Agent(nc = wifiNc, lp = wifiLp2)
+ wifiAgent2.connect()
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+ // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 8),
+ eq(WIFI_IFNAME_2),
+ eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ // Verifying nothing should be removed from local_net_access map
+ verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun testDestroyingNetwork_AddressesRemovedFromBpfMap() {
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+
+ val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Multicast and Broadcast address should always be populated in local_net_access map
+ verifyPopulationOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be populated in local_net_access map
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq( PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+
+ // Unregistering the network
+ wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+ cb.expect<Lost>(wifiAgent.network)
+
+ // Multicast and Broadcast address should be removed in local_net_access map for
+ // old interface
+ verifyRemovalOfMulticastAndBroadcastAddress()
+ // Verifying IPv6 address should be removed from local_net_access map
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+ eq(WIFI_IFNAME),
+ eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+ eq(0),
+ eq(0)
+ )
+ }
+
+ // Verify if multicast and broadcast addresses have been added using addLocalNetAccess
+ fun verifyPopulationOfMulticastAndBroadcastAddress(
+ interfaceName: String = WIFI_IFNAME
+ ) {
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 4),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("224.0.0.0")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + 8),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("ff00::")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ verify(bpfNetMaps).addLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 32),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("255.255.255.255")),
+ eq(0),
+ eq(0),
+ eq(false)
+ )
+ }
+
+ // Verify if multicast and broadcast addresses have been removed using removeLocalNetAccess
+ fun verifyRemovalOfMulticastAndBroadcastAddress(
+ interfaceName: String = WIFI_IFNAME
+ ) {
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 4),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("224.0.0.0")),
+ eq(0),
+ eq(0)
+ )
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV6 + 8),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("ff00::")),
+ eq(0),
+ eq(0)
+ )
+ verify(bpfNetMaps).removeLocalNetAccess(
+ eq(PREFIX_LENGTH_IPV4 + 32),
+ eq(interfaceName),
+ eq(InetAddresses.parseNumericAddress("255.255.255.255")),
+ eq(0),
+ eq(0)
+ )
+ }
+}
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/CSNetworkReservationTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
new file mode 100644
index 0000000..e698930
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P
+import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkOfferCallback
+import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.OnNetworkNeeded
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val ETHERNET_SCORE = NetworkScore.Builder().build()
+private val ETHERNET_CAPS = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+ reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+
+private const val TIMEOUT_MS = 5_000L
+private const val NO_CB_TIMEOUT_MS = 200L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkReservationTest : CSTest() {
+ private lateinit var provider: NetworkProvider
+ private val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+
+ @Before
+ fun subclassSetUp() {
+ provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
+ cm.registerNetworkProvider(provider)
+
+ // register a blanket offer for use in tests.
+ provider.registerNetworkOffer(ETHERNET_SCORE, BLANKET_CAPS, blanketOffer)
+ }
+
+ fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
+ it.reservationId = resId
+ }
+
+ fun NetworkProvider.registerNetworkOffer(
+ score: NetworkScore,
+ caps: NetworkCapabilities,
+ cb: NetworkOfferCallback
+ ) {
+ registerNetworkOffer(score, caps, {r -> r.run()}, cb)
+ }
+
+ @Test
+ fun testReservationRequest() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ // bring up reserved reservation offer
+ val reservedOfferCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedOfferCaps, reservedOfferCb)
+
+ // validate onReserved was sent to the app
+ val onReservedCaps = cb.expect<Reserved>().caps
+ assertEquals(reservedOfferCaps, onReservedCaps)
+
+ // validate the reservation matches the reserved offer.
+ reservedOfferCb.expectOnNetworkNeeded(reservedOfferCaps)
+
+ // reserved offer goes away
+ provider.unregisterNetworkOffer(reservedOfferCb)
+ cb.expect<Unavailable>()
+ }
+
+ fun TestableNetworkOfferCallback.expectNoCallbackWhere(
+ predicate: (TestableNetworkOfferCallback.CallbackEntry) -> Boolean
+ ) {
+ val event = history.poll(NO_CB_TIMEOUT_MS) { predicate(it) }
+ assertNull(event)
+ }
+
+ @Test
+ fun testReservationRequest_notDeliveredToRegularOffer() {
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, {r -> r.run()}, offerCb)
+
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ // validate the offer does not receive onNetworkNeeded for reservation request
+ offerCb.expectNoCallbackWhere {
+ it is OnNetworkNeeded && it.request.type == NetworkRequest.Type.RESERVATION
+ }
+ }
+
+ @Test
+ fun testReservedOffer_preventReservationIdUpdate() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ // bring up reserved offer
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+ cb.expect<Reserved>()
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+ // try to update the offer's reservationId by reusing the same callback object.
+ // first file a new request to try and match the offer later.
+ val cb2 = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb2)
+
+ val reservationReq2 = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId2 = reservationReq2.networkCapabilities.reservationId
+
+ // try to update the offer's reservationId to an existing reservationId.
+ val updatedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId2)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+ // validate the original offer disappeared.
+ cb.expect<Unavailable>()
+ // validate the new offer was rejected by CS.
+ reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+ // validate cb2 never sees onReserved().
+ cb2.assertNoCallback()
+ }
+
+ @Test
+ fun testReservedOffer_capabilitiesCannotBeUpdated() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+ cb.expect<Reserved>()
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+ // update reserved offer capabilities
+ val updatedCaps = NetworkCapabilities(reservedCaps).addCapability(NET_CAPABILITY_WIFI_P2P)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+ cb.expect<Unavailable>()
+ reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testBlanketOffer_updateAllowed() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+ blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS)
+
+ val updatedCaps = NetworkCapabilities(BLANKET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, blanketOffer)
+ blanketOffer.assertNoCallback()
+
+ // Note: NetworkRequest.Builder(NetworkRequest) *does not* perform a defensive copy but
+ // changes the underlying request.
+ val p2pRequest = NetworkRequest.Builder(NetworkRequest(ETHERNET_REQUEST))
+ .addCapability(NET_CAPABILITY_WIFI_P2P)
+ .build()
+ cm.reserveNetwork(p2pRequest, csHandler, cb)
+ blanketOffer.expectOnNetworkNeeded(updatedCaps)
+ }
+
+ @Test
+ fun testReservationOffer_onlyAllowSingleOffer() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ val caps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ provider.registerNetworkOffer(ETHERNET_SCORE, caps, offerCb)
+ offerCb.expectOnNetworkNeeded(caps)
+ cb.expect<Reserved>()
+
+ val newOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, caps, newOfferCb)
+ newOfferCb.assertNoCallback()
+ cb.assertNoCallback()
+
+ // File a regular request and validate only the old offer gets onNetworkNeeded.
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ offerCb.expectOnNetworkNeeded(caps)
+ newOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testReservationOffer_updateScore() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ cb.expect<Reserved>()
+
+ // update reserved offer capabilities
+ val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+ provider.registerNetworkOffer(newScore, reservedCaps, reservedOfferCb)
+ cb.assertNoCallback()
+
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testReservationOffer_regularOfferCanBeUpdated() {
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, offerCb)
+
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb, csHandler)
+ offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+ offerCb.assertNoCallback()
+
+ val updatedCaps = NetworkCapabilities(ETHERNET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+ val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+ provider.registerNetworkOffer(newScore, updatedCaps, offerCb)
+ offerCb.assertNoCallback()
+
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+ offerCb.assertNoCallback()
+ }
+}
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 1f5ee32..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)
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 46c25d2..557bfd6 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -18,6 +18,7 @@
import android.app.AlarmManager
import android.app.AppOpsManager
+import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -71,6 +72,7 @@
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
import com.android.server.connectivity.NetworkRequestStateStatsMetrics
+import com.android.server.connectivity.PermissionMonitor
import com.android.server.connectivity.ProxyTracker
import com.android.server.connectivity.SatelliteAccessController
import com.android.testutils.visibleOnHandlerThread
@@ -209,12 +211,14 @@
doReturn(true).`when`(it).isDataCapable()
}
val subscriptionManager = mock<SubscriptionManager>()
+ val bluetoothManager = mock<BluetoothManager>()
val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
val satelliteAccessController = mock<SatelliteAccessController>()
val destroySocketsWrapper = mock<DestroySocketsWrapper>()
val deps = CSDeps()
+ val permDeps = PermDeps()
// Initializations that start threads are done from setUp to avoid thread leak
lateinit var alarmHandlerThread: HandlerThread
@@ -251,7 +255,9 @@
alarmHandlerThread = HandlerThread("TestAlarmManager").also { it.start() }
alarmManager = makeMockAlarmManager(alarmHandlerThread)
- service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
+ service = makeConnectivityService(context, netd, deps, permDeps).also {
+ it.systemReadyInternal()
+ }
cm = ConnectivityManager(context, service)
// csHandler initialization must be after makeConnectivityService since ConnectivityService
// constructor starts csHandlerThread
@@ -341,6 +347,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) =
@@ -381,6 +399,12 @@
// Call mocked destroyLiveTcpSocketsByOwnerUids so that test can verify this method call
destroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(ownerUids)
}
+
+ override fun makeL2capNetworkProvider(context: Context) = null
+ }
+
+ inner class PermDeps : PermissionMonitor.Dependencies() {
+ override fun shouldEnforceLocalNetRestrictions(uid: Int) = false
}
inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
@@ -491,6 +515,7 @@
Context.BATTERY_STATS_SERVICE -> batteryManager
Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
Context.APP_OPS_SERVICE -> appOpsManager
+ Context.BLUETOOTH_SERVICE -> bluetoothManager
else -> super.getSystemService(serviceName)
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
index 8ff790c..a53d430 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -23,6 +23,7 @@
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PackageManager.FEATURE_BLUETOOTH
+import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
import android.content.pm.PackageManager.FEATURE_ETHERNET
import android.content.pm.PackageManager.FEATURE_WIFI
import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT
@@ -53,6 +54,7 @@
import com.android.modules.utils.build.SdkLevel
import com.android.server.ConnectivityService.Dependencies
import com.android.server.connectivity.ConnectivityResources
+import com.android.server.connectivity.PermissionMonitor
import kotlin.test.fail
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
@@ -103,7 +105,13 @@
}
internal fun makeMockPackageManager(realContext: Context) = mock<PackageManager>().also { pm ->
- val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET)
+ val supported = listOf(
+ FEATURE_WIFI,
+ FEATURE_WIFI_DIRECT,
+ FEATURE_BLUETOOTH,
+ FEATURE_BLUETOOTH_LE,
+ FEATURE_ETHERNET
+ )
doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) })
val myPackageName = realContext.packageName
val myPackageInfo = realContext.packageManager.getPackageInfo(myPackageName,
@@ -185,13 +193,14 @@
private val TEST_LINGER_DELAY_MS = 400
private val TEST_NASCENT_DELAY_MS = 300
-internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies) =
+internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies,
+ mPermDeps: PermissionMonitor.Dependencies) =
ConnectivityService(
context,
mock<IDnsResolver>(),
mock<IpConnectivityLog>(),
netd,
- deps).also {
+ deps, mPermDeps).also {
it.mLingerDelayMs = TEST_LINGER_DELAY_MS
it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index e6aba22..533bbf8 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -79,6 +79,7 @@
initMockResources();
doReturn(false).when(mFactory).updateInterfaceLinkState(anyString(), anyBoolean());
doReturn(new String[0]).when(mNetd).interfaceGetList();
+ doReturn(new String[0]).when(mFactory).getAvailableInterfaces(anyBoolean());
mHandlerThread = new HandlerThread(THREAD_NAME);
mHandlerThread.start();
tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd,
@@ -166,9 +167,10 @@
EthernetTracker.parseStaticIpConfiguration(configAsString));
}
- private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearAll) {
+ private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearDefaults) {
final NetworkCapabilities.Builder builder =
- clearAll ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ clearDefaults
+ ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
: new NetworkCapabilities.Builder();
return builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
@@ -176,21 +178,20 @@
}
/**
- * Test: Attempt to create a capabilties with various valid sets of capabilities/transports
+ * Test: Attempt to create a capabilities with various valid sets of capabilities/transports
*/
@Test
public void createNetworkCapabilities() {
-
// Particularly common expected results
- NetworkCapabilities defaultEthernetCleared =
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ NetworkCapabilities defaultCapabilities =
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build();
NetworkCapabilities ethernetClearedWithCommonCaps =
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(true /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
@@ -200,89 +201,71 @@
.addCapability(15)
.build();
- // Empty capabilities and transports lists with a "please clear defaults" should
- // yield an empty capabilities set with TRANPORT_ETHERNET
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "");
+ // Empty capabilities and transports should return the default capabilities set
+ // with TRANSPORT_ETHERNET
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "");
- // Empty capabilities and transports without the clear defaults flag should return the
- // default capabilities set with TRANSPORT_ETHERNET
- assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(false /* clearAll */)
- .setLinkUpstreamBandwidthKbps(100000)
- .setLinkDownstreamBandwidthKbps(100000)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .build(),
- false, "", "");
-
- // A list of capabilities without the clear defaults flag should return the default
- // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET
- assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(false /* clearAll */)
- .setLinkUpstreamBandwidthKbps(100000)
- .setLinkDownstreamBandwidthKbps(100000)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .addCapability(11)
- .addCapability(12)
- .build(),
- false, "11,12", "");
-
- // Adding a list of capabilities with a clear defaults will leave exactly those capabilities
- // with a default TRANSPORT_ETHERNET since no overrides are specified
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", "");
+ // Adding a list of capabilities will leave exactly those capabilities with a default
+ // TRANSPORT_ETHERNET since no overrides are specified
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15", "");
// Adding any invalid capabilities to the list will cause them to be ignored
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", "");
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", "");
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15,65,73", "");
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15,abcdefg", "");
// Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport
- // and apply only the override to the capabiltities object
+ // and apply only the override to the capabilities object
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(0)
.build(),
- true, "", "0");
+ "",
+ "0");
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(1)
.build(),
- true, "", "1");
+ "",
+ "1");
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(2)
.build(),
- true, "", "2");
+ "",
+ "2");
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(3)
.build(),
- true, "", "3");
+ "",
+ "3");
- // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "4");
+ // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANSPORT_ETHERNET
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "4");
// "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE
// conversion. When that becomes available, this test must be updated
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "5");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "5");
// "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE
// conversion. When that becomes available, this test must be updated
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "6");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "6");
// Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET
- assertParsedNetworkCapabilities(defaultEthernetCleared,true, "", "100");
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "100");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "abcdefg");
// Ensure the adding of both capabilities and transports work
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(true /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addCapability(12)
@@ -291,17 +274,21 @@
.addCapability(15)
.addTransportType(3)
.build(),
- true, "12,13,14,15", "3");
+ "12,13,14,15",
+ "3");
// Ensure order does not matter for capability list
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", "");
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "13,12,15,14", "");
}
- private void assertParsedNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities,
- boolean clearCapabilties, String configCapabiltiies,String configTransports) {
- assertEquals(expectedNetworkCapabilities,
- EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies,
- configTransports).build());
+ private void assertParsedNetworkCapabilities(
+ NetworkCapabilities expectedNetworkCapabilities,
+ String configCapabiltiies,
+ String configTransports) {
+ assertEquals(
+ expectedNetworkCapabilities,
+ EthernetTracker.createNetworkCapabilities(configCapabiltiies, configTransports)
+ .build());
}
@Test
diff --git a/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
new file mode 100644
index 0000000..8431194
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.os.Build
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.internal.util.HexDump
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TIMEOUT = 1000L
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class HeaderCompressionUtilsTest {
+
+ private fun decompressHex(hex: String): ByteArray {
+ val bytes = HexDump.hexStringToByteArray(hex)
+ val buf = bytes.copyOf(1500)
+ val newLen = HeaderCompressionUtils.decompress6lowpan(buf, bytes.size)
+ return buf.copyOf(newLen)
+ }
+
+ private fun compressHex(hex: String): ByteArray {
+ val buf = HexDump.hexStringToByteArray(hex)
+ val newLen = HeaderCompressionUtils.compress6lowpan(buf, buf.size)
+ return buf.copyOf(newLen)
+ }
+
+ private fun String.decodeHex() = HexDump.hexStringToByteArray(this)
+
+ @Test
+ fun testHeaderDecompression() {
+ // TF: 00, NH: 0, HLIM: 00, CID: 0, SAC: 0, SAM: 00, M: 0, DAC: 0, DAM: 00
+ var input = "6000" +
+ "ccf" + // ECN + DSCP + 4-bit Pad (here "f")
+ "12345" + // flow label
+ "11" + // next header
+ "e7" + // hop limit
+ "abcdef1234567890abcdef1234567890" + // source
+ "aaabbbcccdddeeefff00011122233344" + // dest
+ "abcd" // payload
+
+ var output = "6" + // version
+ "cc" + // traffic class
+ "12345" + // flow label
+ "0002" + // payload length
+ "11" + // next header
+ "e7" + // hop limit
+ "abcdef1234567890abcdef1234567890" + // source
+ "aaabbbcccdddeeefff00011122233344" + // dest
+ "abcd" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 01, NH: 0, HLIM: 01, CID: 0, SAC: 0, SAM: 01, M: 0, DAC: 0, DAM: 01
+ input = "6911" +
+ "5" + // ECN + 2-bit pad (here "1")
+ "f100e" + // flow label
+ "42" + // next header
+ "1102030405060708" + // source
+ "aa0b0c0d0e0f1011" + // dest
+ "abcd" // payload
+
+ output = "6" + // version
+ "01" + // traffic class
+ "f100e" + // flow label
+ "0002" + // payload length
+ "42" + // next header
+ "01" + // hop limit
+ "fe800000000000001102030405060708" + // source
+ "fe80000000000000aa0b0c0d0e0f1011" + // dest
+ "abcd" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 10, NH: 0, HLIM: 10, CID: 0, SAC: 0, SAM: 10, M: 0, DAC: 0, DAM: 10
+ input = "7222" +
+ "cc" + // traffic class
+ "43" + // next header
+ "1234" + // source
+ "abcd" + // dest
+ "abcdef" // payload
+
+ output = "6" + // version
+ "cc" + // traffic class
+ "00000" + // flow label
+ "0003" + // payload length
+ "43" + // next header
+ "40" + // hop limit
+ "fe80000000000000000000fffe001234" + // source
+ "fe80000000000000000000fffe00abcd" + // dest
+ "abcdef" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 00
+ input = "7b28" +
+ "44" + // next header
+ "1234" + // source
+ "ff020000000000000000000000000001" + // dest
+ "abcdef01" // payload
+
+ output = "6" + // version
+ "00" + // traffic class
+ "00000" + // flow label
+ "0004" + // payload length
+ "44" + // next header
+ "ff" + // hop limit
+ "fe80000000000000000000fffe001234" + // source
+ "ff020000000000000000000000000001" + // dest
+ "abcdef01" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 01
+ input = "7b29" +
+ "44" + // next header
+ "1234" + // source
+ "02abcdef1234" + // dest
+ "abcdef01" // payload
+
+ output = "6" + // version
+ "00" + // traffic class
+ "00000" + // flow label
+ "0004" + // payload length
+ "44" + // next header
+ "ff" + // hop limit
+ "fe80000000000000000000fffe001234" + // source
+ "ff02000000000000000000abcdef1234" + // dest
+ "abcdef01" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 10
+ input = "7b2a" +
+ "44" + // next header
+ "1234" + // source
+ "ee123456" + // dest
+ "abcdef01" // payload
+
+ output = "6" + // version
+ "00" + // traffic class
+ "00000" + // flow label
+ "0004" + // payload length
+ "44" + // next header
+ "ff" + // hop limit
+ "fe80000000000000000000fffe001234" + // source
+ "ffee0000000000000000000000123456" + // dest
+ "abcdef01" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+ // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 11
+ input = "7b2b" +
+ "44" + // next header
+ "1234" + // source
+ "89" + // dest
+ "abcdef01" // payload
+
+ output = "6" + // version
+ "00" + // traffic class
+ "00000" + // flow label
+ "0004" + // payload length
+ "44" + // next header
+ "ff" + // hop limit
+ "fe80000000000000000000fffe001234" + // source
+ "ff020000000000000000000000000089" + // dest
+ "abcdef01" // payload
+ assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+ }
+
+ @Test
+ fun testHeaderCompression() {
+ val input = "60120304000011fffe800000000000000000000000000001fe800000000000000000000000000002"
+ val output = "60000102030411fffe800000000000000000000000000001fe800000000000000000000000000002"
+ assertThat(compressHex(input)).isEqualTo(output.decodeHex())
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
new file mode 100644
index 0000000..e261732
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.bluetooth.BluetoothSocket
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.ParcelFileDescriptor
+import android.system.Os
+import android.system.OsConstants.AF_UNIX
+import android.system.OsConstants.SHUT_RD
+import android.system.OsConstants.SHUT_WR
+import android.system.OsConstants.SOCK_SEQPACKET
+import android.system.OsConstants.SOL_SOCKET
+import android.system.OsConstants.SO_RCVTIMEO
+import android.system.OsConstants.SO_SNDTIMEO
+import android.system.StructTimeval
+import com.android.server.net.L2capPacketForwarder.BluetoothSocketWrapper
+import com.android.server.net.L2capPacketForwarder.FdWrapper
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.nio.ByteBuffer
+import kotlin.arrayOf
+import kotlin.random.Random
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val TIMEOUT = 1000L
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capPacketForwarderTest {
+ private lateinit var forwarder: L2capPacketForwarder
+ private val tunFds = arrayOf(FileDescriptor(), FileDescriptor())
+ private val l2capFds = arrayOf(FileDescriptor(), FileDescriptor())
+ private lateinit var l2capInputStream: BluetoothL2capInputStream
+ private lateinit var l2capOutputStream: BluetoothL2capOutputStream
+ @Mock private lateinit var bluetoothSocket: BluetoothSocket
+ @Mock private lateinit var callback: L2capPacketForwarder.ICallback
+
+ private val handlerThread = HandlerThread("L2capPacketForwarderTest thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+
+ /** Imitates the behavior of an L2CAP BluetoothSocket */
+ private class BluetoothL2capInputStream(
+ val fd: FileDescriptor,
+ ) : InputStream() {
+ val l2capBuffer = ByteBuffer.wrap(ByteArray(0xffff)).apply {
+ limit(0)
+ }
+
+ override fun read(): Int {
+ throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+ }
+
+ /** See BluetoothSocket#read(buf, off, len) */
+ override fun read(b: ByteArray, off: Int, len: Int): Int {
+ // If no more bytes are remaining, read from the fd into the intermediate buffer.
+ if (l2capBuffer.remaining() == 0) {
+ // fillL2capRxBuffer()
+ // refill buffer and return - 1
+ val backingArray = l2capBuffer.array()
+ var bytesRead = 0
+ try {
+ bytesRead = Os.read(fd, backingArray, 0 /*off*/, backingArray.size)
+ } catch (e: Exception) {
+ // read failed, timed out, or was interrupted
+ // InputStream throws IOException
+ throw IOException(e)
+ }
+ l2capBuffer.rewind()
+ l2capBuffer.limit(bytesRead)
+ }
+
+ val bytesToRead = if (len > l2capBuffer.remaining()) l2capBuffer.remaining() else len
+ l2capBuffer.get(b, off, bytesToRead)
+ return bytesToRead
+ }
+
+ override fun available(): Int {
+ throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+ }
+
+ override fun close() {
+ try {
+ Os.shutdown(fd, SHUT_RD)
+ } catch (e: Exception) {
+ // InputStream throws IOException
+ throw IOException(e)
+ }
+ }
+ }
+
+ /** Imitates the behavior of an L2CAP BluetoothSocket */
+ private class BluetoothL2capOutputStream(
+ val fd: FileDescriptor,
+ ) : OutputStream() {
+
+ override fun write(b: Int) {
+ throw NotImplementedError("This method does not maintain packet boundaries, do not use")
+ }
+
+ /** See BluetoothSocket#write(buf, off, len) */
+ override fun write(b: ByteArray, off: Int, len: Int) {
+ try {
+ Os.write(fd, b, off, len)
+ } catch (e: Exception) {
+ // OutputStream throws IOException
+ throw IOException(e)
+ }
+ }
+
+ override fun close() {
+ try {
+ Os.shutdown(fd, SHUT_WR)
+ } catch (e: Exception) {
+ // OutputStream throws IOException
+ throw IOException(e)
+ }
+ }
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, tunFds[0], tunFds[1])
+ Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, l2capFds[0], l2capFds[1])
+
+ // Set socket i/o timeout for test end.
+ Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+
+ l2capInputStream = BluetoothL2capInputStream(l2capFds[0])
+ l2capOutputStream = BluetoothL2capOutputStream(l2capFds[0])
+ doReturn(l2capInputStream).`when`(bluetoothSocket).getInputStream()
+ doReturn(l2capOutputStream).`when`(bluetoothSocket).getOutputStream()
+ doAnswer({
+ l2capInputStream.close()
+ l2capOutputStream.close()
+ try {
+ // libcore's Linux_close properly invalidates the FileDescriptor, so it is safe to
+ // close multiple times.
+ Os.close(l2capFds[0])
+ } catch (e: Exception) {
+ // BluetoothSocket#close can be called multiple times. Catch EBADF on subsequent
+ // invocations.
+ }
+ }).`when`(bluetoothSocket).close()
+
+ forwarder = L2capPacketForwarder(
+ handler,
+ FdWrapper(ParcelFileDescriptor(tunFds[0])),
+ BluetoothSocketWrapper(bluetoothSocket),
+ false /* compressHeaders */,
+ callback
+ )
+ }
+
+ @After
+ fun tearDown() {
+ if (::forwarder.isInitialized) {
+ // forwarder closes tunFds[0] and l2capFds[0]
+ forwarder.tearDown()
+ } else {
+ Os.close(tunFds[0])
+ Os.close(l2capFds[0])
+ }
+ Os.close(tunFds[1])
+ Os.close(l2capFds[1])
+
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ fun sendPacket(fd: FileDescriptor, size: Int = 1280): ByteArray {
+ val packet = ByteArray(size)
+ Random.nextBytes(packet)
+ Os.write(fd, packet, 0 /*off*/, packet.size)
+ return packet
+ }
+
+ fun assertPacketReceived(fd: FileDescriptor, expected: ByteArray) {
+ val readBuffer = ByteArray(expected.size)
+ Os.read(fd, readBuffer, 0 /*off*/, readBuffer.size)
+ assertThat(readBuffer).isEqualTo(expected)
+ }
+
+ @Test
+ fun testForwarding_withoutHeaderCompression() {
+ var packet = sendPacket(l2capFds[1])
+ var packet2 = sendPacket(l2capFds[1])
+ assertPacketReceived(tunFds[1], packet)
+ assertPacketReceived(tunFds[1], packet2)
+
+ packet = sendPacket(tunFds[1])
+ packet2 = sendPacket(tunFds[1])
+ assertPacketReceived(l2capFds[1], packet)
+ assertPacketReceived(l2capFds[1], packet2)
+ }
+
+ @Test
+ fun testForwarding_packetExceedsMtu() {
+ // Reading from tun drops packets that exceed MTU.
+ // drop
+ sendPacket(tunFds[1], L2capPacketForwarder.MTU + 1)
+ // drop
+ sendPacket(tunFds[1], L2capPacketForwarder.MTU + 42)
+ var packet = sendPacket(l2capFds[1], 1280)
+ assertPacketReceived(tunFds[1], packet)
+
+ // On the BluetoothSocket side, reads that exceed MTU stop forwarding.
+ sendPacket(l2capFds[1], L2capPacketForwarder.MTU + 1)
+ verify(callback, timeout(TIMEOUT)).onError()
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index ef4c44d..697bf9e 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -56,7 +56,6 @@
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.net.TrafficStats.UID_REMOVED;
import static android.net.TrafficStats.UID_TETHERING;
-import static android.net.TrafficStats.getValueForTypeFromFirstEntry;
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;
@@ -79,6 +78,7 @@
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_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;
@@ -128,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;
@@ -161,12 +163,13 @@
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.LocationPermissionChecker;
+import com.android.net.module.util.SkDestroyListener;
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U8;
import com.android.net.module.util.bpf.CookieTagMapKey;
import com.android.net.module.util.bpf.CookieTagMapValue;
-import com.android.server.BpfNetMaps;
+import com.android.net.module.util.netlink.InetDiagMessage;
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.net.NetworkStatsService.AlertObserver;
import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
@@ -209,7 +212,7 @@
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Function;
+import java.util.function.Consumer;
/**
* Tests for {@link NetworkStatsService}.
@@ -223,6 +226,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";
@@ -283,8 +288,6 @@
private LocationPermissionChecker mLocationPermissionChecker;
private TestBpfMap<S32, U8> mUidCounterSetMap = spy(new TestBpfMap<>(S32.class, U8.class));
@Mock
- private BpfNetMaps mBpfNetMaps;
- @Mock
private SkDestroyListener mSkDestroyListener;
private TestBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap = new TestBpfMap<>(
@@ -605,13 +608,8 @@
}
@Override
- public BpfNetMaps makeBpfNetMaps(Context ctx) {
- return mBpfNetMaps;
- }
-
- @Override
- public SkDestroyListener makeSkDestroyListener(
- IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
+ public SkDestroyListener makeSkDestroyListener(Consumer<InetDiagMessage> consumer,
+ Handler handler) {
return mSkDestroyListener;
}
@@ -621,8 +619,9 @@
}
@Override
- public boolean alwaysUseTrafficStatsServiceRateLimitCache(Context ctx) {
- return mFeatureFlags.getOrDefault(
+ public boolean isTrafficStatsServiceRateLimitCacheEnabled(Context ctx,
+ boolean isClientCacheEnabled) {
+ return !isClientCacheEnabled && mFeatureFlags.getOrDefault(
TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
}
@@ -643,6 +642,19 @@
}
@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
public boolean isChangeEnabled(long changeId, int uid) {
return mCompatChanges.getOrDefault(changeId, true);
}
@@ -2453,11 +2465,49 @@
assertUidTotal(sTemplateWifi, UID_GREEN, 64L, 3L, 1024L, 8L, 0);
}
+ @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_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG)
@@ -2475,8 +2525,19 @@
}
@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 */);
}
@@ -2515,22 +2576,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) -> getValueForTypeFromFirstEntry(mService.getTypelessTotalStats(), type));
- assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> getValueForTypeFromFirstEntry(
- mService.getTypelessIfaceStats(iface), type)
- );
- assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> getValueForTypeFromFirstEntry(mService.getTypelessUidStats(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) {
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 50971e7..1e9db03 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -22,6 +22,7 @@
],
shared_libs: [
+ "libbase",
"liblog",
"libnativehelper",
"libnetdutils",
@@ -42,7 +43,7 @@
],
static_libs: [
"libnet_utils_device_common_bpfjni",
- "libnet_utils_device_common_timerfdjni",
+ "libserviceconnectivityjni",
"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..f70b04b 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_ServiceConnectivityJni(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_ServiceConnectivityJni(
+ env, "android/net/frameworktests/util/ServiceConnectivityJni") < 0)
+ return JNI_ERR;
+
return JNI_VERSION_1_6;
}
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/demoapp/AndroidManifest.xml b/thread/demoapp/AndroidManifest.xml
index c31bb71..fddc151 100644
--- a/thread/demoapp/AndroidManifest.xml
+++ b/thread/demoapp/AndroidManifest.xml
@@ -33,6 +33,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
index edb5021..0829265 100644
--- a/thread/framework/java/android/net/thread/ThreadConfiguration.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -44,18 +44,38 @@
@FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
@SystemApi
public final class ThreadConfiguration implements Parcelable {
+ private final boolean mBorderRouterEnabled;
private final boolean mNat64Enabled;
private final boolean mDhcpv6PdEnabled;
private ThreadConfiguration(Builder builder) {
- this(builder.mNat64Enabled, builder.mDhcpv6PdEnabled);
+ this(builder.mBorderRouterEnabled, builder.mNat64Enabled, builder.mDhcpv6PdEnabled);
}
- private ThreadConfiguration(boolean nat64Enabled, boolean dhcpv6PdEnabled) {
+ private ThreadConfiguration(
+ boolean borderRouterEnabled, boolean nat64Enabled, boolean dhcpv6PdEnabled) {
+ this.mBorderRouterEnabled = borderRouterEnabled;
this.mNat64Enabled = nat64Enabled;
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. */
public boolean isNat64Enabled() {
return mNat64Enabled;
@@ -78,22 +98,24 @@
return false;
} else {
ThreadConfiguration otherConfig = (ThreadConfiguration) other;
- return mNat64Enabled == otherConfig.mNat64Enabled
+ return mBorderRouterEnabled == otherConfig.mBorderRouterEnabled
+ && mNat64Enabled == otherConfig.mNat64Enabled
&& mDhcpv6PdEnabled == otherConfig.mDhcpv6PdEnabled;
}
}
@Override
public int hashCode() {
- return Objects.hash(mNat64Enabled, mDhcpv6PdEnabled);
+ return Objects.hash(mBorderRouterEnabled, mNat64Enabled, mDhcpv6PdEnabled);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('{');
- sb.append("Nat64Enabled=").append(mNat64Enabled);
- sb.append(", Dhcpv6PdEnabled=").append(mDhcpv6PdEnabled);
+ sb.append("borderRouterEnabled=").append(mBorderRouterEnabled);
+ sb.append(", nat64Enabled=").append(mNat64Enabled);
+ sb.append(", dhcpv6PdEnabled=").append(mDhcpv6PdEnabled);
sb.append('}');
return sb.toString();
}
@@ -105,6 +127,7 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mBorderRouterEnabled);
dest.writeBoolean(mNat64Enabled);
dest.writeBoolean(mDhcpv6PdEnabled);
}
@@ -114,6 +137,7 @@
@Override
public ThreadConfiguration createFromParcel(Parcel in) {
ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder();
+ builder.setBorderRouterEnabled(in.readBoolean());
builder.setNat64Enabled(in.readBoolean());
builder.setDhcpv6PdEnabled(in.readBoolean());
return builder.build();
@@ -133,6 +157,10 @@
@FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
@SystemApi
public static final class Builder {
+ // 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;
+
private boolean mNat64Enabled = false;
private boolean mDhcpv6PdEnabled = false;
@@ -156,11 +184,26 @@
public Builder(@NonNull ThreadConfiguration config) {
Objects.requireNonNull(config);
+ mBorderRouterEnabled = config.mBorderRouterEnabled;
mNat64Enabled = config.mNat64Enabled;
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;
+ }
+
+ /**
* Enables or disables NAT64 for the device.
*
* <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 14d22d1..73a6bda 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -908,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)) {
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/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/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index d5d24ac..af16d19 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -19,6 +19,7 @@
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;
@@ -122,8 +123,8 @@
import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.net.module.util.RoutingCoordinatorManager;
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;
@@ -145,6 +146,7 @@
import java.io.IOException;
import java.net.Inet6Address;
+import java.net.InetAddress;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.DateTimeException;
@@ -220,6 +222,7 @@
private NetworkRequest mUpstreamNetworkRequest;
private UpstreamNetworkCallback mUpstreamNetworkCallback;
private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
+ private ThreadNetworkCallback mThreadNetworkCallback;
private final Map<Network, LinkProperties> mNetworkToLinkProperties;
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
@@ -313,14 +316,6 @@
.build();
}
- private LocalNetworkConfig newLocalNetworkConfig() {
- return new LocalNetworkConfig.Builder()
- .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
- .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
- .setUpstreamSelector(mUpstreamNetworkRequest)
- .build();
- }
-
private void maybeInitializeOtDaemon() {
if (!shouldEnableThread()) {
return;
@@ -356,13 +351,14 @@
}
otDaemon.initialize(
- mTunIfController.getTunFd(),
shouldEnableThread(),
newOtDaemonConfig(mPersistentSettings.getConfiguration()),
+ mTunIfController.getTunFd(),
mNsdPublisher,
getMeshcopTxtAttributes(mResources.get()),
- mOtDaemonCallbackProxy,
- mCountryCodeSupplier.get());
+ mCountryCodeSupplier.get(),
+ FeatureFlags.isTrelEnabled(),
+ mOtDaemonCallbackProxy);
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
mHandler.post(mNat64CidrController::maybeUpdateNat64Cidr);
@@ -474,10 +470,16 @@
throw new IllegalStateException("Failed to create Thread tunnel interface", e);
}
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
- requestUpstreamNetwork();
- registerThreadNetworkCallback();
mUserRestricted = isThreadUserRestricted();
registerUserRestrictionsReceiver();
+
+ if (isBorderRouterMode()) {
+ requestUpstreamNetwork();
+ registerThreadNetworkCallback();
+ } else {
+ cancelRequestUpstreamNetwork();
+ unregisterThreadNetworkCallback();
+ }
maybeInitializeOtDaemon();
}
@@ -586,7 +588,20 @@
LOG.i("Set Thread configuration: " + configuration);
final boolean changed = mPersistentSettings.putConfiguration(configuration);
+
+ if (changed) {
+ if (isBorderRouterMode()) {
+ requestUpstreamNetwork();
+ registerThreadNetworkCallback();
+ } else {
+ cancelRequestUpstreamNetwork();
+ unregisterThreadNetworkCallback();
+ disableBorderRouting();
+ }
+ }
+
receiver.onSuccess();
+
if (changed) {
for (IConfigurationReceiver configReceiver : mConfigurationReceivers.keySet()) {
try {
@@ -596,6 +611,7 @@
}
}
}
+
try {
getOtDaemon()
.setConfiguration(
@@ -607,14 +623,25 @@
mNat64CidrController.maybeUpdateNat64Cidr();
}
- private static OtDaemonConfiguration newOtDaemonConfig(
- @NonNull ThreadConfiguration threadConfig) {
+ private OtDaemonConfiguration newOtDaemonConfig(ThreadConfiguration threadConfig) {
+ int srpServerConfig = R.bool.config_thread_srp_server_wait_for_border_routing_enabled;
+ boolean srpServerWaitEnabled = mResources.get().getBoolean(srpServerConfig);
+ int autoJoinConfig = R.bool.config_thread_border_router_auto_join_enabled;
+ boolean autoJoinEnabled = mResources.get().getBoolean(autoJoinConfig);
return new OtDaemonConfiguration.Builder()
+ .setBorderRouterEnabled(threadConfig.isBorderRouterEnabled())
.setNat64Enabled(threadConfig.isNat64Enabled())
.setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled())
+ .setSrpServerWaitForBorderRoutingEnabled(srpServerWaitEnabled)
+ .setBorderRouterAutoJoinEnabled(autoJoinEnabled)
.build();
}
+ /** Returns {@code true} if this device is operating as a border router. */
+ private boolean isBorderRouterMode() {
+ return mPersistentSettings.getConfiguration().isBorderRouterEnabled();
+ }
+
@Override
public void registerConfigurationCallback(@NonNull IConfigurationReceiver callback) {
enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED);
@@ -721,7 +748,7 @@
private void requestUpstreamNetwork() {
if (mUpstreamNetworkCallback != null) {
- throw new AssertionError("The upstream network request is already there.");
+ return;
}
mUpstreamNetworkCallback = new UpstreamNetworkCallback();
mConnectivityManager.registerNetworkCallback(
@@ -730,7 +757,7 @@
private void cancelRequestUpstreamNetwork() {
if (mUpstreamNetworkCallback == null) {
- throw new AssertionError("The upstream network request null.");
+ return;
}
mNetworkToLinkProperties.clear();
mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
@@ -810,16 +837,28 @@
}
private void registerThreadNetworkCallback() {
- mConnectivityManager.registerNetworkCallback(
+ 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. */
@@ -833,27 +872,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(),
LOG.getTag(),
- netCaps,
+ netCapsBuilder.build(),
getTunIfLinkProperties(),
- newLocalNetworkConfig(),
- score,
+ 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() {
@@ -899,6 +957,12 @@
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) {
@@ -918,6 +982,12 @@
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) {
@@ -1231,16 +1301,20 @@
@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("otDaemon.leave failed", e);
receiver.onError(e);
@@ -1337,15 +1411,20 @@
}
private void setInfraLinkState(InfraLinkState newInfraLinkState) {
- if (!Objects.equals(mInfraLinkState, newInfraLinkState)) {
- LOG.i("Infra link state changed: " + mInfraLinkState + " -> " + newInfraLinkState);
+ if (Objects.equals(mInfraLinkState, newInfraLinkState)) {
+ return;
}
+ 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 {
@@ -1366,6 +1445,9 @@
}
private void setInfraLinkNat64Prefix(@Nullable String newNat64Prefix) {
+ if (Objects.equals(newNat64Prefix, mInfraLinkState.nat64Prefix)) {
+ return;
+ }
try {
getOtDaemon()
.setInfraLinkNat64Prefix(
@@ -1375,6 +1457,24 @@
}
}
+ 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() {
+ LOG.i("Disabling border routing");
+ setInfraLinkState(newInfraLinkStateBuilder().build());
+ }
+
private void handleThreadInterfaceStateChanged(boolean isUp) {
try {
mTunIfController.setInterfaceUp(isUp);
@@ -1520,7 +1620,17 @@
}
return new InfraLinkState.Builder()
.setInterfaceName(linkProperties.getInterfaceName())
- .setNat64Prefix(nat64Prefix);
+ .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 {
@@ -1548,6 +1658,25 @@
}
}
+ /** 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("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;
@@ -1670,6 +1799,7 @@
// do nothing if the client is dead
}
}
+ mInfraLinkState = newInfraLinkStateBuilder().build();
}
private void onThreadEnabledChanged(int state, long listenerId) {
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
index 5d869df..18ab1ca 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -137,6 +137,8 @@
return setThreadEnabled(true);
case "disable":
return setThreadEnabled(false);
+ case "config":
+ return handleConfigCommand();
case "join":
return join();
case "leave":
@@ -149,8 +151,6 @@
return forceCountryCode();
case "get-country-code":
return getCountryCode();
- case "config":
- return handleConfigCommand();
case "ot-ctl":
return handleOtCtlCommand();
default:
@@ -321,6 +321,7 @@
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);
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index fc18ef9..746b587 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -77,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);
@@ -197,6 +204,7 @@
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.isDhcpv6PdEnabled());
writeToStoreFile();
@@ -206,6 +214,7 @@
/** 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))
.setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
.build();
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index e954d3b..89d2ce5 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -57,13 +57,4 @@
<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
index 386412e..e2f0e47 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
@@ -41,6 +41,7 @@
public final class ThreadConfigurationTest {
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+ public final boolean mIsBorderRouterEnabled;
public final boolean mIsNat64Enabled;
public final boolean mIsDhcpv6PdEnabled;
@@ -48,14 +49,16 @@
public static Collection configArguments() {
return Arrays.asList(
new Object[][] {
- {false, false}, // All disabled
- {true, false}, // NAT64 enabled
- {false, true}, // DHCP6-PD enabled
- {true, true}, // All enabled
+ {false, false, false}, // All disabled
+ {false, true, false}, // NAT64 enabled
+ {false, false, true}, // DHCP6-PD enabled
+ {true, true, true}, // All enabled
});
}
- public ThreadConfigurationTest(boolean isNat64Enabled, boolean isDhcpv6PdEnabled) {
+ public ThreadConfigurationTest(
+ boolean isBorderRouterEnabled, boolean isNat64Enabled, boolean isDhcpv6PdEnabled) {
+ mIsBorderRouterEnabled = isBorderRouterEnabled;
mIsNat64Enabled = isNat64Enabled;
mIsDhcpv6PdEnabled = isDhcpv6PdEnabled;
}
@@ -64,6 +67,7 @@
public void parcelable_parcelingIsLossLess() {
ThreadConfiguration config =
new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(mIsBorderRouterEnabled)
.setNat64Enabled(mIsNat64Enabled)
.setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
.build();
@@ -74,10 +78,12 @@
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);
}
@@ -86,6 +92,7 @@
public void builderConstructor_configsAreEqual() {
ThreadConfiguration config1 =
new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(mIsBorderRouterEnabled)
.setNat64Enabled(mIsNat64Enabled)
.setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
.build();
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 92227b4..a979721 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -922,6 +922,27 @@
@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();
@@ -933,6 +954,26 @@
@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<>();
@@ -1151,9 +1192,15 @@
CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
ConfigurationListener listener = new ConfigurationListener(mController);
ThreadConfiguration config1 =
- new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+ new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(true)
+ .setNat64Enabled(true)
+ .build();
ThreadConfiguration config2 =
- new ThreadConfiguration.Builder().setNat64Enabled(false).build();
+ new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(false)
+ .setNat64Enabled(false)
+ .build();
try {
runAsShell(
@@ -1249,6 +1296,7 @@
}
@Test
+ @Ignore("b/333649897, b/332195449: The 3 meshcop tests are flaky in different environments")
public void meshcopService_threadEnabledButNotJoined_discoveredButNoNetwork() throws Exception {
setUpTestNetwork();
@@ -1301,6 +1349,7 @@
}
@Test
+ @Ignore("b/333649897, b/332195449: The 3 meshcop tests are flaky in different environments")
public void meshcopService_threadDisabled_notDiscovered() throws Exception {
setUpTestNetwork();
CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index 8f082a4..798a51e 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -62,3 +62,23 @@
],
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 f6dd6b9..875a4ad 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -16,10 +16,10 @@
package android.net.thread;
-import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
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;
@@ -27,6 +27,7 @@
import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
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;
@@ -36,7 +37,6 @@
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.TestPermissionUtil.runAsShell;
import static com.google.common.truth.Truth.assertThat;
@@ -54,8 +54,8 @@
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;
@@ -73,7 +73,10 @@
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;
@@ -115,7 +118,7 @@
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;
@@ -123,20 +126,24 @@
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);
@@ -151,8 +158,7 @@
@After
public void tearDown() throws Exception {
mController.setTestNetworkAsUpstreamAndWait(null);
- mController.leaveAndWait();
- tearDownInfraNetwork();
+ TestTunNetworkUtils.tearDownAllInfraNetworks();
mHandlerThread.quitSafely();
mHandlerThread.join();
@@ -219,19 +225,13 @@
FullThreadDevice ftd = mFtds.get(0);
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(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
- } finally {
- runAsShell(MANAGE_TEST_NETWORKS, () -> oldInfraNetworkTracker.teardown());
- }
+ assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
}
@Test
@@ -608,8 +608,6 @@
subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
Inet6Address ftdOmr = ftd.getOmrAddress();
- // Destroy infra link and re-create
- tearDownInfraNetwork();
setUpInfraNetwork();
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
startInfraDeviceAndWaitForOnLinkAddr();
@@ -635,8 +633,6 @@
joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
Inet6Address ftdOmr = ftd.getOmrAddress();
- // Destroy infra link and re-create
- tearDownInfraNetwork();
setUpInfraNetwork();
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
startInfraDeviceAndWaitForOnLinkAddr();
@@ -672,10 +668,11 @@
}
}
+ @Ignore("TODO: b/376573921 - Enable when it's not flaky at all")
@Test
public void nat64_withAilNat64Prefix_threadDevicePingIpv4InfraDevice_outboundPacketIsForwarded()
throws Exception {
- tearDownInfraNetwork();
+ TestTunNetworkUtils.tearDownInfraNetwork(mInfraNetworkTracker);
LinkProperties lp = new LinkProperties();
// NAT64 feature requires the infra network to have an IPv4 default route.
lp.addRoute(
@@ -693,7 +690,7 @@
RouteInfo.RTN_UNICAST,
1500 /* mtu */));
lp.setNat64Prefix(AIL_NAT64_PREFIX);
- mInfraNetworkTracker = IntegrationTestUtils.setUpInfraNetwork(mContext, mController, lp);
+ mInfraNetworkTracker = TestTunNetworkUtils.setUpInfraNetwork(mContext, mController, lp);
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
FullThreadDevice ftd = mFtds.get(0);
joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
@@ -709,16 +706,12 @@
}
private void setUpInfraNetwork() throws Exception {
- mInfraNetworkTracker = IntegrationTestUtils.setUpInfraNetwork(mContext, mController);
- }
-
- private void tearDownInfraNetwork() {
- IntegrationTestUtils.tearDownInfraNetwork(mInfraNetworkTracker);
+ mInfraNetworkTracker = TestTunNetworkUtils.setUpInfraNetwork(mContext, mController);
}
private void startInfraDeviceAndWaitForOnLinkAddr() {
mInfraDevice =
- IntegrationTestUtils.startInfraDeviceAndWaitForOnLinkAddr(mInfraNetworkReader);
+ TestTunNetworkUtils.startInfraDeviceAndWaitForOnLinkAddr(mInfraNetworkReader);
}
private void assertInfraLinkMemberOfGroup(Inet6Address address) throws Exception {
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..46d4708
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
@@ -0,0 +1,229 @@
+/*
+ * 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()
+ ftd.autoStartSrpClient()
+ ftd.waitForSrpServer()
+
+ 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()
+ ftd.autoStartSrpClient()
+ ftd.waitForSrpServer()
+
+ 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 2afca5f..6c2a9bb 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -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;
@@ -441,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..7a5895f 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");
@@ -114,6 +130,7 @@
public void setUp() throws Exception {
mExecutor = Executors.newSingleThreadExecutor();
mOtCtl = new OtDaemonController();
+ mController.setEnabledAndWait(true);
mController.leaveAndWait();
// TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
@@ -124,8 +141,11 @@
@After
public void tearDown() throws Exception {
+ ThreadStateListener.stopAllListeners();
+
mController.setTestNetworkAsUpstreamAndWait(null);
mController.leaveAndWait();
+ mController.setConfigurationAndWait(DEFAULT_CONFIG);
mFtd.destroy();
mExecutor.shutdownNow();
@@ -253,6 +273,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 +361,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 +413,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/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
index 32e3b95..2f0ab34 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
@@ -189,7 +189,7 @@
final String result = runThreadCommand("config");
- assertThat(result).contains("Nat64Enabled=true");
+ assertThat(result).contains("nat64Enabled=true");
}
@Test
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 083a841..38961a3 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);
}
@@ -354,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:
@@ -494,7 +521,7 @@
}
/** Waits for an SRP server to be present in Network Data */
- private void waitForSrpServer() throws TimeoutException {
+ public void waitForSrpServer() throws TimeoutException {
// CLI output:
// > srp client server
// [fd64:db12:25f4:7e0b:1bfc:6344:25ac:2dd7]:53538
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index dc2a9c9..801e21e 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -39,6 +39,7 @@
import android.os.SystemClock
import android.system.OsConstants
import android.system.OsConstants.IPPROTO_ICMP
+import android.util.Log
import androidx.test.core.app.ApplicationProvider
import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
import com.android.net.module.util.IpUtils
@@ -84,6 +85,8 @@
/** Utilities for Thread integration tests. */
object IntegrationTestUtils {
+ private val TAG = IntegrationTestUtils::class.simpleName
+
// 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
@@ -388,7 +391,12 @@
raMsg ?: return pioList
val buf = ByteBuffer.wrap(raMsg)
- val ipv6Header = Struct.parse(Ipv6Header::class.java, buf)
+ val ipv6Header = try {
+ Struct.parse(Ipv6Header::class.java, buf)
+ } catch (e: IllegalArgumentException) {
+ // the packet is not IPv6
+ return pioList
+ }
if (ipv6Header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
return pioList
}
@@ -478,6 +486,7 @@
val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
+ Log.d(TAG, "onServiceFound: $serviceInfo")
serviceInfoFuture.complete(serviceInfo)
}
}
@@ -525,6 +534,7 @@
val resolvedServiceInfoFuture = CompletableFuture<NsdServiceInfo>()
val callback: NsdManager.ServiceInfoCallback = object : DefaultServiceInfoCallback() {
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
+ Log.d(TAG, "onServiceUpdated: $serviceInfo")
if (predicate.test(serviceInfo)) {
resolvedServiceInfoFuture.complete(serviceInfo)
}
@@ -590,6 +600,27 @@
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) {}
@@ -628,57 +659,6 @@
)
}
- 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
- }
-
- @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
- }
-
- @JvmStatic
- @JvmOverloads
- @Throws(java.lang.Exception::class)
- 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)
-
- return infraNetworkTracker
- }
-
- @JvmStatic
- fun tearDownInfraNetwork(testNetworkTracker: TestNetworkTracker) {
- runAsShell(MANAGE_TEST_NETWORKS) { testNetworkTracker.teardown() }
- }
-
/**
* Stop the ot-daemon by shell command.
*/
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 046d9bf..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()
@@ -134,6 +144,18 @@
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 4354702..b6114f3 100644
--- a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
+++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
@@ -37,6 +37,7 @@
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;
@@ -53,6 +54,9 @@
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.
@@ -71,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_*}.
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index c6a24ea..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",
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/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index e188491..bc8da8b 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -17,6 +17,8 @@
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;
@@ -34,6 +36,7 @@
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;
@@ -85,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;
@@ -102,6 +106,7 @@
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;
@@ -226,6 +231,11 @@
when(mConnectivityResources.get()).thenReturn(mResources);
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+ when(mResources.getBoolean(
+ eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
+ .thenReturn(true);
+ when(mResources.getBoolean(eq(R.bool.config_thread_border_router_auto_join_enabled)))
+ .thenReturn(true);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -258,6 +268,14 @@
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);
@@ -272,6 +290,11 @@
@Test
public void initialize_resourceOverlayValuesAreSetToOtDaemon() throws Exception {
+ when(mResources.getBoolean(
+ eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
+ .thenReturn(false);
+ when(mResources.getBoolean(eq(R.bool.config_thread_border_router_auto_join_enabled)))
+ .thenReturn(false);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -284,6 +307,8 @@
mService.initialize();
mTestLooper.dispatchAll();
+ assertThat(mFakeOtDaemon.getConfiguration().srpServerWaitForBorderRoutingEnabled).isFalse();
+ assertThat(mFakeOtDaemon.getConfiguration().borderRouterAutoJoinEnabled).isFalse();
MeshcopTxtAttributes meshcopTxts = mFakeOtDaemon.getOverriddenMeshcopTxtAttributes();
assertThat(meshcopTxts.vendorName).isEqualTo(TEST_VENDOR_NAME);
assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);
@@ -324,6 +349,35 @@
}
@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("");
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 c0e99d7..640b0f1 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -359,7 +359,7 @@
runShellCommand("config");
verify(mErrorWriter, never()).println();
- verify(mOutputWriter, times(1)).println(contains("Nat64Enabled=true"));
+ verify(mOutputWriter, times(1)).println(contains("nat64Enabled=true"));
}
@Test
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)));
+ }
+}