Merge "Revert "Add experiment flag for tethering refactor"" into main
diff --git a/.gitignore b/.gitignore
index c9b6393..b517674 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,6 @@
# VS Code project
**/.vscode
**/*.code-workspace
+
+# Vim temporary files
+**/*.swp
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
index e17081a..a484adb 100644
--- a/Cronet/tests/common/Android.bp
+++ b/Cronet/tests/common/Android.bp
@@ -28,7 +28,10 @@
name: "NetHttpCoverageTests",
enforce_default_target_sdk_version: true,
min_sdk_version: "30",
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
static_libs: [
"modules-utils-native-coverage-listener",
"CtsNetHttpTestsLib",
@@ -37,6 +40,8 @@
jarjar_rules: ":net-http-test-jarjar-rules",
compile_multilib: "both", // Include both the 32 and 64 bit versions
jni_libs: [
- "cronet_aml_components_cronet_android_cronet_tests__testing"
+ "cronet_aml_components_cronet_android_cronet_tests__testing",
+ "cronet_aml_third_party_netty_tcnative_netty_tcnative_so__testing",
],
+ data: [":cronet_javatests_resources"],
}
diff --git a/Cronet/tests/common/AndroidTest.xml b/Cronet/tests/common/AndroidTest.xml
index 2ac418f..bded8fb 100644
--- a/Cronet/tests/common/AndroidTest.xml
+++ b/Cronet/tests/common/AndroidTest.xml
@@ -19,6 +19,11 @@
<option name="install-arg" value="-t" />
</target_preparer>
<option name="test-tag" value="NetHttpCoverageTests" />
+
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="push-file" key="net" value="/storage/emulated/0/chromium_tests_root/net" />
+ <option name="push-file" key="test_server" value="/storage/emulated/0/chromium_tests_root/components/cronet/testing/test_server" />
+ </target_preparer>
<!-- Tethering/Connectivity is a SDK 30+ module -->
<!-- TODO Switch back to Sdk30 when b/270049141 is fixed -->
<object type="module_controller"
@@ -28,7 +33,28 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.net.http.tests.coverage" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <!-- b/298380508 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
+ <!-- b/316571753 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
+ <!-- b/316567693 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
+ <!-- b/316559294 -->
+ <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
+ <!-- b/316559294 -->
+ <option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" />
+ <!-- b/316554711-->
+ <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
+ <!-- b/316550794 -->
+ <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
<option name="hidden-api-checks" value="false"/>
+ <option name="isolated-storage" value="false"/>
<option
name="device-listeners"
value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index 63905c8..743a1ca 100644
--- a/Cronet/tests/mts/Android.bp
+++ b/Cronet/tests/mts/Android.bp
@@ -48,19 +48,20 @@
}
android_test {
- name: "NetHttpTests",
- defaults: [
+ name: "NetHttpTests",
+ defaults: [
"mts-target-sdk-version-current",
- ],
- static_libs: ["NetHttpTestsLibPreJarJar"],
- jarjar_rules: ":net-http-test-jarjar-rules",
- jni_libs: [
+ ],
+ static_libs: ["NetHttpTestsLibPreJarJar"],
+ jarjar_rules: ":net-http-test-jarjar-rules",
+ jni_libs: [
"cronet_aml_components_cronet_android_cronet__testing",
"cronet_aml_components_cronet_android_cronet_tests__testing",
- ],
- test_suites: [
- "general-tests",
- "mts-tethering",
- ],
+ "cronet_aml_third_party_netty_tcnative_netty_tcnative_so__testing",
+ ],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
+ data: [":cronet_javatests_resources"],
}
-
diff --git a/Cronet/tests/mts/AndroidManifest.xml b/Cronet/tests/mts/AndroidManifest.xml
index f597134..2c56e3a 100644
--- a/Cronet/tests/mts/AndroidManifest.xml
+++ b/Cronet/tests/mts/AndroidManifest.xml
@@ -19,6 +19,7 @@
package="android.net.http.mts">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET"/>
<application android:networkSecurityConfig="@xml/network_security_config">
diff --git a/Cronet/tests/mts/AndroidTest.xml b/Cronet/tests/mts/AndroidTest.xml
index 0d780a1..bccbe29 100644
--- a/Cronet/tests/mts/AndroidTest.xml
+++ b/Cronet/tests/mts/AndroidTest.xml
@@ -24,11 +24,37 @@
<option name="test-file-name" value="NetHttpTests.apk" />
</target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="push-file" key="net" value="/storage/emulated/0/chromium_tests_root/net" />
+ <option name="push-file" key="test_server" value="/storage/emulated/0/chromium_tests_root/components/cronet/testing/test_server" />
+ </target_preparer>
+
<option name="test-tag" value="NetHttpTests" />
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.net.http.mts" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <!-- b/298380508 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
+ <!-- b/316571753 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
+ <!-- b/316567693 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
+ <!-- b/316559294 -->
+ <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
+ <!-- b/316559294 -->
+ <option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" />
+ <!-- b/316554711-->
+ <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
+ <!-- b/316550794 -->
+ <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
<option name="hidden-api-checks" value="false"/>
+ <option name="isolated-storage" value="false"/>
</test>
<!-- Only run NetHttpTests in MTS if the Tethering Mainline module is installed. -->
diff --git a/Cronet/tests/mts/jarjar_excludes.txt b/Cronet/tests/mts/jarjar_excludes.txt
index a0ce5c2..b5cdf6e 100644
--- a/Cronet/tests/mts/jarjar_excludes.txt
+++ b/Cronet/tests/mts/jarjar_excludes.txt
@@ -2,6 +2,8 @@
com\.android\.testutils\..+
# jarjar-gen can't handle some kotlin object expression, exclude packages that include them
androidx\..+
+# don't jarjar netty as it does JNI
+io\.netty\..+
kotlin\.test\..+
kotlin\.reflect\..+
org\.mockito\..+
@@ -12,9 +14,16 @@
org\.chromium\.base\..+
J\.cronet_tests_N(\$.+)?
+# don't jarjar automatically generated FooJni files.
+org\.chromium\.net\..+Jni(\$.+)?
+
# Do not jarjar the tests and its utils as they also do JNI with cronet_tests.so
org\.chromium\.net\..*Test.*(\$.+)?
org\.chromium\.net\.NativeTestServer(\$.+)?
org\.chromium\.net\.MockUrlRequestJobFactory(\$.+)?
org\.chromium\.net\.QuicTestServer(\$.+)?
-org\.chromium\.net\.MockCertVerifier(\$.+)?
\ No newline at end of file
+org\.chromium\.net\.MockCertVerifier(\$.+)?
+org\.chromium\.net\.LogcatCapture(\$.+)?
+org\.chromium\.net\.ReportingCollector(\$.+)?
+org\.chromium\.net\.Http2TestServer(\$.+)?
+org\.chromium\.net\.Http2TestHandler(\$.+)?
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/raw/quicroot.pem b/Cronet/tests/mts/res/raw/quicroot.pem
new file mode 100644
index 0000000..af21b3e
--- /dev/null
+++ b/Cronet/tests/mts/res/raw/quicroot.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIC/jCCAeagAwIBAgIUXOi6XoxnMUjJg4jeOwRhsdqEqEQwDQYJKoZIhvcNAQEL
+BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTIzMDYwMTExMjcwMFoXDTMz
+MDUyOTExMjcwMFowFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl9xCMPMIvfmJWz25AG/VtgWbqNs67HXQbXWf
+pDF2wjQpHVOYbfl7Zgly5O+5es1aUbJaGyZ9G6xuYSXKFnnYLoP7M86O05fQQBAj
+K+IE5nO6136ksCAfxCFTFfn4vhPvK8Vba5rqox4WeIXYKvHYSoiHz0ELrnFOHcyN
+Innyze7bLtkMCA1ShHpmvDCR+U3Uj6JwOfoirn29jjU/48/ORha7dcJYtYXk2eGo
+RJfrtIx20tXAaKaGnXOCGYbEVXTeQkQPqKFVzqP7+KYS/Y8eNFV35ugpLNES+44T
+bQ2QruTZdrNRjJkEoyiB/E53a0OUltB/R7Z0L0xstnKfsAf3OwIDAQABo0IwQDAP
+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUVdXNh2lk
+51/6hMmz0Z+OpIe8+f0wDQYJKoZIhvcNAQELBQADggEBADNg7G8n6DUrQ5doXzm9
+kOp5siX6iPs0zFReXKhIT1Gef63l3tb7AdPedF03aj9XkUt0shhNOGG5SK2k5KBQ
+MJc9muYRCAyo2xMr3rFUQdI5B51SCy5HeAMralgTHXN0Hv+TH04YfRrACVmr+5ke
+pH3bF1gYaT+Zy5/pHJnV5lcwS6/H44g9XXWIopjWCwbfzKxIuWofqL4fiToPSIYu
+MCUI4bKZipcJT5O6rdz/S9lbgYVjOJ4HAoT2icNQqNMMfULKevmF8SdJzfNd35yn
+tAKTROhIE2aQRVCclrjo/T3eyjWGGoJlGmxKbeCf/rXzcn1BRtk/UzLnbUFFlg5l
+axw=
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml b/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml
new file mode 100644
index 0000000..48ce420
--- /dev/null
+++ b/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <bool name="is_running_in_aosp">true</bool>
+</resources>
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/xml/network_security_config.xml b/Cronet/tests/mts/res/xml/network_security_config.xml
index d44c36f..32b7171 100644
--- a/Cronet/tests/mts/res/xml/network_security_config.xml
+++ b/Cronet/tests/mts/res/xml/network_security_config.xml
@@ -17,18 +17,31 @@
-->
<network-security-config>
- <domain-config cleartextTrafficPermitted="true">
- <!-- Used as the base URL by native test server (net::EmbeddedTestServer) -->
- <domain includeSubdomains="true">127.0.0.1</domain>
- <!-- Used by CronetHttpURLConnectionTest#testIOExceptionInterruptRethrown -->
- <domain includeSubdomains="true">localhost</domain>
- <!-- Used by CronetHttpURLConnectionTest#testBadIP -->
- <domain includeSubdomains="true">0.0.0.0</domain>
- <!-- Used by CronetHttpURLConnectionTest#testSetUseCachesFalse -->
- <domain includeSubdomains="true">host-cache-test-host</domain>
- <!-- Used by CronetHttpURLConnectionTest#testBadHostname -->
- <domain includeSubdomains="true">this-weird-host-name-does-not-exist</domain>
- <!-- Used by CronetUrlRequestContextTest#testHostResolverRules -->
- <domain includeSubdomains="true">some-weird-hostname</domain>
- </domain-config>
+ <base-config>
+ <trust-anchors>
+ <certificates src="@raw/quicroot"/>
+ <certificates src="system"/>
+ </trust-anchors>
+ </base-config>
+ <!-- Since Android 9 (API 28) cleartext support is disabled by default, this
+ causes some of our tests to fail (see crbug/1220357).
+ The following configs allow http requests for the domains used in these
+ tests.
+
+ TODO(stefanoduo): Figure out if we really need to use http for these tests
+ -->
+ <domain-config cleartextTrafficPermitted="true">
+ <!-- Used as the base URL by native test server (net::EmbeddedTestServer) -->
+ <domain includeSubdomains="true">127.0.0.1</domain>
+ <!-- Used by CronetHttpURLConnectionTest#testIOExceptionInterruptRethrown -->
+ <domain includeSubdomains="true">localhost</domain>
+ <!-- Used by CronetHttpURLConnectionTest#testBadIP -->
+ <domain includeSubdomains="true">0.0.0.0</domain>
+ <!-- Used by CronetHttpURLConnectionTest#testSetUseCachesFalse -->
+ <domain includeSubdomains="true">host-cache-test-host</domain>
+ <!-- Used by CronetHttpURLConnectionTest#testBadHostname -->
+ <domain includeSubdomains="true">this-weird-host-name-does-not-exist</domain>
+ <!-- Used by CronetUrlRequestContextTest#testHostResolverRules -->
+ <domain includeSubdomains="true">some-weird-hostname</domain>
+ </domain-config>
</network-security-config>
\ No newline at end of file
diff --git a/DnsResolver/include/DnsHelperPublic.h b/DnsResolver/include/DnsHelperPublic.h
index 7c9fc9e..44b0012 100644
--- a/DnsResolver/include/DnsHelperPublic.h
+++ b/DnsResolver/include/DnsHelperPublic.h
@@ -25,7 +25,8 @@
* Perform any required initialization - including opening any required BPF maps. This function
* needs to be called before using other functions of this library.
*
- * Returns 0 on success, a negative POSIX error code (see errno.h) on other failures.
+ * Returns 0 on success, -EOPNOTSUPP when the function is called on the Android version before
+ * T. Returns a negative POSIX error code (see errno.h) on other failures.
*/
int ADnsHelper_init();
@@ -36,7 +37,9 @@
* |uid| is a Linux/Android UID to be queried. It is a combination of UserID and AppID.
* |metered| indicates whether the uid is currently using a billing network.
*
- * Returns 0(false)/1(true) on success, a negative POSIX error code (see errno.h) on other failures.
+ * Returns 0(false)/1(true) on success, -EUNATCH when the ADnsHelper_init is not called before
+ * calling this function. Returns a negative POSIX error code (see errno.h) on other failures
+ * that return from bpf syscall.
*/
int ADnsHelper_isUidNetworkingBlocked(uid_t uid, bool metered);
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 6d17476..83f798a 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,16 +1,13 @@
chiachangwang@google.com
cken@google.com
-huangaaron@google.com
jchalard@google.com
junyulai@google.com
lifr@google.com
lorenzo@google.com
-lucaslin@google.com
markchien@google.com
martinwu@google.com
maze@google.com
motomuman@google.com
-nuccachen@google.com
paulhu@google.com
prohr@google.com
reminv@google.com
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 414e50a..73c11ba 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -94,14 +94,17 @@
"ConnectivityNextEnableDefaults",
"TetheringAndroidLibraryDefaults",
"TetheringApiLevel",
- "TetheringReleaseTargetSdk"
+ "TetheringReleaseTargetSdk",
],
static_libs: [
"NetworkStackApiCurrentShims",
"net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
}
android_library {
@@ -109,14 +112,17 @@
defaults: [
"TetheringAndroidLibraryDefaults",
"TetheringApiLevel",
- "TetheringReleaseTargetSdk"
+ "TetheringReleaseTargetSdk",
],
static_libs: [
"NetworkStackApiStableShims",
"net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Due to b/143733063, APK can't access a jni lib that is in APEX (but not in the APK).
@@ -189,20 +195,28 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
// Updatable tethering packaged for finalized API
android_app {
name: "Tethering",
- defaults: ["TetheringAppDefaults", "TetheringApiLevel"],
+ defaults: [
+ "TetheringAppDefaults",
+ "TetheringApiLevel",
+ ],
static_libs: ["TetheringApiStableLib"],
certificate: "networkstack",
manifest: "AndroidManifest.xml",
use_embedded_native_libs: true,
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
}
android_app {
@@ -221,6 +235,7 @@
lint: {
strict_updatability_linting: true,
error_checks: ["NewApi"],
+ baseline_filename: "lint-baseline.xml",
},
}
@@ -239,19 +254,24 @@
java_library_static {
name: "tetheringstatsprotos",
- proto: {type: "lite"},
+ proto: {
+ type: "lite",
+ },
srcs: [
"src/com/android/networkstack/tethering/metrics/stats.proto",
],
static_libs: ["tetheringprotos"],
apex_available: ["com.android.tethering"],
min_sdk_version: "30",
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
genrule {
name: "statslog-tethering-java-gen",
tools: ["stats-log-api-gen"],
cmd: "$(location stats-log-api-gen) --java $(out) --module network_tethering" +
- " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
+ " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
out: ["com/android/networkstack/tethering/metrics/TetheringStatsLog.java"],
}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 6e8d0c9..bcea425 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -43,6 +43,7 @@
"//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index c065cd6..544ba01 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -60,6 +60,7 @@
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
@@ -899,7 +900,7 @@
}
private void configureLocalIPv6Routes(
- HashSet<IpPrefix> deprecatedPrefixes, HashSet<IpPrefix> newPrefixes) {
+ ArraySet<IpPrefix> deprecatedPrefixes, ArraySet<IpPrefix> newPrefixes) {
// [1] Remove the routes that are deprecated.
if (!deprecatedPrefixes.isEmpty()) {
removeRoutesFromLocalNetwork(getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
@@ -907,7 +908,7 @@
// [2] Add only the routes that have not previously been added.
if (newPrefixes != null && !newPrefixes.isEmpty()) {
- HashSet<IpPrefix> addedPrefixes = (HashSet) newPrefixes.clone();
+ ArraySet<IpPrefix> addedPrefixes = new ArraySet<IpPrefix>(newPrefixes);
if (mLastRaParams != null) {
addedPrefixes.removeAll(mLastRaParams.prefixes);
}
@@ -919,7 +920,7 @@
}
private void configureLocalIPv6Dns(
- HashSet<Inet6Address> deprecatedDnses, HashSet<Inet6Address> newDnses) {
+ ArraySet<Inet6Address> deprecatedDnses, ArraySet<Inet6Address> newDnses) {
// TODO: Is this really necessary? Can we not fail earlier if INetd cannot be located?
if (mNetd == null) {
if (newDnses != null) newDnses.clear();
@@ -940,7 +941,7 @@
// [2] Add only the local DNS IP addresses that have not previously been added.
if (newDnses != null && !newDnses.isEmpty()) {
- final HashSet<Inet6Address> addedDnses = (HashSet) newDnses.clone();
+ final ArraySet<Inet6Address> addedDnses = new ArraySet<Inet6Address>(newDnses);
if (mLastRaParams != null) {
addedDnses.removeAll(mLastRaParams.dnses);
}
@@ -1547,7 +1548,7 @@
// Accumulate routes representing "prefixes to be assigned to the local
// interface", for subsequent modification of local_network routing.
private static ArrayList<RouteInfo> getLocalRoutesFor(
- String ifname, HashSet<IpPrefix> prefixes) {
+ String ifname, ArraySet<IpPrefix> prefixes) {
final ArrayList<RouteInfo> localRoutes = new ArrayList<RouteInfo>();
for (IpPrefix ipp : prefixes) {
localRoutes.add(new RouteInfo(ipp, null, ifname, RTN_UNICAST));
@@ -1578,8 +1579,8 @@
/** Get IPv6 prefixes from LinkProperties */
@NonNull
@VisibleForTesting
- static HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull Collection<LinkAddress> addrs) {
- final HashSet<IpPrefix> prefixes = new HashSet<>();
+ static ArraySet<IpPrefix> getTetherableIpv6Prefixes(@NonNull Collection<LinkAddress> addrs) {
+ final ArraySet<IpPrefix> prefixes = new ArraySet<>();
for (LinkAddress linkAddr : addrs) {
if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
prefixes.add(new IpPrefix(linkAddr.getAddress(), RFC7421_PREFIX_LENGTH));
@@ -1588,7 +1589,7 @@
}
@NonNull
- private HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull LinkProperties lp) {
+ private ArraySet<IpPrefix> getTetherableIpv6Prefixes(@NonNull LinkProperties lp) {
return getTetherableIpv6Prefixes(lp.getLinkAddresses());
}
}
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 50d6c4b..d848ea8 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -41,6 +41,7 @@
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructTimeval;
+import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
@@ -134,23 +135,23 @@
public boolean hasDefaultRoute;
public byte hopLimit;
public int mtu;
- public HashSet<IpPrefix> prefixes;
- public HashSet<Inet6Address> dnses;
+ public ArraySet<IpPrefix> prefixes;
+ public ArraySet<Inet6Address> dnses;
public RaParams() {
hasDefaultRoute = false;
hopLimit = DEFAULT_HOPLIMIT;
mtu = IPV6_MIN_MTU;
- prefixes = new HashSet<IpPrefix>();
- dnses = new HashSet<Inet6Address>();
+ prefixes = new ArraySet<IpPrefix>();
+ dnses = new ArraySet<Inet6Address>();
}
public RaParams(RaParams other) {
hasDefaultRoute = other.hasDefaultRoute;
hopLimit = other.hopLimit;
mtu = other.mtu;
- prefixes = (HashSet) other.prefixes.clone();
- dnses = (HashSet) other.dnses.clone();
+ prefixes = new ArraySet<IpPrefix>(other.prefixes);
+ dnses = new ArraySet<Inet6Address>(other.dnses);
}
/**
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 9f542f4..81e18ab 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -379,7 +379,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH,
- BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
+ Tether4Key.class, Tether4Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create downstream4 map: " + e);
return null;
@@ -391,7 +391,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_UPSTREAM4_MAP_PATH,
- BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
+ Tether4Key.class, Tether4Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create upstream4 map: " + e);
return null;
@@ -403,7 +403,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH,
- BpfMap.BPF_F_RDWR, TetherDownstream6Key.class, Tether6Value.class);
+ TetherDownstream6Key.class, Tether6Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create downstream6 map: " + e);
return null;
@@ -414,7 +414,7 @@
@Nullable public IBpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
if (!isAtLeastS()) return null;
try {
- return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH,
TetherUpstream6Key.class, Tether6Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create upstream6 map: " + e);
@@ -427,7 +427,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_STATS_MAP_PATH,
- BpfMap.BPF_F_RDWR, TetherStatsKey.class, TetherStatsValue.class);
+ TetherStatsKey.class, TetherStatsValue.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create stats map: " + e);
return null;
@@ -439,7 +439,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_LIMIT_MAP_PATH,
- BpfMap.BPF_F_RDWR, TetherLimitKey.class, TetherLimitValue.class);
+ TetherLimitKey.class, TetherLimitValue.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create limit map: " + e);
return null;
@@ -451,7 +451,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_DEV_MAP_PATH,
- BpfMap.BPF_F_RDWR, TetherDevKey.class, TetherDevValue.class);
+ TetherDevKey.class, TetherDevValue.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create dev map: " + e);
return null;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 5022b40..552b105 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -136,6 +136,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
@@ -161,11 +162,8 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
/**
*
@@ -2694,31 +2692,10 @@
return;
}
- final CountDownLatch latch = new CountDownLatch(1);
-
- // Don't crash the system if something in doDump throws an exception, but try to propagate
- // the exception to the caller.
- AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
- mHandler.post(() -> {
- try {
- doDump(fd, writer, args);
- } catch (RuntimeException e) {
- exceptionRef.set(e);
- }
- latch.countDown();
- });
-
- try {
- if (!latch.await(DUMP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
- return;
- }
- } catch (InterruptedException e) {
- exceptionRef.compareAndSet(null, new IllegalStateException("Dump interrupted", e));
+ if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, writer, args),
+ DUMP_TIMEOUT_MS)) {
+ writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
}
-
- final RuntimeException e = exceptionRef.get();
- if (e != null) throw e;
}
private void maybeDhcpLeasesChanged() {
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 377da91..c232697 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -31,12 +31,14 @@
import static android.net.TetheringTester.isExpectedIcmpPacket;
import static android.net.TetheringTester.isExpectedTcpPacket;
import static android.net.TetheringTester.isExpectedUdpPacket;
+
import static com.android.net.module.util.HexDump.dumpHexString;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -46,7 +48,6 @@
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
-import android.app.UiAutomation;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.EthernetManager.TetheredInterfaceCallback;
@@ -56,8 +57,6 @@
import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringTester.TetheredDevice;
import android.net.cts.util.CtsNetUtils;
-import android.net.cts.util.CtsTetheringUtils;
-import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
@@ -141,11 +140,12 @@
protected static final ByteBuffer TX_PAYLOAD =
ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 });
- private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
- private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
- private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
- private final PackageManager mPackageManager = mContext.getPackageManager();
- private final CtsNetUtils mCtsNetUtils = new CtsNetUtils(mContext);
+ private static final Context sContext =
+ InstrumentationRegistry.getInstrumentation().getContext();
+ private static final EthernetManager sEm = sContext.getSystemService(EthernetManager.class);
+ private static final TetheringManager sTm = sContext.getSystemService(TetheringManager.class);
+ private static final PackageManager sPackageManager = sContext.getPackageManager();
+ private static final CtsNetUtils sCtsNetUtils = new CtsNetUtils(sContext);
// Late initialization in setUp()
private boolean mRunTests;
@@ -161,7 +161,7 @@
private MyTetheringEventCallback mTetheringEventCallback;
public Context getContext() {
- return mContext;
+ return sContext;
}
@BeforeClass
@@ -170,19 +170,24 @@
// Tethering would cache the last upstreams so that the next enabled tethering avoids
// picking up the address that is in conflict with the upstreams. To protect subsequent
// tests, turn tethering on and off before running them.
- final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
- final CtsTetheringUtils utils = new CtsTetheringUtils(ctx);
- final TestTetheringEventCallback callback = utils.registerTetheringEventCallback();
+ MyTetheringEventCallback callback = null;
+ TestNetworkInterface testIface = null;
try {
- if (!callback.isWifiTetheringSupported(ctx)) return;
+ // If the physical ethernet interface is available, do nothing.
+ if (isInterfaceForTetheringAvailable()) return;
- callback.expectNoTetheringActive();
+ testIface = createTestInterface();
+ setIncludeTestInterfaces(true);
- utils.startWifiTethering(callback);
- callback.getCurrentValidUpstream();
- utils.stopWifiTethering(callback);
+ callback = enableEthernetTethering(testIface.getInterfaceName(), null);
+ callback.awaitUpstreamChanged(true /* throwTimeoutException */);
+ } catch (TimeoutException e) {
+ Log.d(TAG, "WARNNING " + e);
} finally {
- utils.unregisterTetheringEventCallback(callback);
+ maybeCloseTestInterface(testIface);
+ maybeUnregisterTetheringEventCallback(callback);
+
+ setIncludeTestInterfaces(false);
}
}
@@ -195,13 +200,13 @@
mRunTests = isEthernetTetheringSupported();
assumeTrue(mRunTests);
- mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
+ mTetheredInterfaceRequester = new TetheredInterfaceRequester();
}
private boolean isEthernetTetheringSupported() throws Exception {
- if (mEm == null) return false;
+ if (sEm == null) return false;
- return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> mTm.isTetheringSupported());
+ return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> sTm.isTetheringSupported());
}
protected void maybeStopTapPacketReader(final TapPacketReader tapPacketReader)
@@ -212,7 +217,7 @@
}
}
- protected void maybeCloseTestInterface(final TestNetworkInterface testInterface)
+ protected static void maybeCloseTestInterface(final TestNetworkInterface testInterface)
throws Exception {
if (testInterface != null) {
testInterface.getFileDescriptor().close();
@@ -220,8 +225,8 @@
}
}
- protected void maybeUnregisterTetheringEventCallback(final MyTetheringEventCallback callback)
- throws Exception {
+ protected static void maybeUnregisterTetheringEventCallback(
+ final MyTetheringEventCallback callback) throws Exception {
if (callback != null) {
callback.awaitInterfaceUntethered();
callback.unregister();
@@ -230,7 +235,7 @@
protected void stopEthernetTethering(final MyTetheringEventCallback callback) {
runAsShell(TETHER_PRIVILEGED, () -> {
- mTm.stopTethering(TETHERING_ETHERNET);
+ sTm.stopTethering(TETHERING_ETHERNET);
maybeUnregisterTetheringEventCallback(callback);
});
}
@@ -277,18 +282,18 @@
}
}
- protected boolean isInterfaceForTetheringAvailable() throws Exception {
+ protected static boolean isInterfaceForTetheringAvailable() throws Exception {
// Before T, all ethernet interfaces could be used for server mode. Instead of
// waiting timeout, just checking whether the system currently has any
// ethernet interface is more reliable.
if (!SdkLevel.isAtLeastT()) {
- return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> mEm.isAvailable());
+ return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> sEm.isAvailable());
}
// If previous test case doesn't release tethering interface successfully, the other tests
// after that test may be skipped as unexcepted.
// TODO: figure out a better way to check default tethering interface existenion.
- final TetheredInterfaceRequester requester = new TetheredInterfaceRequester(mHandler, mEm);
+ final TetheredInterfaceRequester requester = new TetheredInterfaceRequester();
try {
// Use short timeout (200ms) for requesting an existing interface, if any, because
// it should reurn faster than requesting a new tethering interface. Using default
@@ -306,15 +311,15 @@
}
}
- protected void setIncludeTestInterfaces(boolean include) {
+ protected static void setIncludeTestInterfaces(boolean include) {
runAsShell(NETWORK_SETTINGS, () -> {
- mEm.setIncludeTestInterfaces(include);
+ sEm.setIncludeTestInterfaces(include);
});
}
- protected void setPreferTestNetworks(boolean prefer) {
+ protected static void setPreferTestNetworks(boolean prefer) {
runAsShell(NETWORK_SETTINGS, () -> {
- mTm.setPreferTestNetworks(prefer);
+ sTm.setPreferTestNetworks(prefer);
});
}
@@ -344,7 +349,6 @@
protected static final class MyTetheringEventCallback implements TetheringEventCallback {
- private final TetheringManager mTm;
private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1);
private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1);
@@ -355,7 +359,7 @@
private final TetheringInterface mIface;
private final Network mExpectedUpstream;
- private boolean mAcceptAnyUpstream = false;
+ private final boolean mAcceptAnyUpstream;
private volatile boolean mInterfaceWasTethered = false;
private volatile boolean mInterfaceWasLocalOnly = false;
@@ -368,19 +372,21 @@
// seconds. See b/289881008.
private static final int EXPANDED_TIMEOUT_MS = 30000;
- MyTetheringEventCallback(TetheringManager tm, String iface) {
- this(tm, iface, null);
+ MyTetheringEventCallback(String iface) {
+ mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+ mExpectedUpstream = null;
mAcceptAnyUpstream = true;
}
- MyTetheringEventCallback(TetheringManager tm, String iface, Network expectedUpstream) {
- mTm = tm;
+ MyTetheringEventCallback(String iface, @NonNull Network expectedUpstream) {
+ Objects.requireNonNull(expectedUpstream);
mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
mExpectedUpstream = expectedUpstream;
+ mAcceptAnyUpstream = false;
}
public void unregister() {
- mTm.unregisterTetheringEventCallback(this);
+ sTm.unregisterTetheringEventCallback(this);
mUnregistered = true;
}
@Override
@@ -504,6 +510,11 @@
Log.d(TAG, "Got upstream changed: " + network);
mUpstream = network;
+ // The callback always updates the current tethering status when it's first registered.
+ // If the caller registers the callback before tethering starts, the null upstream
+ // would be updated. Filtering out the null case because it's not a valid upstream that
+ // we care about.
+ if (mUpstream == null) return;
if (mAcceptAnyUpstream || Objects.equals(mUpstream, mExpectedUpstream)) {
mUpstreamLatch.countDown();
}
@@ -525,18 +536,18 @@
}
}
- protected MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableEthernetTethering(String iface,
TetheringRequest request, Network expectedUpstream) throws Exception {
// Enable ethernet tethering with null expectedUpstream means the test accept any upstream
// after etherent tethering started.
final MyTetheringEventCallback callback;
if (expectedUpstream != null) {
- callback = new MyTetheringEventCallback(mTm, iface, expectedUpstream);
+ callback = new MyTetheringEventCallback(iface, expectedUpstream);
} else {
- callback = new MyTetheringEventCallback(mTm, iface);
+ callback = new MyTetheringEventCallback(iface);
}
runAsShell(NETWORK_SETTINGS, () -> {
- mTm.registerTetheringEventCallback(mHandler::post, callback);
+ sTm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
// Need to hold the shell permission until callback is registered. This helps to avoid
// the test become flaky.
callback.awaitCallbackRegistered();
@@ -556,7 +567,7 @@
};
Log.d(TAG, "Starting Ethernet tethering");
runAsShell(TETHER_PRIVILEGED, () -> {
- mTm.startTethering(request, mHandler::post /* executor */, startTetheringCallback);
+ 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.
if (!tetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
@@ -579,7 +590,7 @@
return callback;
}
- protected MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableEthernetTethering(String iface,
Network expectedUpstream) throws Exception {
return enableEthernetTethering(iface,
new TetheringRequest.Builder(TETHERING_ETHERNET)
@@ -605,17 +616,9 @@
}
protected static final class TetheredInterfaceRequester implements TetheredInterfaceCallback {
- private final Handler mHandler;
- private final EthernetManager mEm;
-
private TetheredInterfaceRequest mRequest;
private final CompletableFuture<String> mFuture = new CompletableFuture<>();
- TetheredInterfaceRequester(Handler handler, EthernetManager em) {
- mHandler = handler;
- mEm = em;
- }
-
@Override
public void onAvailable(String iface) {
Log.d(TAG, "Ethernet interface available: " + iface);
@@ -631,7 +634,7 @@
assertNull("BUG: more than one tethered interface request", mRequest);
Log.d(TAG, "Requesting tethered interface");
mRequest = runAsShell(NETWORK_SETTINGS, () ->
- mEm.requestTetheredInterface(mHandler::post, this));
+ sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
return mFuture;
}
@@ -652,9 +655,9 @@
}
}
- protected TestNetworkInterface createTestInterface() throws Exception {
+ protected static TestNetworkInterface createTestInterface() throws Exception {
TestNetworkManager tnm = runAsShell(MANAGE_TEST_NETWORKS, () ->
- mContext.getSystemService(TestNetworkManager.class));
+ sContext.getSystemService(TestNetworkManager.class));
TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () ->
tnm.createTapInterface());
Log.d(TAG, "Created test interface " + iface.getInterfaceName());
@@ -669,7 +672,7 @@
lp.setLinkAddresses(addresses);
lp.setDnsServers(dnses);
- return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(mContext, lp, TIMEOUT_MS));
+ return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, TIMEOUT_MS));
}
protected void sendDownloadPacketUdp(@NonNull final InetAddress srcIp,
@@ -851,7 +854,7 @@
private void maybeRetryTestedUpstreamChanged(final Network expectedUpstream,
final TimeoutException fallbackException) throws Exception {
// Fall back original exception because no way to reselect if there is no WIFI feature.
- assertTrue(fallbackException.toString(), mPackageManager.hasSystemFeature(FEATURE_WIFI));
+ assertTrue(fallbackException.toString(), sPackageManager.hasSystemFeature(FEATURE_WIFI));
// Try to toggle wifi network, if any, to reselect upstream network via default network
// switching. Because test network has higher priority than internet network, this can
@@ -867,7 +870,7 @@
// See Tethering#chooseUpstreamType, CtsNetUtils#toggleWifi.
// TODO: toggle cellular network if the device has no WIFI feature.
Log.d(TAG, "Toggle WIFI to retry upstream selection");
- mCtsNetUtils.toggleWifi();
+ sCtsNetUtils.toggleWifi();
// Wait for expected upstream.
final CompletableFuture<Network> future = new CompletableFuture<>();
@@ -881,14 +884,14 @@
}
};
try {
- mTm.registerTetheringEventCallback(mHandler::post, callback);
+ sTm.registerTetheringEventCallback(mHandler::post, callback);
assertEquals("onUpstreamChanged for unexpected network", expectedUpstream,
future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
throw new AssertionError("Did not receive upstream " + expectedUpstream
+ " callback after " + TIMEOUT_MS + "ms");
} finally {
- mTm.unregisterTetheringEventCallback(callback);
+ sTm.unregisterTetheringEventCallback(callback);
}
}
@@ -925,7 +928,7 @@
mDownstreamReader = makePacketReader(mDownstreamIface);
mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
- final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ final ConnectivityManager cm = sContext.getSystemService(ConnectivityManager.class);
// Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make
// sure tethering already have ipv6 connectivity before testing.
if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) {
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
index dac5b63..90ceaa1 100644
--- a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -47,6 +47,7 @@
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.util.ArraySet;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -77,7 +78,6 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
-import java.util.HashSet;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -236,7 +236,7 @@
final RdnssOption rdnss = Struct.parse(RdnssOption.class, RdnssBuf);
final String msg =
rdnss.lifetime > 0 ? "Unknown dns" : "Unknown deprecated dns";
- final HashSet<Inet6Address> dnses =
+ final ArraySet<Inet6Address> dnses =
rdnss.lifetime > 0 ? mNewParams.dnses : mOldParams.dnses;
assertNotNull(msg, dnses);
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index 0e8b044..d5d71bc 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -84,7 +84,7 @@
private void initTestMap() throws Exception {
mTestMap = new BpfMap<>(
- TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ TETHER_DOWNSTREAM6_FS_PATH,
TetherDownstream6Key.class, Tether6Value.class);
mTestMap.forEach((key, value) -> {
@@ -135,7 +135,7 @@
assertEquals(OsConstants.EPERM, expected.errno);
}
}
- try (BpfMap readWriteMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ try (BpfMap readWriteMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH,
TetherDownstream6Key.class, Tether6Value.class)) {
assertNotNull(readWriteMap);
}
@@ -389,7 +389,7 @@
public void testOpenNonexistentMap() throws Exception {
try {
final BpfMap<TetherDownstream6Key, Tether6Value> nonexistentMap = new BpfMap<>(
- "/sys/fs/bpf/tethering/nonexistent", BpfMap.BPF_F_RDWR,
+ "/sys/fs/bpf/tethering/nonexistent",
TetherDownstream6Key.class, Tether6Value.class);
} catch (ErrnoException expected) {
assertEquals(OsConstants.ENOENT, expected.errno);
@@ -409,8 +409,8 @@
final int before = getNumOpenFds();
for (int i = 0; i < iterations; i++) {
try (BpfMap<TetherDownstream6Key, Tether6Value> map = new BpfMap<>(
- TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
- TetherDownstream6Key.class, Tether6Value.class)) {
+ TETHER_DOWNSTREAM6_FS_PATH,
+ TetherDownstream6Key.class, Tether6Value.class)) {
// do nothing
}
}
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 82b8845..750bfce 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -2810,12 +2810,10 @@
final FileDescriptor mockFd = mock(FileDescriptor.class);
final PrintWriter mockPw = mock(PrintWriter.class);
runUsbTethering(null);
- mLooper.startAutoDispatch();
mTethering.dump(mockFd, mockPw, new String[0]);
verify(mConfig).dump(any());
verify(mEntitleMgr).dump(any());
verify(mOffloadCtrl).dump(any());
- mLooper.stopAutoDispatch();
}
@Test
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index deda74e..c31dcf5 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -57,6 +57,10 @@
"app-compat-annotations",
"androidx.annotation_annotation",
],
+ static_libs: [
+ // Cannot go to framework-connectivity because mid_sdk checks require 31.
+ "modules-utils-binary-xml",
+ ],
impl_only_libs: [
// The build system will use framework-bluetooth module_current stubs, because
// of sdk_version: "module_current" above.
@@ -182,6 +186,7 @@
"//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 3513573..d346af3 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -532,6 +532,7 @@
field public static final int ERROR_RESPONSE_BAD_FORMAT = 9; // 0x9
field public static final int ERROR_TIMEOUT = 3; // 0x3
field public static final int ERROR_UNAVAILABLE = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 11; // 0xb
field public static final int ERROR_UNSUPPORTED_CHANNEL = 7; // 0x7
}
diff --git a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
index d89964d..d7cff2c 100644
--- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
+++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -27,6 +27,8 @@
import android.net.thread.IThreadNetworkManager;
import android.net.thread.ThreadNetworkManager;
+import com.android.modules.utils.build.SdkLevel;
+
/**
* Class for performing registration for Connectivity services which are exposed via updatable APIs
* since Android T.
@@ -83,14 +85,17 @@
}
);
- SystemServiceRegistry.registerStaticService(
- MDnsManager.MDNS_SERVICE,
- MDnsManager.class,
- (serviceBinder) -> {
- IMDns service = IMDns.Stub.asInterface(serviceBinder);
- return new MDnsManager(service);
- }
- );
+ // mdns service is removed from Netd from Android V.
+ if (!SdkLevel.isAtLeastV()) {
+ SystemServiceRegistry.registerStaticService(
+ MDnsManager.MDNS_SERVICE,
+ MDnsManager.class,
+ (serviceBinder) -> {
+ IMDns service = IMDns.Stub.asInterface(serviceBinder);
+ return new MDnsManager(service);
+ }
+ );
+ }
SystemServiceRegistry.registerContextAwareService(
ThreadNetworkManager.SERVICE_NAME,
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 23902dc..7fe499b 100644
--- a/framework-t/src/android/net/NetworkStatsAccess.java
+++ b/framework-t/src/android/net/NetworkStatsAccess.java
@@ -23,6 +23,7 @@
import android.Manifest;
import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
@@ -109,7 +110,7 @@
/** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
public static @NetworkStatsAccess.Level int checkAccessLevel(
- Context context, int callingPid, int callingUid, String callingPackage) {
+ Context context, int callingPid, int callingUid, @Nullable String callingPackage) {
final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class);
final TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index b6f6dbb..934b4c6 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -60,6 +60,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FileRotator;
+import com.android.modules.utils.FastDataInput;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.NetworkStatsUtils;
@@ -116,15 +117,28 @@
private long mEndMillis;
private long mTotalBytes;
private boolean mDirty;
+ private final boolean mUseFastDataInput;
/**
* Construct a {@link NetworkStatsCollection} object.
*
- * @param bucketDuration duration of the buckets in this object, in milliseconds.
+ * @param bucketDurationMillis duration of the buckets in this object, in milliseconds.
* @hide
*/
public NetworkStatsCollection(long bucketDurationMillis) {
+ this(bucketDurationMillis, false /* useFastDataInput */);
+ }
+
+ /**
+ * Construct a {@link NetworkStatsCollection} object.
+ *
+ * @param bucketDurationMillis duration of the buckets in this object, in milliseconds.
+ * @param useFastDataInput true if using {@link FastDataInput} is preferred. Otherwise, false.
+ * @hide
+ */
+ public NetworkStatsCollection(long bucketDurationMillis, boolean useFastDataInput) {
mBucketDurationMillis = bucketDurationMillis;
+ mUseFastDataInput = useFastDataInput;
reset();
}
@@ -483,7 +497,11 @@
/** @hide */
@Override
public void read(InputStream in) throws IOException {
- read((DataInput) new DataInputStream(in));
+ if (mUseFastDataInput) {
+ read(FastDataInput.obtain(in));
+ } else {
+ read((DataInput) new DataInputStream(in));
+ }
}
private void read(DataInput in) throws IOException {
@@ -967,8 +985,8 @@
* @hide
*/
@Nullable
- public static String compareStats(
- NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
+ public static String compareStats(NetworkStatsCollection migrated,
+ NetworkStatsCollection legacy, boolean allowKeyChange) {
final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
migrated.getEntries();
final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
@@ -980,7 +998,7 @@
final NetworkStatsHistory legHistory = legEntries.get(legKey);
final NetworkStatsHistory migHistory = migEntries.get(legKey);
- if (migHistory == null && couldKeyChangeOnImport(legKey)) {
+ if (migHistory == null && allowKeyChange && couldKeyChangeOnImport(legKey)) {
unmatchedLegKeys.remove(legKey);
continue;
}
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
new file mode 100644
index 0000000..b1ef98f
--- /dev/null
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.nsd;
+
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Encapsulates parameters for {@link NsdManager#registerService}.
+ * @hide
+ */
+//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+public final class AdvertisingRequest implements Parcelable {
+
+ /**
+ * Only update the registration without sending exit and re-announcement.
+ */
+ public static final int NSD_ADVERTISING_UPDATE_ONLY = 1;
+
+
+ @NonNull
+ public static final Creator<AdvertisingRequest> CREATOR =
+ new Creator<>() {
+ @Override
+ public AdvertisingRequest createFromParcel(Parcel in) {
+ final NsdServiceInfo serviceInfo = in.readParcelable(
+ NsdServiceInfo.class.getClassLoader(), NsdServiceInfo.class);
+ final int protocolType = in.readInt();
+ final long advertiseConfig = in.readLong();
+ return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig);
+ }
+
+ @Override
+ public AdvertisingRequest[] newArray(int size) {
+ return new AdvertisingRequest[size];
+ }
+ };
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ // Bitmask of @AdvertisingConfig flags. Uses a long to allow 64 possible flags in the future.
+ private final long mAdvertisingConfig;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(flag = true, prefix = {"NSD_ADVERTISING"}, value = {
+ NSD_ADVERTISING_UPDATE_ONLY,
+ })
+ @interface AdvertisingConfig {}
+
+ /**
+ * The constructor for the advertiseRequest
+ */
+ private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+ long advertisingConfig) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ mAdvertisingConfig = advertisingConfig;
+ }
+
+ /**
+ * Returns the {@link NsdServiceInfo}
+ */
+ @NonNull
+ public NsdServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ /**
+ * Returns the service advertise protocol
+ */
+ public int getProtocolType() {
+ return mProtocolType;
+ }
+
+ /**
+ * Returns the advertising config.
+ */
+ public long getAdvertisingConfig() {
+ return mAdvertisingConfig;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("serviceInfo: ").append(mServiceInfo)
+ .append(", protocolType: ").append(mProtocolType)
+ .append(", advertisingConfig: ").append(mAdvertisingConfig);
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof AdvertisingRequest)) {
+ return false;
+ } else {
+ final AdvertisingRequest otherRequest = (AdvertisingRequest) other;
+ return mServiceInfo.equals(otherRequest.mServiceInfo)
+ && mProtocolType == otherRequest.mProtocolType
+ && mAdvertisingConfig == otherRequest.mAdvertisingConfig;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mServiceInfo, flags);
+ dest.writeInt(mProtocolType);
+ dest.writeLong(mAdvertisingConfig);
+ }
+
+// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+ /**
+ * The builder for creating new {@link AdvertisingRequest} objects.
+ * @hide
+ */
+ public static final class Builder {
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ private long mAdvertisingConfig;
+ /**
+ * Creates a new {@link Builder} object.
+ */
+ public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ }
+
+ /**
+ * Sets advertising configuration flags.
+ *
+ * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags.
+ */
+ @NonNull
+ public Builder setAdvertisingConfig(long advertisingConfigFlags) {
+ mAdvertisingConfig = advertisingConfigFlags;
+ return this;
+ }
+
+
+ /** Creates a new {@link AdvertisingRequest} object. */
+ @NonNull
+ public AdvertisingRequest build() {
+ return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ }
+ }
+}
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index e671db1..b03eb29 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -16,6 +16,7 @@
package android.net.nsd;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.IOffloadEngine;
import android.net.nsd.NsdServiceInfo;
@@ -27,7 +28,7 @@
* {@hide}
*/
interface INsdServiceConnector {
- void registerService(int listenerKey, in NsdServiceInfo serviceInfo);
+ void registerService(int listenerKey, in AdvertisingRequest advertisingRequest);
void unregisterService(int listenerKey);
void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo);
void stopDiscovery(int listenerKey);
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index fcf79eb..b4f2be9 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -46,10 +46,12 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import java.lang.annotation.Retention;
@@ -57,6 +59,8 @@
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* The Network Service Discovery Manager class provides the API to discover services
@@ -152,9 +156,38 @@
"com.android.net.flags.register_nsd_offload_engine_api";
static final String NSD_SUBTYPES_SUPPORT_ENABLED =
"com.android.net.flags.nsd_subtypes_support_enabled";
+ static final String ADVERTISE_REQUEST_API =
+ "com.android.net.flags.advertise_request_api";
}
/**
+ * A regex for the acceptable format of a type or subtype label.
+ * @hide
+ */
+ public static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
+ /**
+ * A regex for the acceptable format of a service type specification.
+ *
+ * When it matches, matcher group 1 is an optional leading subtype when using legacy dot syntax
+ * (_subtype._type._tcp). Matcher group 2 is the actual type, and matcher group 3 contains
+ * optional comma-separated subtypes.
+ * @hide
+ */
+ public static final String TYPE_REGEX =
+ // Optional leading subtype (_subtype._type._tcp)
+ // (?: xxx) is a non-capturing parenthesis, don't capture the dot
+ "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
+ // Actual type (_type._tcp.local)
+ + "(" + TYPE_SUBTYPE_LABEL_REGEX + "\\._(?:tcp|udp))"
+ // Drop '.' at the end of service type that is compatible with old backend.
+ // e.g. allow "_type._tcp.local."
+ + "\\.?"
+ // Optional subtype after comma, for "_type._tcp,_subtype1,_subtype2" format
+ + "((?:," + TYPE_SUBTYPE_LABEL_REGEX + ")*)"
+ + "$";
+
+ /**
* Broadcast intent action to indicate whether network service discovery is
* enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
* information as int.
@@ -656,9 +689,12 @@
throw new RuntimeException("Failed to connect to NsdService");
}
- // Only proactively start the daemon if the target SDK < S, otherwise the internal service
- // would automatically start/stop the native daemon as needed.
- if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)) {
+ // Only proactively start the daemon if the target SDK < S AND platform < V, For target
+ // SDK >= S AND platform < V, the internal service would automatically start/stop the native
+ // daemon as needed. For platform >= V, no action is required because the native daemon is
+ // completely removed.
+ if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
+ && !SdkLevel.isAtLeastV()) {
try {
mService.startDaemon();
} catch (RemoteException e) {
@@ -1098,6 +1134,16 @@
return key;
}
+ private int updateRegisteredListener(Object listener, Executor e, NsdServiceInfo s) {
+ final int key;
+ synchronized (mMapLock) {
+ key = getListenerKey(listener);
+ mServiceMap.put(key, s);
+ mExecutorMap.put(key, e);
+ }
+ return key;
+ }
+
private void removeListener(int key) {
synchronized (mMapLock) {
mListenerMap.remove(key);
@@ -1162,14 +1208,111 @@
*/
public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType,
@NonNull Executor executor, @NonNull RegistrationListener listener) {
+ checkServiceInfo(serviceInfo);
+ checkProtocol(protocolType);
+ final AdvertisingRequest.Builder builder = new AdvertisingRequest.Builder(serviceInfo,
+ protocolType);
+ // Optionally assume that the request is an update request if it uses subtypes and the same
+ // listener. This is not documented behavior as support for advertising subtypes via
+ // "_servicename,_sub1,_sub2" has never been documented in the first place, and using
+ // multiple subtypes was broken in T until a later module update. Subtype registration is
+ // 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);
+ }
+ registerService(builder.build(), executor, listener);
+ }
+
+ private boolean isSubtypeUpdateRequest(@NonNull NsdServiceInfo serviceInfo, @NonNull
+ RegistrationListener listener) {
+ // If the listener is the same object, serviceInfo is for the same service name and
+ // type (outside of subtypes), and either of them use subtypes, treat the request as a
+ // subtype update request.
+ synchronized (mMapLock) {
+ int valueIndex = mListenerMap.indexOfValue(listener);
+ if (valueIndex == -1) {
+ return false;
+ }
+ final int key = mListenerMap.keyAt(valueIndex);
+ NsdServiceInfo existingService = mServiceMap.get(key);
+ if (existingService == null) {
+ return false;
+ }
+ final Pair<String, String> existingTypeSubtype = getTypeAndSubtypes(
+ existingService.getServiceType());
+ final Pair<String, String> newTypeSubtype = getTypeAndSubtypes(
+ serviceInfo.getServiceType());
+ if (existingTypeSubtype == null || newTypeSubtype == null) {
+ return false;
+ }
+ final boolean existingHasNoSubtype = TextUtils.isEmpty(existingTypeSubtype.second);
+ final boolean updatedHasNoSubtype = TextUtils.isEmpty(newTypeSubtype.second);
+ if (existingHasNoSubtype && updatedHasNoSubtype) {
+ // Only allow subtype changes when subtypes are used. This ensures that this
+ // behavior does not affect most requests.
+ return false;
+ }
+
+ return Objects.equals(existingService.getServiceName(), serviceInfo.getServiceName())
+ && Objects.equals(existingTypeSubtype.first, newTypeSubtype.first);
+ }
+ }
+
+ /**
+ * Get the base type from a type specification with "_type._tcp,sub1,sub2" syntax.
+ *
+ * <p>This rejects specifications using dot syntax to specify subtypes ("_sub1._type._tcp").
+ *
+ * @return Type and comma-separated list of subtypes, or null if invalid format.
+ */
+ @Nullable
+ private static Pair<String, String> getTypeAndSubtypes(@NonNull String typeWithSubtype) {
+ final Matcher matcher = Pattern.compile(TYPE_REGEX).matcher(typeWithSubtype);
+ if (!matcher.matches()) return null;
+ // Reject specifications using leading subtypes with a dot
+ if (!TextUtils.isEmpty(matcher.group(1))) return null;
+ return new Pair<>(matcher.group(2), matcher.group(3));
+ }
+
+ /**
+ * Register a service to be discovered by other services.
+ *
+ * <p> The function call immediately returns after sending a request to register service
+ * to the framework. The application is notified of a successful registration
+ * through the callback {@link RegistrationListener#onServiceRegistered} or a failure
+ * through {@link RegistrationListener#onRegistrationFailed}.
+ *
+ * <p> The application should call {@link #unregisterService} when the service
+ * registration is no longer required, and/or whenever the application is stopped.
+ * @param advertisingRequest service being registered
+ * @param executor Executor to run listener callbacks with
+ * @param listener The listener notifies of a successful registration and is used to
+ * unregister this service through a call on {@link #unregisterService}. Cannot be null.
+ *
+ * @hide
+ */
+// @FlaggedApi(Flags.ADVERTISE_REQUEST_API)
+ public void registerService(@NonNull AdvertisingRequest advertisingRequest,
+ @NonNull Executor executor,
+ @NonNull RegistrationListener listener) {
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
+ final int protocolType = advertisingRequest.getProtocolType();
if (serviceInfo.getPort() <= 0) {
throw new IllegalArgumentException("Invalid port number");
}
checkServiceInfo(serviceInfo);
checkProtocol(protocolType);
- int key = putListener(listener, executor, serviceInfo);
+ final int key;
+ // For update only request, the old listener has to be reused
+ if ((advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) {
+ key = updateRegisteredListener(listener, executor, serviceInfo);
+ } else {
+ key = putListener(listener, executor, serviceInfo);
+ }
try {
- mService.registerService(key, serviceInfo);
+ mService.registerService(key, advertisingRequest);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
diff --git a/framework/Android.bp b/framework/Android.bp
index 1e6262d..432cabf 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -95,6 +95,7 @@
],
impl_only_static_libs: [
"net-utils-device-common-bpf",
+ "net-utils-device-common-struct",
],
libs: [
"androidx.annotation_annotation",
@@ -105,7 +106,9 @@
apex_available: [
"com.android.tethering",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
java_library {
@@ -124,6 +127,7 @@
// Even if the library is included in "impl_only_static_libs" of defaults. This is still
// needed because java_library which doesn't understand "impl_only_static_libs".
"net-utils-device-common-bpf",
+ "net-utils-device-common-struct",
],
libs: [
// This cannot be in the defaults clause above because if it were, it would be used
@@ -134,7 +138,10 @@
"framework-tethering.impl",
"framework-wifi.stubs.module_lib",
],
- visibility: ["//packages/modules/Connectivity:__subpackages__"]
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_defaults {
@@ -185,10 +192,14 @@
"//packages/modules/Connectivity/Cronet/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
platform_compat_config {
@@ -248,6 +259,9 @@
apex_available: [
"com.android.tethering",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_genrule {
@@ -293,9 +307,9 @@
],
flags: [
"--show-for-stub-purposes-annotation android.annotation.SystemApi" +
- "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
+ "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
"--show-for-stub-purposes-annotation android.annotation.SystemApi" +
- "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
+ "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
],
aidl: {
include_dirs: [
@@ -308,6 +322,9 @@
java_library {
name: "framework-connectivity-module-api-stubs-including-flagged",
srcs: [":framework-connectivity-module-api-stubs-including-flagged-droidstubs"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Library providing limited APIs within the connectivity module, so that R+ components like
@@ -332,4 +349,7 @@
visibility: [
"//packages/modules/Connectivity/Tethering:__subpackages__",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
diff --git a/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
new file mode 100644
index 0000000..2848074
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+@JavaOnlyStableParcelable parcelable AdvertisingRequest;
\ No newline at end of file
diff --git a/framework/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp
index 5403be7..51eaf1c 100644
--- a/framework/jni/android_net_NetworkUtils.cpp
+++ b/framework/jni/android_net_NetworkUtils.cpp
@@ -24,6 +24,7 @@
#include <string.h>
#include <bpf/BpfClassic.h>
+#include <bpf/KernelUtils.h>
#include <DnsProxydProtocol.h> // NETID_USE_LOCAL_NAMESERVERS
#include <nativehelper/JNIPlatformHelp.h>
#include <nativehelper/ScopedPrimitiveArray.h>
@@ -250,6 +251,10 @@
}
}
+static jboolean android_net_utils_isKernel64Bit(JNIEnv *env, jclass clazz) {
+ return bpf::isKernel64Bit();
+}
+
// ----------------------------------------------------------------------------
/*
@@ -272,6 +277,7 @@
{ "getDnsNetwork", "()Landroid/net/Network;", (void*) android_net_utils_getDnsNetwork },
{ "setsockoptBytes", "(Ljava/io/FileDescriptor;II[B)V",
(void*) android_net_utils_setsockoptBytes},
+ { "isKernel64Bit", "()Z", (void*) android_net_utils_isKernel64Bit },
};
// clang-format on
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index fa27d0e..1ea1815 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -6022,6 +6022,13 @@
/**
* Sets data saver switch.
*
+ * <p>This API configures the bandwidth control, and filling data saver status in BpfMap,
+ * which is intended for internal use by the network stack to optimize performance
+ * when frequently checking data saver status for multiple uids without doing IPC.
+ * It does not directly control the global data saver mode that users manage in settings.
+ * To query the comprehensive data saver status for a specific UID, including allowlist
+ * considerations, use {@link #getRestrictBackgroundStatus}.
+ *
* @param enable True if enable.
* @throws IllegalStateException if failed.
* @hide
diff --git a/framework/src/android/net/NetworkUtils.java b/framework/src/android/net/NetworkUtils.java
index fbdc024..785c029 100644
--- a/framework/src/android/net/NetworkUtils.java
+++ b/framework/src/android/net/NetworkUtils.java
@@ -438,4 +438,6 @@
public static native void setsockoptBytes(FileDescriptor fd, int level, int option,
byte[] value) throws ErrnoException;
+ /** Returns whether the Linux Kernel is 64 bit */
+ public static native boolean isKernel64Bit();
}
diff --git a/nearby/README.md b/nearby/README.md
index 81d2199..8dac61c 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -55,6 +55,7 @@
$ adb install /tmp/tethering.apex
$ adb reboot
+NOTE: Developers should use AOSP by default, udc-mainline-prod should not be used unless for Google internal features.
For udc-mainline-prod on Google internal host
Build unbundled module using banchan
$ source build/envsetup.sh
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 4630902..17b80b0 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -30,7 +30,7 @@
srcs: [":nearby-service-srcs"],
defaults: [
- "framework-system-server-module-defaults"
+ "framework-system-server-module-defaults",
],
libs: [
"androidx.annotation_annotation",
@@ -66,13 +66,16 @@
apex_available: [
"com.android.tethering",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
genrule {
name: "statslog-nearby-java-gen",
tools: ["stats-log-api-gen"],
cmd: "$(location stats-log-api-gen) --java $(out) --module nearby " +
- " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
- " --minApiLevel 33",
+ " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
+ " --minApiLevel 33",
out: ["com/android/server/nearby/proto/NearbyStatsLog.java"],
}
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 112c751..bbf42c7 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -43,7 +43,6 @@
"platform-test-annotations",
"service-nearby-pre-jarjar",
"truth",
- // "Robolectric_all-target",
],
// these are needed for Extended Mockito
jni_libs: [
diff --git a/netd/Android.bp b/netd/Android.bp
index 4325d89..3cdbc97 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -69,10 +69,10 @@
"BpfBaseTest.cpp"
],
static_libs: [
+ "libbase",
"libnetd_updatable",
],
shared_libs: [
- "libbase",
"libcutils",
"liblog",
"libnetdutils",
diff --git a/netd/NetdUpdatable.cpp b/netd/NetdUpdatable.cpp
index 8b9e5a7..3b15916 100644
--- a/netd/NetdUpdatable.cpp
+++ b/netd/NetdUpdatable.cpp
@@ -31,8 +31,8 @@
android::netdutils::Status ret = sBpfHandler.init(cg2_path);
if (!android::netdutils::isOk(ret)) {
- LOG(ERROR) << __func__ << ": Failed. " << ret.code() << " " << ret.msg();
- return -ret.code();
+ LOG(ERROR) << __func__ << ": Failed: (" << ret.code() << ") " << ret.msg();
+ abort();
}
return 0;
}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index bc49f0e..de879f3 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -31,6 +31,7 @@
],
visibility: ["//visibility:private"],
}
+
// The above filegroup can be used to specify different sources depending
// on the branch, while minimizing merge conflicts in the rest of the
// build rules.
@@ -78,6 +79,9 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Test building mDNS as a standalone, so that it can be imported into other repositories as-is.
@@ -94,11 +98,12 @@
min_sdk_version: "21",
lint: {
error_checks: ["NewApi"],
+ baseline_filename: "lint-baseline.xml",
},
srcs: [
"src/com/android/server/connectivity/mdns/**/*.java",
":framework-connectivity-t-mdns-standalone-build-sources",
- ":service-mdns-droidstubs"
+ ":service-mdns-droidstubs",
],
exclude_srcs: [
"src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
@@ -127,7 +132,7 @@
srcs: ["src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"],
libs: [
"net-utils-device-common-mdns-standalone-build-test",
- "service-connectivity-tiramisu-pre-jarjar"
+ "service-connectivity-tiramisu-pre-jarjar",
],
visibility: [
"//visibility:private",
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index bdbb655..81912ae 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -34,77 +34,64 @@
using android::bpf::bpfGetUidStats;
using android::bpf::bpfGetIfaceStats;
-using android::bpf::bpfGetIfIndexStats;
using android::bpf::NetworkTraceHandler;
namespace android {
-// NOTE: keep these in sync with TrafficStats.java
-static const uint64_t UNKNOWN = -1;
-
-enum StatsType {
- RX_BYTES = 0,
- RX_PACKETS = 1,
- TX_BYTES = 2,
- TX_PACKETS = 3,
-};
-
-static uint64_t getStatsType(StatsValue* stats, StatsType type) {
- switch (type) {
- case RX_BYTES:
- return stats->rxBytes;
- case RX_PACKETS:
- return stats->rxPackets;
- case TX_BYTES:
- return stats->txBytes;
- case TX_PACKETS:
- return stats->txPackets;
- default:
- return UNKNOWN;
+static jobject statsValueToEntry(JNIEnv* env, StatsValue* stats) {
+ // Find the Java class that represents the structure
+ jclass gEntryClass = env->FindClass("android/net/NetworkStats$Entry");
+ if (gEntryClass == nullptr) {
+ return nullptr;
}
+
+ // Create a new instance of the Java class
+ jobject result = env->AllocObject(gEntryClass);
+ if (result == nullptr) {
+ return nullptr;
+ }
+
+ // Set the values of the structure fields in the Java object
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "rxBytes", "J"), stats->rxBytes);
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "txBytes", "J"), stats->txBytes);
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "rxPackets", "J"), stats->rxPackets);
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "txPackets", "J"), stats->txPackets);
+
+ return result;
}
-static jlong nativeGetTotalStat(JNIEnv* env, jclass clazz, jint type) {
+static jobject nativeGetTotalStat(JNIEnv* env, jclass clazz) {
StatsValue stats = {};
if (bpfGetIfaceStats(NULL, &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
+ return statsValueToEntry(env, &stats);
} else {
- return UNKNOWN;
+ return nullptr;
}
}
-static jlong nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) {
+static jobject nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface) {
ScopedUtfChars iface8(env, iface);
if (iface8.c_str() == NULL) {
- return UNKNOWN;
+ return nullptr;
}
StatsValue stats = {};
if (bpfGetIfaceStats(iface8.c_str(), &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
+ return statsValueToEntry(env, &stats);
} else {
- return UNKNOWN;
+ return nullptr;
}
}
-static jlong nativeGetIfIndexStat(JNIEnv* env, jclass clazz, jint ifindex, jint type) {
- StatsValue stats = {};
- if (bpfGetIfIndexStats(ifindex, &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
- } else {
- return UNKNOWN;
- }
-}
-
-static jlong nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
+static jobject nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid) {
StatsValue stats = {};
if (bpfGetUidStats(uid, &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
+ return statsValueToEntry(env, &stats);
} else {
- return UNKNOWN;
+ return nullptr;
}
}
@@ -113,11 +100,26 @@
}
static const JNINativeMethod gMethods[] = {
- {"nativeGetTotalStat", "(I)J", (void*)nativeGetTotalStat},
- {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)nativeGetIfaceStat},
- {"nativeGetIfIndexStat", "(II)J", (void*)nativeGetIfIndexStat},
- {"nativeGetUidStat", "(II)J", (void*)nativeGetUidStat},
- {"nativeInitNetworkTracing", "()V", (void*)nativeInitNetworkTracing},
+ {
+ "nativeGetTotalStat",
+ "()Landroid/net/NetworkStats$Entry;",
+ (void*)nativeGetTotalStat
+ },
+ {
+ "nativeGetIfaceStat",
+ "(Ljava/lang/String;)Landroid/net/NetworkStats$Entry;",
+ (void*)nativeGetIfaceStat
+ },
+ {
+ "nativeGetUidStat",
+ "(I)Landroid/net/NetworkStats$Entry;",
+ (void*)nativeGetUidStat
+ },
+ {
+ "nativeInitNetworkTracing",
+ "()V",
+ (void*)nativeInitNetworkTracing
+ },
};
int register_android_server_net_NetworkStatsService(JNIEnv* env) {
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index 0dfd0af..b9f3adb 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -73,6 +73,7 @@
"-Wthread-safety",
],
static_libs: [
+ "libbase",
"libgmock",
"libnetworkstats",
"libperfetto_client_experimental",
@@ -80,7 +81,6 @@
"perfetto_trace_protos",
],
shared_libs: [
- "libbase",
"liblog",
"libcutils",
"libandroid_net",
diff --git a/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
new file mode 100644
index 0000000..08a8603
--- /dev/null
+++ b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
@@ -0,0 +1,168 @@
+/*
+ * 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.metrics;
+
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_DISABLED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_ENABLED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__OPERATION_TYPE__ROT_READ;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UNKNOWN;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to log NetworkStats related metrics.
+ *
+ * This class does not provide thread-safe.
+ */
+public class NetworkStatsMetricsLogger {
+ final Dependencies mDeps;
+ int mReadIndex = 1;
+
+ /** Dependency class */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * Writes a NETWORK_STATS_RECORDER_FILE_OPERATION_REPORTED event to ConnectivityStatsLog.
+ */
+ public void writeRecorderFileReadingStats(int recorderType, int readIndex,
+ int readLatencyMillis,
+ int fileCount, int totalFileSize,
+ int keys, int uids, int totalHistorySize,
+ boolean useFastDataInput) {
+ ConnectivityStatsLog.write(NETWORK_STATS_RECORDER_FILE_OPERATED,
+ NETWORK_STATS_RECORDER_FILE_OPERATED__OPERATION_TYPE__ROT_READ,
+ recorderType,
+ readIndex,
+ readLatencyMillis,
+ fileCount,
+ totalFileSize,
+ keys,
+ uids,
+ totalHistorySize,
+ useFastDataInput
+ ? NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_ENABLED
+ : NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_DISABLED);
+ }
+ }
+
+ public NetworkStatsMetricsLogger() {
+ mDeps = new Dependencies();
+ }
+
+ @VisibleForTesting
+ public NetworkStatsMetricsLogger(Dependencies deps) {
+ mDeps = deps;
+ }
+
+ private static int prefixToRecorderType(@NonNull String prefix) {
+ switch (prefix) {
+ case PREFIX_XT:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
+ case PREFIX_UID:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+ case PREFIX_UID_TAG:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+ default:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UNKNOWN;
+ }
+ }
+
+ /**
+ * Get file count and total byte count for the given directory and prefix.
+ *
+ * @return File count and total byte count as a pair, or 0s if met errors.
+ */
+ private static Pair<Integer, Integer> getStatsFilesAttributes(
+ @Nullable File statsDir, @NonNull String prefix) {
+ if (statsDir == null || !statsDir.isDirectory()) return new Pair<>(0, 0);
+
+ // Only counts the matching files.
+ // The files are named in the following format:
+ // <prefix>.<startTimestamp>-[<endTimestamp>]
+ // e.g. uid_tag.12345-
+ // See FileRotator#FileInfo for more detail.
+ final Pattern pattern = Pattern.compile("^" + prefix + "\\.[0-9]+-[0-9]*$");
+
+ int totalFiles = 0;
+ int totalBytes = 0;
+ for (String name : emptyIfNull(statsDir.list())) {
+ if (!pattern.matcher(name).matches()) continue;
+
+ totalFiles++;
+ // Cast to int is safe since stats persistent files are several MBs in total.
+ totalBytes += (int) (new File(statsDir, name).length());
+
+ }
+ return new Pair<>(totalFiles, totalBytes);
+ }
+
+ private static String [] emptyIfNull(@Nullable String [] array) {
+ return (array == null) ? new String[0] : array;
+ }
+
+ /**
+ * Log statistics from the NetworkStatsRecorder file reading process into statsd.
+ */
+ public void logRecorderFileReading(@NonNull String prefix, int readLatencyMillis,
+ @Nullable File statsDir, @NonNull NetworkStatsCollection collection,
+ boolean useFastDataInput) {
+ final Set<Integer> uids = new HashSet<>();
+ final Map<NetworkStatsCollection.Key, NetworkStatsHistory> entries =
+ collection.getEntries();
+
+ for (final NetworkStatsCollection.Key key : entries.keySet()) {
+ uids.add(key.uid);
+ }
+
+ int totalHistorySize = 0;
+ for (final NetworkStatsHistory history : entries.values()) {
+ totalHistorySize += history.size();
+ }
+
+ final Pair<Integer, Integer> fileAttributes = getStatsFilesAttributes(statsDir, prefix);
+ mDeps.writeRecorderFileReadingStats(prefixToRecorderType(prefix),
+ mReadIndex++,
+ readLatencyMillis,
+ fileAttributes.first /* fileCount */,
+ fileAttributes.second /* totalFileSize */,
+ entries.size(),
+ uids.size(),
+ totalHistorySize,
+ useFastDataInput);
+ }
+}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 6086207..9b2f80b 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -26,6 +26,8 @@
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;
+import static android.net.nsd.NsdManager.TYPE_REGEX;
+import static android.net.nsd.NsdManager.TYPE_SUBTYPE_LABEL_REGEX;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -51,6 +53,7 @@
import android.net.mdns.aidl.IMDnsEventListener;
import android.net.mdns.aidl.RegistrationInfo;
import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManager;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
@@ -173,7 +176,7 @@
"mdns_advertiser_allowlist_";
private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
- private static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+ private static final String FORCE_ENABLE_FLAG_FOR_TEST_PREFIX = "test_";
@VisibleForTesting
static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
@@ -196,7 +199,8 @@
private final Context mContext;
private final NsdStateMachine mNsdStateMachine;
- private final MDnsManager mMDnsManager;
+ // It can be null on V+ device since mdns native service provided by netd is removed.
+ private final @Nullable MDnsManager mMDnsManager;
private final MDnsEventCallback mMDnsEventCallback;
@NonNull
private final Dependencies mDeps;
@@ -267,15 +271,11 @@
protected final int mClientRequestId;
protected final int mTransactionId;
@NonNull
- protected final NsdServiceInfo mReqServiceInfo;
- @NonNull
protected final String mListenedServiceType;
- MdnsListener(int clientRequestId, int transactionId, @NonNull NsdServiceInfo reqServiceInfo,
- @NonNull String listenedServiceType) {
+ MdnsListener(int clientRequestId, int transactionId, @NonNull String listenedServiceType) {
mClientRequestId = clientRequestId;
mTransactionId = transactionId;
- mReqServiceInfo = reqServiceInfo;
mListenedServiceType = listenedServiceType;
}
@@ -318,8 +318,8 @@
private class DiscoveryListener extends MdnsListener {
DiscoveryListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType) {
+ super(clientRequestId, transactionId, listenServiceType);
}
@Override
@@ -348,8 +348,8 @@
private class ResolutionListener extends MdnsListener {
ResolutionListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType) {
+ super(clientRequestId, transactionId, listenServiceType);
}
@Override
@@ -370,8 +370,8 @@
private class ServiceInfoListener extends MdnsListener {
ServiceInfoListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType) {
+ super(clientRequestId, transactionId, listenServiceType);
}
@Override
@@ -542,6 +542,11 @@
if (DBG) Log.d(TAG, "Daemon is already started.");
return;
}
+
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStartDaemon: mMDnsManager is null");
+ return;
+ }
mMDnsManager.registerEventListener(mMDnsEventCallback);
mMDnsManager.startDaemon();
mIsDaemonStarted = true;
@@ -554,6 +559,11 @@
if (DBG) Log.d(TAG, "Daemon has not been started.");
return;
}
+
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStopDaemon: mMDnsManager is null");
+ return;
+ }
mMDnsManager.unregisterEventListener(mMDnsEventCallback);
mMDnsManager.stopDaemon();
mIsDaemonStarted = false;
@@ -728,12 +738,11 @@
final ClientInfo clientInfo;
final int transactionId;
final int clientRequestId = msg.arg2;
- final ListenerArgs args;
final OffloadEngineInfo offloadEngineInfo;
switch (msg.what) {
case NsdManager.DISCOVER_SERVICES: {
if (DBG) Log.d(TAG, "Discover services");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -767,7 +776,7 @@
final String listenServiceType = serviceType + ".local";
maybeStartMonitoringSockets();
final MdnsListener listener = new DiscoveryListener(clientRequestId,
- transactionId, info, listenServiceType);
+ transactionId, listenServiceType);
final MdnsSearchOptions.Builder optionsBuilder =
MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
@@ -809,7 +818,7 @@
}
case NsdManager.STOP_DISCOVERY: {
if (DBG) Log.d(TAG, "Stop service discovery");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -847,7 +856,7 @@
}
case NsdManager.REGISTER_SERVICE: {
if (DBG) Log.d(TAG, "Register service");
- args = (ListenerArgs) msg.obj;
+ final AdvertisingArgs args = (AdvertisingArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -862,9 +871,12 @@
NsdManager.FAILURE_MAX_LIMIT, true /* isLegacy */);
break;
}
-
- transactionId = getUniqueId();
- final NsdServiceInfo serviceInfo = args.serviceInfo;
+ final AdvertisingRequest advertisingRequest = args.advertisingRequest;
+ if (advertisingRequest == null) {
+ Log.e(TAG, "Unknown advertisingRequest in registration");
+ break;
+ }
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
final String serviceType = serviceInfo.getServiceType();
final Pair<String, List<String>> typeSubtype = parseTypeAndSubtype(
serviceType);
@@ -879,6 +891,23 @@
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
}
+ boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0;
+ // If it is an update request, then reuse the old transactionId
+ if (isUpdateOnly) {
+ final ClientRequest existingClientRequest =
+ clientInfo.mClientRequests.get(clientRequestId);
+ if (existingClientRequest == null) {
+ Log.e(TAG, "Invalid update on requestId: " + clientRequestId);
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_INTERNAL_ERROR,
+ false /* isLegacy */);
+ break;
+ }
+ transactionId = existingClientRequest.mTransactionId;
+ } else {
+ transactionId = getUniqueId();
+ }
serviceInfo.setServiceType(registerServiceType);
serviceInfo.setServiceName(truncateServiceName(
serviceInfo.getServiceName()));
@@ -899,12 +928,16 @@
serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
+ final MdnsAdvertisingOptions mdnsAdvertisingOptions =
+ MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(
+ isUpdateOnly).build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
- MdnsAdvertisingOptions.newBuilder().build());
+ mdnsAdvertisingOptions);
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
serviceInfo.getNetwork());
} else {
maybeStartDaemon();
+ transactionId = getUniqueId();
if (registerService(transactionId, serviceInfo)) {
if (DBG) {
Log.d(TAG, "Register " + clientRequestId
@@ -924,7 +957,7 @@
}
case NsdManager.UNREGISTER_SERVICE: {
if (DBG) Log.d(TAG, "unregister service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -967,7 +1000,7 @@
}
case NsdManager.RESOLVE_SERVICE: {
if (DBG) Log.d(TAG, "Resolve service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -995,7 +1028,7 @@
maybeStartMonitoringSockets();
final MdnsListener listener = new ResolutionListener(clientRequestId,
- transactionId, info, resolveServiceType);
+ transactionId, resolveServiceType);
final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
.setIsPassiveMode(true)
@@ -1029,7 +1062,7 @@
}
case NsdManager.STOP_RESOLUTION: {
if (DBG) Log.d(TAG, "Stop service resolution");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1068,7 +1101,7 @@
}
case NsdManager.REGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Register a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1093,7 +1126,7 @@
maybeStartMonitoringSockets();
final MdnsListener listener = new ServiceInfoListener(clientRequestId,
- transactionId, info, resolveServiceType);
+ transactionId, resolveServiceType);
final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
.setIsPassiveMode(true)
@@ -1111,7 +1144,7 @@
}
case NsdManager.UNREGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Unregister a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1638,20 +1671,7 @@
@Nullable
public static Pair<String, List<String>> parseTypeAndSubtype(String serviceType) {
if (TextUtils.isEmpty(serviceType)) return null;
-
- final String regexString =
- // Optional leading subtype (_subtype._type._tcp)
- // (?: xxx) is a non-capturing parenthesis, don't capture the dot
- "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
- // Actual type (_type._tcp.local)
- + "(" + TYPE_SUBTYPE_LABEL_REGEX + "\\._(?:tcp|udp))"
- // Drop '.' at the end of service type that is compatible with old backend.
- // e.g. allow "_type._tcp.local."
- + "\\.?"
- // Optional subtype after comma, for "_type._tcp,_subtype1,_subtype2" format
- + "((?:," + TYPE_SUBTYPE_LABEL_REGEX + ")*)"
- + "$";
- final Pattern serviceTypePattern = Pattern.compile(regexString);
+ final Pattern serviceTypePattern = Pattern.compile(TYPE_REGEX);
final Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
final String queryType = matcher.group(2);
@@ -1685,7 +1705,8 @@
mContext = ctx;
mNsdStateMachine = new NsdStateMachine(TAG, handler);
mNsdStateMachine.start();
- mMDnsManager = ctx.getSystemService(MDnsManager.class);
+ // It can fail on V+ device since mdns native service provided by netd is removed.
+ mMDnsManager = SdkLevel.isAtLeastV() ? null : ctx.getSystemService(MDnsManager.class);
mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine);
mDeps = deps;
@@ -1716,6 +1737,12 @@
mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
.setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
+ .setIsKnownAnswerSuppressionEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION))
+ .setIsUnicastReplyEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_UNICAST_REPLY_ENABLED))
+ .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
+ mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
.build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
@@ -2077,20 +2104,33 @@
}
}
+ private static class AdvertisingArgs {
+ public final NsdServiceConnector connector;
+ public final AdvertisingRequest advertisingRequest;
+
+ AdvertisingArgs(NsdServiceConnector connector, AdvertisingRequest advertisingRequest) {
+ this.connector = connector;
+ this.advertisingRequest = advertisingRequest;
+ }
+ }
+
private class NsdServiceConnector extends INsdServiceConnector.Stub
implements IBinder.DeathRecipient {
+
@Override
- public void registerService(int listenerKey, NsdServiceInfo serviceInfo) {
+ public void registerService(int listenerKey, AdvertisingRequest advertisingRequest)
+ throws RemoteException {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.REGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, serviceInfo)));
+ new AdvertisingArgs(this, advertisingRequest)
+ ));
}
@Override
public void unregisterService(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2102,8 +2142,8 @@
@Override
public void stopDiscovery(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_DISCOVERY, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_DISCOVERY,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2115,8 +2155,8 @@
@Override
public void stopResolution(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_RESOLUTION,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2130,13 +2170,13 @@
public void unregisterServiceInfoCallback(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE_CALLBACK, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
public void startDaemon() {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.DAEMON_STARTUP,
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2172,25 +2212,24 @@
throw new SecurityException("API is not available in before API level 33");
}
- // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V.
- if (SdkLevel.isAtLeastV() && PermissionUtils.checkAnyPermissionOf(context,
- REGISTER_NSD_OFFLOAD_ENGINE)) {
- return;
+ final ArrayList<String> permissionsList = new ArrayList<>(Arrays.asList(NETWORK_STACK,
+ PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS));
+
+ if (SdkLevel.isAtLeastV()) {
+ // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V.
+ permissionsList.add(REGISTER_NSD_OFFLOAD_ENGINE);
+ } else if (SdkLevel.isAtLeastU()) {
+ // REGISTER_NSD_OFFLOAD_ENGINE cannot be backport to U. In U, check the DEVICE_POWER
+ // permission instead.
+ permissionsList.add(DEVICE_POWER);
}
- // REGISTER_NSD_OFFLOAD_ENGINE cannot be backport to U. In U, check the DEVICE_POWER
- // permission instead.
- if (!SdkLevel.isAtLeastV() && SdkLevel.isAtLeastU()
- && PermissionUtils.checkAnyPermissionOf(context, DEVICE_POWER)) {
- return;
- }
- if (PermissionUtils.checkAnyPermissionOf(context, NETWORK_STACK,
- PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) {
+ if (PermissionUtils.checkAnyPermissionOf(context,
+ permissionsList.toArray(new String[0]))) {
return;
}
throw new SecurityException("Requires one of the following permissions: "
- + String.join(", ", List.of(REGISTER_NSD_OFFLOAD_ENGINE, NETWORK_STACK,
- PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) + ".");
+ + String.join(", ", permissionsList) + ".");
}
}
@@ -2208,6 +2247,11 @@
}
private boolean registerService(int transactionId, NsdServiceInfo service) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "registerService: mMDnsManager is null");
+ return false;
+ }
+
if (DBG) {
Log.d(TAG, "registerService: " + transactionId + " " + service);
}
@@ -2225,10 +2269,19 @@
}
private boolean unregisterService(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "unregisterService: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean discoverServices(int transactionId, NsdServiceInfo serviceInfo) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "discoverServices: mMDnsManager is null");
+ return false;
+ }
+
final String type = serviceInfo.getServiceType();
final int discoverInterface = getNetworkInterfaceIndex(serviceInfo);
if (serviceInfo.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
@@ -2239,10 +2292,18 @@
}
private boolean stopServiceDiscovery(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopServiceDiscovery: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean resolveService(int transactionId, NsdServiceInfo service) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "resolveService: mMDnsManager is null");
+ return false;
+ }
final String name = service.getServiceName();
final String type = service.getServiceType();
final int resolveInterface = getNetworkInterfaceIndex(service);
@@ -2316,14 +2377,26 @@
}
private boolean stopResolveService(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopResolveService: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean getAddrInfo(int transactionId, String hostname, int interfaceIdx) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "getAddrInfo: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.getServiceAddress(transactionId, hostname, interfaceIdx);
}
private boolean stopGetAddrInfo(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopGetAddrInfo: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
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 0a6d8c1..9466162 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -15,6 +15,9 @@
*/
package com.android.server.connectivity.mdns;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
/**
* The class that contains mDNS feature flags;
*/
@@ -41,6 +44,19 @@
*/
public static final String NSD_LIMIT_LABEL_COUNT = "nsd_limit_label_count";
+ /**
+ * A feature flag to control whether the known-answer suppression should be enabled.
+ */
+ public static final String NSD_KNOWN_ANSWER_SUPPRESSION = "nsd_known_answer_suppression";
+
+ /**
+ * A feature flag to control whether unicast replies should be enabled.
+ *
+ * <p>Enabling this feature causes replies to queries with the Query Unicast (QU) flag set to be
+ * sent unicast instead of multicast, as per RFC6762 5.4.
+ */
+ public static final String NSD_UNICAST_REPLY_ENABLED = "nsd_unicast_reply_enabled";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -53,17 +69,56 @@
// Flag for label count limit
public final boolean mIsLabelCountLimitEnabled;
+ // Flag for known-answer suppression
+ public final boolean mIsKnownAnswerSuppressionEnabled;
+
+ // Flag to enable replying unicast to queries requesting unicast replies
+ public final boolean mIsUnicastReplyEnabled;
+
+ @Nullable
+ private final FlagOverrideProvider mOverrideProvider;
+
+ /**
+ * A provider that can indicate whether a flag should be force-enabled for testing purposes.
+ */
+ public interface FlagOverrideProvider {
+ /**
+ * Indicates whether the flag should be force-enabled for testing purposes.
+ */
+ boolean isForceEnabledForTest(@NonNull String flag);
+ }
+
+ /**
+ * Indicates whether the flag should be force-enabled for testing purposes.
+ */
+ private boolean isForceEnabledForTest(@NonNull String flag) {
+ return mOverrideProvider != null && mOverrideProvider.isForceEnabledForTest(flag);
+ }
+
+ /**
+ * Indicates whether {@link #NSD_UNICAST_REPLY_ENABLED} is enabled, including for testing.
+ */
+ public boolean isUnicastReplyEnabled() {
+ return mIsUnicastReplyEnabled || isForceEnabledForTest(NSD_UNICAST_REPLY_ENABLED);
+ }
+
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
boolean includeInetAddressRecordsInProbing,
boolean isExpiredServicesRemovalEnabled,
- boolean isLabelCountLimitEnabled) {
+ boolean isLabelCountLimitEnabled,
+ boolean isKnownAnswerSuppressionEnabled,
+ boolean isUnicastReplyEnabled,
+ @Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
+ mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
+ mIsUnicastReplyEnabled = isUnicastReplyEnabled;
+ mOverrideProvider = overrideProvider;
}
@@ -79,6 +134,9 @@
private boolean mIncludeInetAddressRecordsInProbing;
private boolean mIsExpiredServicesRemovalEnabled;
private boolean mIsLabelCountLimitEnabled;
+ private boolean mIsKnownAnswerSuppressionEnabled;
+ private boolean mIsUnicastReplyEnabled;
+ private FlagOverrideProvider mOverrideProvider;
/**
* The constructor for {@link Builder}.
@@ -88,6 +146,9 @@
mIncludeInetAddressRecordsInProbing = false;
mIsExpiredServicesRemovalEnabled = false;
mIsLabelCountLimitEnabled = true; // Default enabled.
+ mIsKnownAnswerSuppressionEnabled = false;
+ mIsUnicastReplyEnabled = true;
+ mOverrideProvider = null;
}
/**
@@ -132,13 +193,47 @@
}
/**
+ * Set whether the known-answer suppression is enabled.
+ *
+ * @see #NSD_KNOWN_ANSWER_SUPPRESSION
+ */
+ public Builder setIsKnownAnswerSuppressionEnabled(boolean isKnownAnswerSuppressionEnabled) {
+ mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
+ return this;
+ }
+
+ /**
+ * Set whether the unicast reply feature is enabled.
+ *
+ * @see #NSD_UNICAST_REPLY_ENABLED
+ */
+ public Builder setIsUnicastReplyEnabled(boolean isUnicastReplyEnabled) {
+ mIsUnicastReplyEnabled = isUnicastReplyEnabled;
+ return this;
+ }
+
+ /**
+ * Set a {@link FlagOverrideProvider} to be used by {@link #isForceEnabledForTest(String)}.
+ *
+ * If non-null, features that use {@link #isForceEnabledForTest(String)} will use that
+ * provider to query whether the flag should be force-enabled.
+ */
+ public Builder setOverrideProvider(@Nullable FlagOverrideProvider overrideProvider) {
+ mOverrideProvider = overrideProvider;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled,
mIncludeInetAddressRecordsInProbing,
mIsExpiredServicesRemovalEnabled,
- mIsLabelCountLimitEnabled);
+ mIsLabelCountLimitEnabled,
+ mIsKnownAnswerSuppressionEnabled,
+ mIsUnicastReplyEnabled,
+ mOverrideProvider);
}
}
}
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 aa40c92..3a04dcd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -373,12 +373,14 @@
}
return;
}
+ // 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 " + src);
+ + packet.additionalRecords.size() + " additional from " + srcCopy);
}
for (int conflictServiceId : mRecordRepository.getConflictingServices(packet)) {
@@ -389,7 +391,7 @@
// happen when the incoming packet has answer records (not a question), so there will be no
// 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, src);
+ final MdnsReplyInfo answers = mRecordRepository.getReply(packet, srcCopy);
if (answers == null) return;
mReplySender.queueReply(answers);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index e7b0eaa..869ac9b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -96,7 +96,8 @@
@Override
public void onInterfaceDestroyed(@NonNull SocketKey socketKey,
@NonNull MdnsInterfaceSocket socket) {
- notifySocketDestroyed(socketKey);
+ mActiveSockets.remove(socketKey);
+ mSocketCreationCallback.onSocketDestroyed(socketKey);
maybeCleanupPacketHandler(socketKey);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
index 28bd1b4..4b43989 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -176,6 +176,16 @@
}
/**
+ * For questions, returns whether a unicast reply was requested.
+ *
+ * In practice this is identical to {@link #getCacheFlush()}, as the "cache flush" flag in
+ * replies is the same as "unicast reply requested" in questions.
+ */
+ public final boolean isUnicastReplyRequested() {
+ return (cls & MdnsConstants.QCLASS_UNICAST) != 0;
+ }
+
+ /**
* Returns the record's remaining TTL.
*
* If the record was not sent yet (receipt time {@link #RECEIPT_TIME_NOT_SENT}), this is the
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 1909208..585b097 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -46,6 +46,7 @@
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -90,6 +91,7 @@
private final Looper mLooper;
@NonNull
private final String[] mDeviceHostname;
+ @NonNull
private final MdnsFeatureFlags mMdnsFeatureFlags;
public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname,
@@ -139,6 +141,9 @@
* Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast,
* 0 if never
*/
+ // FIXME: the `lastSentTimeMs` and `lastAdvertisedTimeMs` should be maintained separately
+ // for IPv4 and IPv6, because neither IPv4 nor and IPv6 clients can receive replies in
+ // different address space.
public long lastSentTimeMs;
RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
@@ -316,7 +321,6 @@
*
* @param serviceId An existing service ID.
* @param subtypes New subtypes
- * @return
*/
public void updateService(int serviceId, @NonNull Set<String> subtypes) {
final ServiceRegistration existingRegistration = mServices.get(serviceId);
@@ -495,28 +499,56 @@
@Nullable
public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
final long now = SystemClock.elapsedRealtime();
- final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
- final ArrayList<MdnsRecord> additionalAnswerRecords = new ArrayList<>();
- final ArrayList<RecordInfo<?>> answerInfo = new ArrayList<>();
+
+ // Use LinkedHashSet for preserving the insert order of the RRs, so that RRs of the same
+ // service or host are grouped together (which is more developer-friendly).
+ final Set<RecordInfo<?>> answerInfo = new LinkedHashSet<>();
+ final Set<RecordInfo<?>> additionalAnswerInfo = new LinkedHashSet<>();
+ // Reply unicast if the feature is enabled AND all replied questions request unicast
+ final boolean replyUnicastEnabled = mMdnsFeatureFlags.isUnicastReplyEnabled();
+ boolean replyUnicast = replyUnicastEnabled;
for (MdnsRecord question : packet.questions) {
// Add answers from general records
- addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
- null /* serviceSrvRecord */, null /* serviceTxtRecord */, replyUnicast, now,
- answerInfo, additionalAnswerRecords);
+ if (addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
+ null /* serviceSrvRecord */, null /* serviceTxtRecord */, replyUnicastEnabled,
+ now, answerInfo, additionalAnswerInfo, Collections.emptyList())) {
+ replyUnicast &= question.isUnicastReplyRequested();
+ }
// Add answers from each service
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
if (registration.exiting || registration.isProbing) continue;
if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
- registration.srvRecord, registration.txtRecord, replyUnicast, now,
- answerInfo, additionalAnswerRecords)) {
+ registration.srvRecord, registration.txtRecord, replyUnicastEnabled, now,
+ answerInfo, additionalAnswerInfo, packet.answers)) {
+ replyUnicast &= question.isUnicastReplyRequested();
registration.repliedServiceCount++;
registration.sentPacketCount++;
}
}
}
+ // If any record was already in the answer section, remove it from the additional answer
+ // section. This can typically happen when there are both queries for
+ // SRV / TXT / A / AAAA and PTR (which can cause SRV / TXT / A / AAAA records being added
+ // to the additional answer section).
+ additionalAnswerInfo.removeAll(answerInfo);
+
+ final List<MdnsRecord> additionalAnswerRecords =
+ new ArrayList<>(additionalAnswerInfo.size());
+ for (RecordInfo<?> info : additionalAnswerInfo) {
+ additionalAnswerRecords.add(info.record);
+ }
+
+ // RFC6762 6.1: negative responses
+ // "On receipt of a question for a particular name, rrtype, and rrclass, for which a
+ // responder does have one or more unique answers, the responder MAY also include an NSEC
+ // record in the Additional Record Section indicating the nonexistence of other rrtypes
+ // for that name and rrclass."
+ addNsecRecordsForUniqueNames(additionalAnswerRecords,
+ answerInfo.iterator(), additionalAnswerInfo.iterator());
+
if (answerInfo.size() == 0 && additionalAnswerRecords.size() == 0) {
return null;
}
@@ -542,6 +574,12 @@
// Determine the send destination
final InetSocketAddress dest;
if (replyUnicast) {
+ // As per RFC6762 5.4, "if the responder has not multicast that record recently (within
+ // one quarter of its TTL), then the responder SHOULD instead multicast the response so
+ // as to keep all the peer caches up to date": this SHOULD is not implemented to
+ // minimize latency for queriers who have just started, so they did not receive previous
+ // multicast responses. Unicast replies are faster as they do not need to wait for the
+ // beacon interval on Wi-Fi.
dest = src;
} else if (src.getAddress() instanceof Inet4Address) {
dest = IPV4_SOCKET_ADDR;
@@ -563,6 +601,15 @@
return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
}
+ private boolean isKnownAnswer(MdnsRecord answer, @NonNull List<MdnsRecord> knownAnswerRecords) {
+ for (MdnsRecord knownAnswer : knownAnswerRecords) {
+ if (answer.equals(knownAnswer) && knownAnswer.getTtl() > (answer.getTtl() / 2)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Add answers and additional answers for a question, from a ServiceRegistration.
*/
@@ -571,14 +618,15 @@
@Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
@Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
- boolean replyUnicast, long now, @NonNull List<RecordInfo<?>> answerInfo,
- @NonNull List<MdnsRecord> additionalAnswerRecords) {
+ boolean replyUnicastEnabled, long now, @NonNull Set<RecordInfo<?>> answerInfo,
+ @NonNull Set<RecordInfo<?>> additionalAnswerInfo,
+ @NonNull List<MdnsRecord> knownAnswerRecords) {
boolean hasDnsSdPtrRecordAnswer = false;
boolean hasDnsSdSrvRecordAnswer = false;
boolean hasFullyOwnedNameMatch = false;
boolean hasKnownAnswer = false;
- final int answersStartIndex = answerInfo.size();
+ final int answersStartSize = answerInfo.size();
for (RecordInfo<?> info : serviceRecords) {
/* RFC6762 6.: the record name must match the question name, the record rrtype
@@ -601,19 +649,32 @@
}
hasKnownAnswer = true;
+
+ // RFC6762 7.1. Known-Answer Suppression:
+ // A Multicast DNS responder MUST NOT answer a Multicast DNS query if
+ // the answer it would give is already included in the Answer Section
+ // with an RR TTL at least half the correct value. If the RR TTL of the
+ // answer as given in the Answer Section is less than half of the true
+ // RR TTL as known by the Multicast DNS responder, the responder MUST
+ // send an answer so as to update the querier's cache before the record
+ // becomes in danger of expiration.
+ if (mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled
+ && isKnownAnswer(info.record, knownAnswerRecords)) {
+ continue;
+ }
+
hasDnsSdPtrRecordAnswer |= (servicePtrRecords != null
&& CollectionUtils.any(servicePtrRecords, r -> info == r));
hasDnsSdSrvRecordAnswer |= (info == serviceSrvRecord);
// TODO: responses to probe queries should bypass this check and only ensure the
// reply is sent 250ms after the last sent time (RFC 6762 p.15)
- if (!replyUnicast && info.lastAdvertisedTimeMs > 0L
+ if (!(replyUnicastEnabled && question.isUnicastReplyRequested())
+ && info.lastAdvertisedTimeMs > 0L
&& now - info.lastAdvertisedTimeMs < MIN_MULTICAST_REPLY_INTERVAL_MS) {
continue;
}
- // TODO: Don't reply if in known answers of the querier (7.1) if TTL is > half
-
answerInfo.add(info);
}
@@ -622,7 +683,7 @@
// ownership, for a type for which that name has no records, the responder MUST [...]
// respond asserting the nonexistence of that record"
if (hasFullyOwnedNameMatch && !hasKnownAnswer) {
- additionalAnswerRecords.add(new MdnsNsecRecord(
+ MdnsNsecRecord nsecRecord = new MdnsNsecRecord(
question.getName(),
0L /* receiptTimeMillis */,
true /* cacheFlush */,
@@ -630,13 +691,14 @@
// be the same as the TTL that the record would have had, had it existed."
NAME_RECORDS_TTL_MILLIS,
question.getName(),
- new int[] { question.getType() }));
+ new int[] { question.getType() });
+ additionalAnswerInfo.add(
+ new RecordInfo<>(null /* serviceInfo */, nsecRecord, false /* isSharedName */));
}
// No more records to add if no answer
- if (answerInfo.size() == answersStartIndex) return false;
+ if (answerInfo.size() == answersStartSize) return false;
- final List<RecordInfo<?>> additionalAnswerInfo = new ArrayList<>();
// RFC6763 12.1: if including PTR record, include the SRV and TXT records it names
if (hasDnsSdPtrRecordAnswer) {
if (serviceTxtRecord != null) {
@@ -655,15 +717,6 @@
}
}
}
-
- for (RecordInfo<?> info : additionalAnswerInfo) {
- additionalAnswerRecords.add(info.record);
- }
-
- // RFC6762 6.1: negative responses
- addNsecRecordsForUniqueNames(additionalAnswerRecords,
- answerInfo.listIterator(answersStartIndex),
- additionalAnswerInfo.listIterator());
return true;
}
@@ -680,7 +733,7 @@
* answer and additionalAnswer sections)
*/
@SafeVarargs
- private static void addNsecRecordsForUniqueNames(
+ private void addNsecRecordsForUniqueNames(
List<MdnsRecord> destinationList,
Iterator<RecordInfo<?>>... answerRecords) {
// Group unique records by name. Use a TreeMap with comparator as arrays don't implement
@@ -696,6 +749,12 @@
for (String[] nsecName : namesInAddedOrder) {
final List<MdnsRecord> entryRecords = nsecByName.get(nsecName);
+
+ // Add NSEC records only when the answers include all unique records of this name
+ if (entryRecords.size() != countUniqueRecords(nsecName)) {
+ continue;
+ }
+
long minTtl = Long.MAX_VALUE;
final Set<Integer> types = new ArraySet<>(entryRecords.size());
for (MdnsRecord record : entryRecords) {
@@ -713,6 +772,27 @@
}
}
+ /** Returns the number of unique records on this device for a given {@code name}. */
+ private int countUniqueRecords(String[] name) {
+ int cnt = countUniqueRecords(mGeneralRecords, name);
+
+ for (int i = 0; i < mServices.size(); i++) {
+ final ServiceRegistration registration = mServices.valueAt(i);
+ cnt += countUniqueRecords(registration.allRecords, name);
+ }
+ return cnt;
+ }
+
+ private static int countUniqueRecords(List<RecordInfo<?>> records, String[] name) {
+ int cnt = 0;
+ for (RecordInfo<?> record : records) {
+ if (!record.isSharedName && Arrays.equals(name, record.record.getName())) {
+ cnt++;
+ }
+ }
+ return cnt;
+ }
+
/**
* Add non-shared records to a map listing them by record name, and to a list of names that
* remembers the adding order.
@@ -727,10 +807,10 @@
private static void addNonSharedRecordsToMap(
Iterator<RecordInfo<?>> records,
Map<String[], List<MdnsRecord>> dest,
- List<String[]> namesInAddedOrder) {
+ @Nullable List<String[]> namesInAddedOrder) {
while (records.hasNext()) {
final RecordInfo<?> record = records.next();
- if (record.isSharedName) continue;
+ if (record.isSharedName || record.record instanceof MdnsNsecRecord) continue;
final List<MdnsRecord> recordsForName = dest.computeIfAbsent(record.record.name,
key -> {
namesInAddedOrder.add(key);
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 ea3af5e..651b643 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -25,6 +25,7 @@
import android.os.Looper;
import android.os.Message;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -57,15 +58,46 @@
@NonNull
private final SharedLog mSharedLog;
private final boolean mEnableDebugLog;
+ @NonNull
+ private final Dependencies mDependencies;
+
+ /**
+ * Dependencies of MdnsReplySender, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * @see Handler#sendMessageDelayed(Message, long)
+ */
+ public void sendMessageDelayed(@NonNull Handler handler, @NonNull Message message,
+ long delayMillis) {
+ handler.sendMessageDelayed(message, delayMillis);
+ }
+
+ /**
+ * @see Handler#removeMessages(int)
+ */
+ public void removeMessages(@NonNull Handler handler, int what) {
+ handler.removeMessages(what);
+ }
+ }
public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
@NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
boolean enableDebugLog) {
+ this(looper, socket, packetCreationBuffer, sharedLog, enableDebugLog, new Dependencies());
+ }
+
+ @VisibleForTesting
+ public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
+ @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
+ boolean enableDebugLog, @NonNull Dependencies dependencies) {
mHandler = new SendHandler(looper);
mSocket = socket;
mPacketCreationBuffer = packetCreationBuffer;
mSharedLog = sharedLog;
mEnableDebugLog = enableDebugLog;
+ mDependencies = dependencies;
}
/**
@@ -74,7 +106,8 @@
public void queueReply(@NonNull MdnsReplyInfo reply) {
ensureRunningOnHandlerThread(mHandler);
// TODO: implement response aggregation (RFC 6762 6.4)
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
+ mDependencies.sendMessageDelayed(
+ mHandler, mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
if (mEnableDebugLog) {
mSharedLog.v("Scheduling " + reply);
@@ -104,7 +137,7 @@
*/
public void cancelAll() {
ensureRunningOnHandlerThread(mHandler);
- mHandler.removeMessages(MSG_SEND);
+ mDependencies.removeMessages(mHandler, MSG_SEND);
}
private class SendHandler extends Handler {
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 32f604e..df0a040 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -541,6 +541,9 @@
}
if (response.isComplete()) {
+ // There is a bug here: the newServiceFound is global right now. The state needs
+ // to be per listener because of the responseMatchesOptions() filter.
+ // Otherwise, it won't handle the subType update properly.
if (newServiceFound || serviceBecomesComplete) {
sharedLog.log("onServiceFound: " + serviceInfo);
listener.onServiceFound(serviceInfo, false /* isServiceFromCache */);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
index 3cd77a4..70451f3 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
@@ -42,6 +42,12 @@
private final Set<PacketHandler> mPacketHandlers = MdnsUtils.newSet();
interface PacketHandler {
+ /**
+ * Handle an incoming packet.
+ *
+ * The recvbuf and src <b>will be reused and modified</b> after this method returns, so
+ * implementers must ensure that they are not accessed after handlePacket returns.
+ */
void handlePacket(byte[] recvbuf, int length, InetSocketAddress src);
}
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 48e86d8..458d64f 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -237,7 +238,18 @@
mDeps = deps;
// Interface match regex.
- mIfaceMatch = mDeps.getInterfaceRegexFromResource(mContext);
+ String ifaceMatchRegex = mDeps.getInterfaceRegexFromResource(mContext);
+ // "*" is a magic string to indicate "pick the default".
+ if (ifaceMatchRegex.equals("*")) {
+ if (SdkLevel.isAtLeastV()) {
+ // On V+, include both usb%d and eth%d interfaces.
+ ifaceMatchRegex = "(usb|eth)\\d+";
+ } else {
+ // On T and U, include only eth%d interfaces.
+ ifaceMatchRegex = "eth\\d+";
+ }
+ }
+ mIfaceMatch = ifaceMatchRegex;
// Read default Ethernet interface configuration from resources
final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
index 27c0f9f..4ec1562 100644
--- a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
+++ b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
@@ -66,7 +66,7 @@
/** Create BpfMap for updating interface and index mapping. */
public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
try {
- return new BpfMap<>(IFACE_INDEX_NAME_MAP_PATH, BpfMap.BPF_F_RDWR,
+ return new BpfMap<>(IFACE_INDEX_NAME_MAP_PATH,
S32.class, InterfaceMapValue.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create interface map: " + e);
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index 3da1585..8ee8591 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -22,6 +22,7 @@
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
import android.net.NetworkStats.NonMonotonicObserver;
@@ -32,17 +33,20 @@
import android.net.TrafficStats;
import android.os.Binder;
import android.os.DropBoxManager;
+import android.os.SystemClock;
import android.service.NetworkStatsRecorderProto;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
import com.android.internal.util.FileRotator;
+import com.android.metrics.NetworkStatsMetricsLogger;
import com.android.net.module.util.NetworkStatsUtils;
import libcore.io.IoUtils;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -79,6 +83,7 @@
private final long mBucketDuration;
private final boolean mOnlyTags;
private final boolean mWipeOnError;
+ private final boolean mUseFastDataInput;
private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
private NetworkStats mLastSnapshot;
@@ -89,6 +94,9 @@
private final CombiningRewriter mPendingRewriter;
private WeakReference<NetworkStatsCollection> mComplete;
+ private final NetworkStatsMetricsLogger mMetricsLogger = new NetworkStatsMetricsLogger();
+ @Nullable
+ private final File mStatsDir;
/**
* Non-persisted recorder, with only one bucket. Used by {@link NetworkStatsObservers}.
@@ -104,11 +112,13 @@
mBucketDuration = YEAR_IN_MILLIS;
mOnlyTags = false;
mWipeOnError = true;
+ mUseFastDataInput = false;
mPending = null;
mSinceBoot = new NetworkStatsCollection(mBucketDuration);
mPendingRewriter = null;
+ mStatsDir = null;
}
/**
@@ -116,7 +126,7 @@
*/
public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags,
- boolean wipeOnError) {
+ boolean wipeOnError, boolean useFastDataInput, @Nullable File statsDir) {
mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
@@ -125,11 +135,13 @@
mBucketDuration = bucketDuration;
mOnlyTags = onlyTags;
mWipeOnError = wipeOnError;
+ mUseFastDataInput = useFastDataInput;
mPending = new NetworkStatsCollection(bucketDuration);
mSinceBoot = new NetworkStatsCollection(bucketDuration);
mPendingRewriter = new CombiningRewriter(mPending);
+ mStatsDir = statsDir;
}
public void setPersistThreshold(long thresholdBytes) {
@@ -179,8 +191,16 @@
Objects.requireNonNull(mRotator, "missing FileRotator");
NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
if (res == null) {
+ final long readStart = SystemClock.elapsedRealtime();
res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE);
mComplete = new WeakReference<NetworkStatsCollection>(res);
+ final long readEnd = SystemClock.elapsedRealtime();
+ // For legacy recorders which are used for data integrity check, which
+ // have wipeOnError flag unset, skip reporting metrics.
+ if (mWipeOnError) {
+ mMetricsLogger.logRecorderFileReading(mCookie, (int) (readEnd - readStart),
+ mStatsDir, res, mUseFastDataInput);
+ }
}
return res;
}
@@ -195,8 +215,12 @@
}
private NetworkStatsCollection loadLocked(long start, long end) {
- if (LOGD) Log.d(TAG, "loadLocked() reading from disk for " + mCookie);
- final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration);
+ if (LOGD) {
+ Log.d(TAG, "loadLocked() reading from disk for " + mCookie
+ + " useFastDataInput: " + mUseFastDataInput);
+ }
+ final NetworkStatsCollection res =
+ new NetworkStatsCollection(mBucketDuration, mUseFastDataInput);
try {
mRotator.readMatching(res, start, end);
res.recordCollection(mPending);
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 2c9f30c..78c4504 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -44,7 +44,6 @@
import static android.net.NetworkStats.TAG_ALL;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
-import static android.net.NetworkStatsCollection.compareStats;
import static android.net.NetworkStatsHistory.FIELD_ALL;
import static android.net.NetworkTemplate.MATCH_MOBILE;
import static android.net.NetworkTemplate.MATCH_TEST;
@@ -295,6 +294,11 @@
static final String CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER =
"enable_network_stats_event_logger";
+ static final String NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS =
+ "netstats_fastdatainput_target_attempts";
+ static final String NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME = "fastdatainput.successes";
+ static final String NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME = "fastdatainput.fallbacks";
+
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
private final AlarmManager mAlarmManager;
@@ -318,6 +322,8 @@
private PersistentInt mImportLegacyAttemptsCounter = null;
private PersistentInt mImportLegacySuccessesCounter = null;
private PersistentInt mImportLegacyFallbacksCounter = null;
+ private PersistentInt mFastDataInputSuccessesCounter = null;
+ private PersistentInt mFastDataInputFallbacksCounter = null;
@VisibleForTesting
public static final String ACTION_NETWORK_STATS_POLL =
@@ -475,6 +481,8 @@
@Nullable
private final SkDestroyListener mSkDestroyListener;
+ private static final int MAX_SOCKET_DESTROY_LISTENER_LOGS = 20;
+
private static @NonNull Clock getDefaultClock() {
return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
Clock.systemUTC());
@@ -486,9 +494,10 @@
*/
private static class OpenSessionKey {
public final int uid;
+ @Nullable
public final String packageName;
- OpenSessionKey(int uid, @NonNull String packageName) {
+ OpenSessionKey(int uid, @Nullable String packageName) {
this.uid = uid;
this.packageName = packageName;
}
@@ -695,6 +704,24 @@
}
/**
+ * Get the count of using FastDataInput target attempts.
+ */
+ public int getUseFastDataInputTargetAttempts() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(
+ DeviceConfig.NAMESPACE_TETHERING,
+ NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS, 0);
+ }
+
+ /**
+ * Compare two {@link NetworkStatsCollection} instances and returning a human-readable
+ * string description of difference for debugging purpose.
+ */
+ public String compareStats(@NonNull NetworkStatsCollection a,
+ @NonNull NetworkStatsCollection b, boolean allowKeyChange) {
+ return NetworkStatsCollection.compareStats(a, b, allowKeyChange);
+ }
+
+ /**
* Create a persistent counter for given directory and name.
*/
public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name)
@@ -781,7 +808,7 @@
/** Get counter sets map for each UID. */
public IBpfMap<S32, U8> getUidCounterSetMap() {
try {
- return new BpfMap<S32, U8>(UID_COUNTERSET_MAP_PATH, BpfMap.BPF_F_RDWR,
+ return new BpfMap<S32, U8>(UID_COUNTERSET_MAP_PATH,
S32.class, U8.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open uid counter set map: " + e);
@@ -793,7 +820,7 @@
public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
try {
return new BpfMap<CookieTagMapKey, CookieTagMapValue>(COOKIE_TAG_MAP_PATH,
- BpfMap.BPF_F_RDWR, CookieTagMapKey.class, CookieTagMapValue.class);
+ CookieTagMapKey.class, CookieTagMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open cookie tag map: " + e);
return null;
@@ -804,7 +831,7 @@
public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
try {
return new BpfMap<StatsMapKey, StatsMapValue>(STATS_MAP_A_PATH,
- BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class);
+ StatsMapKey.class, StatsMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open stats map A: " + e);
return null;
@@ -815,7 +842,7 @@
public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
try {
return new BpfMap<StatsMapKey, StatsMapValue>(STATS_MAP_B_PATH,
- BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class);
+ StatsMapKey.class, StatsMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open stats map B: " + e);
return null;
@@ -826,7 +853,7 @@
public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
try {
return new BpfMap<UidStatsMapKey, StatsMapValue>(APP_UID_STATS_MAP_PATH,
- BpfMap.BPF_F_RDWR, UidStatsMapKey.class, StatsMapValue.class);
+ UidStatsMapKey.class, StatsMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open app uid stats map: " + e);
return null;
@@ -837,7 +864,7 @@
public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
try {
return new BpfMap<S32, StatsMapValue>(IFACE_STATS_MAP_PATH,
- BpfMap.BPF_F_RDWR, S32.class, StatsMapValue.class);
+ S32.class, StatsMapValue.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Failed to open interface stats map", e);
}
@@ -856,7 +883,8 @@
/** Create a new SkDestroyListener. */
public SkDestroyListener makeSkDestroyListener(
IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
- return new SkDestroyListener(cookieTagMap, handler, new SharedLog(TAG));
+ return new SkDestroyListener(
+ cookieTagMap, handler, new SharedLog(MAX_SOCKET_DESTROY_LISTENER_LOGS, TAG));
}
/**
@@ -892,13 +920,7 @@
synchronized (mStatsLock) {
mSystemReady = true;
- // create data recorders along with historical rotators
- mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
- true /* wipeOnError */);
- mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
- true /* wipeOnError */);
- mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
- mStatsDir, true /* wipeOnError */);
+ makeRecordersLocked();
updatePersistThresholdsLocked();
@@ -963,13 +985,106 @@
private NetworkStatsRecorder buildRecorder(
String prefix, NetworkStatsSettings.Config config, boolean includeTags,
- File baseDir, boolean wipeOnError) {
+ File baseDir, boolean wipeOnError, boolean useFastDataInput) {
final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
Context.DROPBOX_SERVICE);
return new NetworkStatsRecorder(new FileRotator(
baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags,
- wipeOnError);
+ wipeOnError, useFastDataInput, baseDir);
+ }
+
+ @GuardedBy("mStatsLock")
+ private void makeRecordersLocked() {
+ boolean useFastDataInput = true;
+ try {
+ mFastDataInputSuccessesCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME);
+ mFastDataInputFallbacksCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME);
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
+ useFastDataInput = false;
+ }
+
+ final int targetAttempts = mDeps.getUseFastDataInputTargetAttempts();
+ int successes = 0;
+ int fallbacks = 0;
+ try {
+ successes = mFastDataInputSuccessesCounter.get();
+ // Fallbacks counter would be set to non-zero value to indicate the reading was
+ // not successful.
+ fallbacks = mFastDataInputFallbacksCounter.get();
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to read counters, skip.", e);
+ useFastDataInput = false;
+ }
+
+ final boolean doComparison;
+ if (useFastDataInput) {
+ // Use FastDataInput if it needs to be evaluated or at least one success.
+ doComparison = targetAttempts > successes + fallbacks;
+ // Set target attempt to -1 as the kill switch to disable the feature.
+ useFastDataInput = targetAttempts >= 0 && (doComparison || successes > 0);
+ } else {
+ // useFastDataInput is false due to previous failures.
+ doComparison = false;
+ }
+
+ // create data recorders along with historical rotators.
+ // Don't wipe on error if comparison is needed.
+ mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+ !doComparison /* wipeOnError */, useFastDataInput);
+ mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+ !doComparison /* wipeOnError */, useFastDataInput);
+ mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
+ mStatsDir, !doComparison /* wipeOnError */, useFastDataInput);
+
+ if (!doComparison) return;
+
+ final MigrationInfo[] migrations = new MigrationInfo[]{
+ new MigrationInfo(mXtRecorder),
+ new MigrationInfo(mUidRecorder),
+ new MigrationInfo(mUidTagRecorder)
+ };
+ // Set wipeOnError flag false so the recorder won't damage persistent data if reads
+ // failed and calling deleteAll.
+ final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
+ buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */),
+ buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */),
+ buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */)};
+ boolean success = true;
+ for (int i = 0; i < migrations.length; i++) {
+ try {
+ migrations[i].collection = migrations[i].recorder.getOrLoadCompleteLocked();
+ } catch (Throwable t) {
+ Log.wtf(TAG, "Failed to load collection, skip.", t);
+ success = false;
+ break;
+ }
+ if (!compareImportedToLegacyStats(migrations[i], legacyRecorders[i],
+ false /* allowKeyChange */)) {
+ success = false;
+ break;
+ }
+ }
+
+ try {
+ if (success) {
+ mFastDataInputSuccessesCounter.set(successes + 1);
+ } else {
+ // Fallback.
+ mXtRecorder = legacyRecorders[0];
+ mUidRecorder = legacyRecorders[1];
+ mUidTagRecorder = legacyRecorders[2];
+ mFastDataInputFallbacksCounter.set(fallbacks + 1);
+ }
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to update counters. success = " + success, e);
+ }
}
@GuardedBy("mStatsLock")
@@ -1068,7 +1183,7 @@
new NetworkStatsSettings.Config(HOUR_IN_MILLIS,
15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS);
final NetworkStatsRecorder devRecorder = buildRecorder(PREFIX_DEV, devConfig,
- false, mStatsDir, true /* wipeOnError */);
+ false, mStatsDir, true /* wipeOnError */, false /* useFastDataInput */);
final MigrationInfo[] migrations = new MigrationInfo[]{
new MigrationInfo(devRecorder), new MigrationInfo(mXtRecorder),
new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
@@ -1085,11 +1200,11 @@
legacyRecorders = new NetworkStatsRecorder[]{
null /* dev Recorder */,
buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir,
- false /* wipeOnError */),
+ false /* wipeOnError */, false /* useFastDataInput */),
buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir,
- false /* wipeOnError */),
+ false /* wipeOnError */, false /* useFastDataInput */),
buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir,
- false /* wipeOnError */)};
+ false /* wipeOnError */, false /* useFastDataInput */)};
} else {
legacyRecorders = null;
}
@@ -1120,7 +1235,8 @@
if (runComparison) {
final boolean success =
- compareImportedToLegacyStats(migration, legacyRecorders[i]);
+ compareImportedToLegacyStats(migration, legacyRecorders[i],
+ true /* allowKeyChange */);
if (!success && !dryRunImportOnly) {
tryIncrementLegacyFallbacksCounter();
}
@@ -1243,7 +1359,7 @@
* does not match or throw with exceptions.
*/
private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration,
- @Nullable NetworkStatsRecorder legacyRecorder) {
+ @Nullable NetworkStatsRecorder legacyRecorder, boolean allowKeyChange) {
final NetworkStatsCollection legacyStats;
// Skip the recorder that doesn't need to be compared.
if (legacyRecorder == null) return true;
@@ -1258,7 +1374,8 @@
// The result of comparison is only for logging.
try {
- final String error = compareStats(migration.collection, legacyStats);
+ final String error = mDeps.compareStats(migration.collection, legacyStats,
+ allowKeyChange);
if (error != null) {
Log.wtf(TAG, "Unexpected comparison result for recorder "
+ legacyRecorder.getCookie() + ": " + error);
@@ -1348,7 +1465,7 @@
return now - lastCallTime < POLL_RATE_LIMIT_MS;
}
- private int restrictFlagsForCaller(int flags, @NonNull String callingPackage) {
+ private int restrictFlagsForCaller(int flags, @Nullable String callingPackage) {
// All non-privileged callers are not allowed to turn off POLL_ON_OPEN.
final boolean isPrivileged = PermissionUtils.checkAnyPermissionOf(mContext,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
@@ -1365,7 +1482,8 @@
return flags;
}
- private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) {
+ private INetworkStatsSession openSessionInternal(
+ final int flags, @Nullable final String callingPackage) {
final int restrictedFlags = restrictFlagsForCaller(flags, callingPackage);
if ((restrictedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN
| NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
@@ -1382,6 +1500,7 @@
return new INetworkStatsSession.Stub() {
private final int mCallingUid = Binder.getCallingUid();
+ @Nullable
private final String mCallingPackage = callingPackage;
private final @NetworkStatsAccess.Level int mAccessLevel = checkAccessLevel(
callingPackage);
@@ -1520,7 +1639,7 @@
}
private void enforceTemplatePermissions(@NonNull NetworkTemplate template,
- @NonNull String callingPackage) {
+ @Nullable String callingPackage) {
// For a template with wifi network keys, it is possible for a malicious
// client to track the user locations via querying data usage. Thus, enforce
// fine location permission check.
@@ -1541,7 +1660,7 @@
}
}
- private @NetworkStatsAccess.Level int checkAccessLevel(String callingPackage) {
+ private @NetworkStatsAccess.Level int checkAccessLevel(@Nullable String callingPackage) {
return NetworkStatsAccess.checkAccessLevel(
mContext, Binder.getCallingPid(), Binder.getCallingUid(), callingPackage);
}
@@ -1868,36 +1987,56 @@
if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
return UNSUPPORTED;
}
- return nativeGetUidStat(uid, type);
+ return getEntryValueForType(nativeGetUidStat(uid), type);
}
@Override
public long getIfaceStats(@NonNull String iface, int type) {
Objects.requireNonNull(iface);
- long nativeIfaceStats = nativeGetIfaceStat(iface, type);
- if (nativeIfaceStats == -1) {
- return nativeIfaceStats;
+ final NetworkStats.Entry entry = nativeGetIfaceStat(iface);
+ final long value = getEntryValueForType(entry, type);
+ if (value == UNSUPPORTED) {
+ return UNSUPPORTED;
} else {
// When tethering offload is in use, nativeIfaceStats does not contain usage from
// offload, add it back here. Note that the included statistics might be stale
// since polling newest stats from hardware might impact system health and not
// suitable for TrafficStats API use cases.
- return nativeIfaceStats + getProviderIfaceStats(iface, type);
+ entry.add(getProviderIfaceStats(iface));
+ return getEntryValueForType(entry, type);
+ }
+ }
+
+ private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
+ if (entry == null) return UNSUPPORTED;
+ switch (type) {
+ case TrafficStats.TYPE_RX_BYTES:
+ return entry.rxBytes;
+ case TrafficStats.TYPE_TX_BYTES:
+ return entry.txBytes;
+ case TrafficStats.TYPE_RX_PACKETS:
+ return entry.rxPackets;
+ case TrafficStats.TYPE_TX_PACKETS:
+ return entry.txPackets;
+ default:
+ return UNSUPPORTED;
}
}
@Override
public long getTotalStats(int type) {
- long nativeTotalStats = nativeGetTotalStat(type);
- if (nativeTotalStats == -1) {
- return nativeTotalStats;
+ final NetworkStats.Entry entry = nativeGetTotalStat();
+ final long value = getEntryValueForType(entry, type);
+ if (value == UNSUPPORTED) {
+ return UNSUPPORTED;
} else {
// Refer to comment in getIfaceStats
- return nativeTotalStats + getProviderIfaceStats(IFACE_ALL, type);
+ entry.add(getProviderIfaceStats(IFACE_ALL));
+ return getEntryValueForType(entry, type);
}
}
- private long getProviderIfaceStats(@Nullable String iface, int type) {
+ private NetworkStats.Entry getProviderIfaceStats(@Nullable String iface) {
final NetworkStats providerSnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE);
final HashSet<String> limitIfaces;
if (iface == IFACE_ALL) {
@@ -1906,19 +2045,7 @@
limitIfaces = new HashSet<>();
limitIfaces.add(iface);
}
- final NetworkStats.Entry entry = providerSnapshot.getTotal(null, limitIfaces);
- switch (type) {
- case TrafficStats.TYPE_RX_BYTES:
- return entry.rxBytes;
- case TrafficStats.TYPE_RX_PACKETS:
- return entry.rxPackets;
- case TrafficStats.TYPE_TX_BYTES:
- return entry.txBytes;
- case TrafficStats.TYPE_TX_PACKETS:
- return entry.txPackets;
- default:
- return 0;
- }
+ return providerSnapshot.getTotal(null, limitIfaces);
}
/**
@@ -2639,6 +2766,17 @@
}
}
pw.println(CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER + ": " + mSupportEventLogger);
+ pw.print(NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS,
+ mDeps.getUseFastDataInputTargetAttempts());
+ pw.println();
+ try {
+ pw.print("FastDataInput successes", mFastDataInputSuccessesCounter.get());
+ pw.println();
+ pw.print("FastDataInput fallbacks", mFastDataInputFallbacksCounter.get());
+ pw.println();
+ } catch (IOException e) {
+ pw.println("(failed to dump FastDataInput counters)");
+ }
pw.decreaseIndent();
@@ -2777,6 +2915,12 @@
dumpStatsMapLocked(mStatsMapB, pw, "mStatsMapB");
dumpIfaceStatsMapLocked(pw);
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("SkDestroyListener logs:");
+ pw.increaseIndent();
+ mSkDestroyListener.dump(pw);
+ pw.decreaseIndent();
}
}
@@ -3274,10 +3418,13 @@
}
}
- private static native long nativeGetTotalStat(int type);
- private static native long nativeGetIfaceStat(String iface, int type);
- private static native long nativeGetIfIndexStat(int ifindex, int type);
- private static native long nativeGetUidStat(int uid, int type);
+ // TODO: Read stats by using BpfNetMapsReader.
+ @Nullable
+ private static native NetworkStats.Entry nativeGetTotalStat();
+ @Nullable
+ private static native NetworkStats.Entry nativeGetIfaceStat(String iface);
+ @Nullable
+ private static native NetworkStats.Entry nativeGetUidStat(int uid);
/** Initializes and registers the Perfetto Network Trace data source */
public static native void nativeInitNetworkTracing();
diff --git a/service-t/src/com/android/server/net/SkDestroyListener.java b/service-t/src/com/android/server/net/SkDestroyListener.java
index 7b68f89..a6cc2b5 100644
--- a/service-t/src/com/android/server/net/SkDestroyListener.java
+++ b/service-t/src/com/android/server/net/SkDestroyListener.java
@@ -30,6 +30,8 @@
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.
*/
@@ -72,4 +74,11 @@
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/Android.bp b/service/Android.bp
index e2dab9e..7c5da0d 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -70,6 +70,9 @@
apex_available: [
"com.android.tethering",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// The library name match the service-connectivity jarjar rules that put the JNI utils in the
@@ -200,7 +203,10 @@
apex_available: [
"com.android.tethering",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
visibility: [
"//packages/modules/Connectivity/service-t",
"//packages/modules/Connectivity/tests:__subpackages__",
@@ -225,6 +231,7 @@
],
lint: {
strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
},
}
@@ -283,12 +290,18 @@
java_library {
name: "service-connectivity-for-tests",
defaults: ["service-connectivity-defaults"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_library {
name: "service-connectivity",
defaults: ["service-connectivity-defaults"],
installable: true,
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_library_static {
@@ -303,6 +316,9 @@
],
static_libs: ["ConnectivityServiceprotos"],
apex_available: ["com.android.tethering"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
genrule {
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index f30abc6..6f9d46f 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -194,8 +194,11 @@
-->
</string-array>
- <!-- Regex of wired ethernet ifaces -->
- <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
+ <!-- Regex of wired ethernet ifaces. Network interfaces that match this regex will be tracked
+ by ethernet service.
+ If set to "*", ethernet service uses "(eth|usb)\\d+" on Android V+ and eth\\d+ on
+ Android T and U. -->
+ <string translatable="false" name="config_ethernet_iface_regex">*</string>
<!-- Ignores Wi-Fi validation failures after roam.
If validation fails on a Wi-Fi network after a roam to a new BSSID,
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
new file mode 100644
index 0000000..14b5427
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- These resources are around just to allow their values to be customized
+ for different hardware and product builds for Thread Network. All
+ configuration names should use the "config_thread" prefix.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Whether to use location APIs in the algorithm to determine country code or not.
+ If disabled, will use other sources (telephony, wifi, etc) to determine device location for
+ Thread Network regulatory purposes.
+ -->
+ <bool name="config_thread_location_use_for_country_code_enabled">true</bool>
+
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 4c85e8c..1c07599 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -43,6 +43,9 @@
<item type="string" name="config_ethernet_iface_regex"/>
<item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" />
<item type="integer" name="config_netstats_validate_import" />
+
+ <!-- Configuration values for ThreadNetworkService -->
+ <item type="bool" name="config_thread_location_use_for_country_code_enabled" />
</policy>
</overlayable>
</resources>
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index ad9cfbe..aa40285 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -187,7 +187,7 @@
private static IBpfMap<S32, U32> getConfigurationMap() {
try {
return new BpfMap<>(
- CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U32.class);
+ CONFIGURATION_MAP_PATH, S32.class, U32.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open netd configuration map", e);
}
@@ -196,7 +196,7 @@
private static IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
try {
return new BpfMap<>(
- UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, UidOwnerValue.class);
+ UID_OWNER_MAP_PATH, S32.class, UidOwnerValue.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open uid owner map", e);
}
@@ -205,7 +205,7 @@
private static IBpfMap<S32, U8> getUidPermissionMap() {
try {
return new BpfMap<>(
- UID_PERMISSION_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U8.class);
+ UID_PERMISSION_MAP_PATH, S32.class, U8.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open uid permission map", e);
}
@@ -213,7 +213,7 @@
private static IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
try {
- return new BpfMap<>(COOKIE_TAG_MAP_PATH, BpfMap.BPF_F_RDWR,
+ return new BpfMap<>(COOKIE_TAG_MAP_PATH,
CookieTagMapKey.class, CookieTagMapValue.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open cookie tag map", e);
@@ -223,7 +223,7 @@
private static IBpfMap<S32, U8> getDataSaverEnabledMap() {
try {
return new BpfMap<>(
- DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U8.class);
+ DATA_SAVER_ENABLED_MAP_PATH, S32.class, U8.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open data saver enabled map", e);
}
@@ -231,7 +231,7 @@
private static IBpfMap<IngressDiscardKey, IngressDiscardValue> getIngressDiscardMap() {
try {
- return new BpfMap<>(INGRESS_DISCARD_MAP_PATH, BpfMap.BPF_F_RDWR,
+ return new BpfMap<>(INGRESS_DISCARD_MAP_PATH,
IngressDiscardKey.class, IngressDiscardValue.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open ingress discard map", e);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0ec0f13..a995439 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -65,6 +65,7 @@
import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_SKIPPED;
import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID;
+import static android.net.MulticastRoutingConfig.FORWARD_NONE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
@@ -171,6 +172,7 @@
import android.net.LocalNetworkConfig;
import android.net.LocalNetworkInfo;
import android.net.MatchAllNetworkSpecifier;
+import android.net.MulticastRoutingConfig;
import android.net.NativeNetworkConfig;
import android.net.NativeNetworkType;
import android.net.NattSocketKeepalive;
@@ -290,6 +292,7 @@
import com.android.net.module.util.BpfUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
@@ -314,12 +317,12 @@
import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
import com.android.server.connectivity.DscpPolicyTracker;
import com.android.server.connectivity.FullScore;
-import com.android.server.connectivity.HandlerUtils;
import com.android.server.connectivity.InvalidTagException;
import com.android.server.connectivity.KeepaliveResourceUtil;
import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.LingerMonitor;
import com.android.server.connectivity.MockableSystemProperties;
+import com.android.server.connectivity.MulticastRoutingCoordinatorService;
import com.android.server.connectivity.MultinetworkPolicyTracker;
import com.android.server.connectivity.NetworkAgentInfo;
import com.android.server.connectivity.NetworkDiagnostics;
@@ -328,6 +331,7 @@
import com.android.server.connectivity.NetworkOffer;
import com.android.server.connectivity.NetworkPreferenceList;
import com.android.server.connectivity.NetworkRanker;
+import com.android.server.connectivity.NetworkRequestStateStatsMetrics;
import com.android.server.connectivity.PermissionMonitor;
import com.android.server.connectivity.ProfileNetworkPreferenceInfo;
import com.android.server.connectivity.ProxyTracker;
@@ -347,6 +351,7 @@
import java.io.PrintWriter;
import java.io.Writer;
import java.net.Inet4Address;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
@@ -361,6 +366,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
@@ -496,6 +502,7 @@
@GuardedBy("mTNSLock")
private TestNetworkService mTNS;
private final CompanionDeviceManagerProxyService mCdmps;
+ private final MulticastRoutingCoordinatorService mMulticastRoutingCoordinatorService;
private final RoutingCoordinatorService mRoutingCoordinatorService;
private final Object mTNSLock = new Object();
@@ -941,6 +948,8 @@
private final IpConnectivityLog mMetricsLog;
+ @Nullable private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+
@GuardedBy("mBandwidthRequests")
private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10);
@@ -1277,7 +1286,7 @@
LocalPriorityDump() {}
private void dumpHigh(FileDescriptor fd, PrintWriter pw) {
- if (!HandlerUtils.runWithScissors(mHandler, () -> {
+ if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> {
doDump(fd, pw, new String[]{DIAG_ARG});
doDump(fd, pw, new String[]{SHORT_ARG});
}, DUMPSYS_DEFAULT_TIMEOUT_MS)) {
@@ -1286,7 +1295,7 @@
}
private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) {
- if (!HandlerUtils.runWithScissors(mHandler, () -> doDump(fd, pw, args),
+ if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, pw, args),
DUMPSYS_DEFAULT_TIMEOUT_MS)) {
pw.println("dumpNormal timeout");
}
@@ -1421,6 +1430,30 @@
return new AutomaticOnOffKeepaliveTracker(c, h);
}
+ public MulticastRoutingCoordinatorService makeMulticastRoutingCoordinatorService(
+ @NonNull Handler h) {
+ try {
+ return new MulticastRoutingCoordinatorService(h);
+ } catch (UnsupportedOperationException e) {
+ // Multicast routing is not supported by the kernel
+ Log.i(TAG, "Skipping unsupported MulticastRoutingCoordinatorService");
+ return null;
+ }
+ }
+
+ /**
+ * @see NetworkRequestStateStatsMetrics
+ */
+ public NetworkRequestStateStatsMetrics makeNetworkRequestStateStatsMetrics(
+ Context context) {
+ // We currently have network requests metric for Watch devices only
+ if (context.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
+ return new NetworkRequestStateStatsMetrics();
+ } else {
+ return null;
+ }
+ }
+
/**
* @see BatteryStatsManager
*/
@@ -1654,6 +1687,7 @@
new RequestInfoPerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1);
mMetricsLog = logger;
+ mNetworkRequestStateStatsMetrics = mDeps.makeNetworkRequestStateStatsMetrics(mContext);
final NetworkRequest defaultInternetRequest = createDefaultRequest();
mDefaultRequest = new NetworkRequestInfo(
Process.myUid(), defaultInternetRequest, null,
@@ -1859,6 +1893,8 @@
}
mRoutingCoordinatorService = new RoutingCoordinatorService(netd);
+ mMulticastRoutingCoordinatorService =
+ mDeps.makeMulticastRoutingCoordinatorService(mHandler);
mDestroyFrozenSockets = mDeps.isAtLeastU()
&& mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
@@ -3004,26 +3040,6 @@
return false;
}
- private int getAppUid(final String app, final UserHandle user) {
- final PackageManager pm =
- mContext.createContextAsUser(user, 0 /* flags */).getPackageManager();
- final long token = Binder.clearCallingIdentity();
- try {
- return pm.getPackageUid(app, 0 /* flags */);
- } catch (PackageManager.NameNotFoundException e) {
- return -1;
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- private void verifyCallingUidAndPackage(String packageName, int callingUid) {
- final UserHandle user = UserHandle.getUserHandleForUid(callingUid);
- if (getAppUid(packageName, user) != callingUid) {
- throw new SecurityException(packageName + " does not belong to uid " + callingUid);
- }
- }
-
/**
* Ensure that a network route exists to deliver traffic to the specified
* host via the specified network interface.
@@ -3039,7 +3055,8 @@
if (disallowedBecauseSystemCaller()) {
return false;
}
- verifyCallingUidAndPackage(callingPackageName, mDeps.getCallingUid());
+ PermissionUtils.enforcePackageNameMatchesUid(
+ mContext, mDeps.getCallingUid(), callingPackageName);
enforceChangePermission(callingPackageName, callingAttributionTag);
if (mProtectedNetworks.contains(networkType)) {
enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
@@ -4034,6 +4051,10 @@
pw.increaseIndent();
mNetworkActivityTracker.dump(pw);
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("Multicast routing supported: " +
+ (mMulticastRoutingCoordinatorService != null));
}
private void dumpNetworks(IndentingPrintWriter pw) {
@@ -5175,9 +5196,12 @@
private void removeLocalNetworkUpstream(@NonNull final NetworkAgentInfo localAgent,
@NonNull final NetworkAgentInfo upstream) {
try {
+ final String localNetworkInterfaceName = localAgent.linkProperties.getInterfaceName();
+ final String upstreamNetworkInterfaceName = upstream.linkProperties.getInterfaceName();
mRoutingCoordinatorService.removeInterfaceForward(
- localAgent.linkProperties.getInterfaceName(),
- upstream.linkProperties.getInterfaceName());
+ localNetworkInterfaceName,
+ upstreamNetworkInterfaceName);
+ disableMulticastRouting(localNetworkInterfaceName, upstreamNetworkInterfaceName);
} catch (RemoteException e) {
loge("Couldn't remove interface forward for "
+ localAgent.linkProperties.getInterfaceName() + " to "
@@ -5324,6 +5348,8 @@
updateSignalStrengthThresholds(network, "REGISTER", req);
}
}
+ } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req);
}
}
@@ -5541,6 +5567,8 @@
}
if (req.isListen()) {
removeListenRequestFromNetworks(req);
+ } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(req);
}
}
nri.unlinkDeathRecipient();
@@ -6292,10 +6320,8 @@
if (!networkFound) return;
if (underpinnedNetworkFound) {
- final NetworkCapabilities underpinnedNc =
- getNetworkCapabilitiesInternal(underpinnedNetwork);
mKeepaliveTracker.handleMonitorAutomaticKeepalive(ki,
- underpinnedNetwork.netId, underpinnedNc.getUids());
+ underpinnedNetwork.netId);
} else {
// If no underpinned network, then make sure the keepalive is running.
mKeepaliveTracker.handleMaybeResumeKeepalive(ki);
@@ -9095,6 +9121,71 @@
updateCapabilities(nai.getScore(), nai, nai.networkCapabilities);
}
+ private void maybeApplyMulticastRoutingConfig(@NonNull final NetworkAgentInfo nai,
+ final LocalNetworkConfig oldConfig,
+ final LocalNetworkConfig newConfig) {
+ final MulticastRoutingConfig oldUpstreamConfig =
+ oldConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+ oldConfig.getUpstreamMulticastRoutingConfig();
+ final MulticastRoutingConfig oldDownstreamConfig =
+ oldConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+ oldConfig.getDownstreamMulticastRoutingConfig();
+ final MulticastRoutingConfig newUpstreamConfig =
+ newConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+ newConfig.getUpstreamMulticastRoutingConfig();
+ final MulticastRoutingConfig newDownstreamConfig =
+ newConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+ newConfig.getDownstreamMulticastRoutingConfig();
+
+ if (oldUpstreamConfig.equals(newUpstreamConfig) &&
+ oldDownstreamConfig.equals(newDownstreamConfig)) {
+ return;
+ }
+
+ final String downstreamNetworkName = nai.linkProperties.getInterfaceName();
+ final LocalNetworkInfo lni = localNetworkInfoForNai(nai);
+ final Network upstreamNetwork = lni.getUpstreamNetwork();
+
+ if (upstreamNetwork != null) {
+ final String upstreamNetworkName =
+ getLinkProperties(upstreamNetwork).getInterfaceName();
+ applyMulticastRoutingConfig(downstreamNetworkName, upstreamNetworkName, newConfig);
+ }
+ }
+
+ private void applyMulticastRoutingConfig(@NonNull String localNetworkInterfaceName,
+ @NonNull String upstreamNetworkInterfaceName,
+ @NonNull final LocalNetworkConfig config) {
+ if (mMulticastRoutingCoordinatorService == null) {
+ if (config.getDownstreamMulticastRoutingConfig().getForwardingMode() != FORWARD_NONE ||
+ config.getUpstreamMulticastRoutingConfig().getForwardingMode() != FORWARD_NONE) {
+ loge("Multicast routing is not supported, failed to configure " + config
+ + " for " + localNetworkInterfaceName + " to "
+ + upstreamNetworkInterfaceName);
+ }
+ return;
+ }
+
+ mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig(localNetworkInterfaceName,
+ upstreamNetworkInterfaceName, config.getUpstreamMulticastRoutingConfig());
+ mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig
+ (upstreamNetworkInterfaceName, localNetworkInterfaceName,
+ config.getDownstreamMulticastRoutingConfig());
+ }
+
+ private void disableMulticastRouting(@NonNull String localNetworkInterfaceName,
+ @NonNull String upstreamNetworkInterfaceName) {
+ if (mMulticastRoutingCoordinatorService == null) {
+ return;
+ }
+
+ mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig(localNetworkInterfaceName,
+ upstreamNetworkInterfaceName, MulticastRoutingConfig.CONFIG_FORWARD_NONE);
+ mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig
+ (upstreamNetworkInterfaceName, localNetworkInterfaceName,
+ MulticastRoutingConfig.CONFIG_FORWARD_NONE);
+ }
+
// oldConfig is null iff this is the original registration of the local network config
private void handleUpdateLocalNetworkConfig(@NonNull final NetworkAgentInfo nai,
@Nullable final LocalNetworkConfig oldConfig,
@@ -9108,7 +9199,6 @@
Log.v(TAG, "Update local network config " + nai.network.netId + " : " + newConfig);
}
final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
- // TODO : apply the diff for multicast routing.
configBuilder.setUpstreamMulticastRoutingConfig(
newConfig.getUpstreamMulticastRoutingConfig());
configBuilder.setDownstreamMulticastRoutingConfig(
@@ -9167,6 +9257,7 @@
configBuilder.setUpstreamSelector(oldRequest);
nai.localNetworkConfig = configBuilder.build();
}
+ maybeApplyMulticastRoutingConfig(nai, oldConfig, newConfig);
}
/**
@@ -10166,6 +10257,8 @@
if (null != change.mOldNetwork) {
mRoutingCoordinatorService.removeInterfaceForward(fromIface,
change.mOldNetwork.linkProperties.getInterfaceName());
+ disableMulticastRouting(fromIface,
+ change.mOldNetwork.linkProperties.getInterfaceName());
}
// If the new upstream is already destroyed, there is no point in setting up
// a forward (in fact, it might forward to the interface for some new network !)
@@ -10174,6 +10267,9 @@
if (null != change.mNewNetwork && !change.mNewNetwork.isDestroyed()) {
mRoutingCoordinatorService.addInterfaceForward(fromIface,
change.mNewNetwork.linkProperties.getInterfaceName());
+ applyMulticastRoutingConfig(fromIface,
+ change.mNewNetwork.linkProperties.getInterfaceName(),
+ nai.localNetworkConfig);
}
} catch (final RemoteException e) {
loge("Can't update forwarding rules", e);
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 8036ae9..31108fc 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -25,9 +25,6 @@
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
-import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
import android.annotation.IntDef;
@@ -53,7 +50,6 @@
import android.util.LocalLog;
import android.util.Log;
import android.util.Pair;
-import android.util.Range;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -78,7 +74,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import java.util.Set;
/**
* Manages automatic on/off socket keepalive requests.
@@ -90,6 +85,7 @@
*/
public class AutomaticOnOffKeepaliveTracker {
private static final String TAG = "AutomaticOnOffKeepaliveTracker";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
private static final long LOW_TCP_POLLING_INTERVAL_MS = 1_000L;
private static final int ADJUST_TCP_POLLING_DELAY_MS = 2000;
@@ -375,27 +371,26 @@
* Determine if any state transition is needed for the specific automatic keepalive.
*/
public void handleMonitorAutomaticKeepalive(@NonNull final AutomaticOnOffKeepalive ki,
- final int vpnNetId, @NonNull Set<Range<Integer>> vpnUidRanges) {
+ final int vpnNetId) {
// Might happen if the automatic keepalive was removed by the app just as the alarm fires.
if (!mAutomaticOnOffKeepalives.contains(ki)) return;
if (STATE_ALWAYS_ON == ki.mAutomaticOnOffState) {
throw new IllegalStateException("Should not monitor non-auto keepalive");
}
- handleMonitorTcpConnections(ki, vpnNetId, vpnUidRanges);
+ handleMonitorTcpConnections(ki, vpnNetId);
}
/**
* Determine if disable or re-enable keepalive is needed or not based on TCP sockets status.
*/
- private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId,
- @NonNull Set<Range<Integer>> vpnUidRanges) {
+ private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId) {
// Might happen if the automatic keepalive was removed by the app just as the alarm fires.
if (!mAutomaticOnOffKeepalives.contains(ki)) return;
if (STATE_ALWAYS_ON == ki.mAutomaticOnOffState) {
throw new IllegalStateException("Should not monitor non-auto keepalive");
}
- if (!isAnyTcpSocketConnected(vpnNetId, vpnUidRanges)) {
+ if (!isAnyTcpSocketConnected(vpnNetId)) {
// No TCP socket exists. Stop keepalive if ENABLED, and remain SUSPENDED if currently
// SUSPENDED.
if (ki.mAutomaticOnOffState == STATE_ENABLED) {
@@ -747,7 +742,7 @@
}
@VisibleForTesting
- boolean isAnyTcpSocketConnected(int netId, @NonNull Set<Range<Integer>> vpnUidRanges) {
+ boolean isAnyTcpSocketConnected(int netId) {
FileDescriptor fd = null;
try {
@@ -760,8 +755,7 @@
// Send request for each IP family
for (final int family : ADDRESS_FAMILIES) {
- if (isAnyTcpSocketConnectedForFamily(
- fd, family, networkMark, networkMask, vpnUidRanges)) {
+ if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
return true;
}
}
@@ -775,7 +769,7 @@
}
private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
- int networkMask, @NonNull Set<Range<Integer>> vpnUidRanges)
+ int networkMask)
throws ErrnoException, InterruptedIOException {
ensureRunningOnHandlerThread();
// Build SocketDiag messages and cache it.
@@ -794,22 +788,18 @@
try {
while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
- final int startPos = bytes.position();
+ // NetlinkMessage.parse() will move the byte buffer position.
+ // TODO: Parse dst address information to filter socket.
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(
+ bytes, OsConstants.NETLINK_INET_DIAG);
+ if (!(nlMsg instanceof InetDiagMessage)) {
+ if (DBG) Log.e(TAG, "Not a SOCK_DIAG_BY_FAMILY msg");
+ return false;
+ }
- final int nlmsgLen = bytes.getInt();
- final int nlmsgType = bytes.getShort();
- if (isEndOfMessageOrError(nlmsgType)) return false;
- // TODO: Parse InetDiagMessage to get uid and dst address information to filter
- // socket via NetlinkMessage.parse.
-
- // Skip the header to move to data part.
- bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
-
- if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- bytes.position(startPos);
- final InetDiagMessage diagMsg = (InetDiagMessage) NetlinkMessage.parse(
- bytes, OsConstants.NETLINK_INET_DIAG);
+ final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
+ if (isTargetTcpSocket(diagMsg, networkMark, networkMask)) {
+ if (DBG) {
Log.d(TAG, String.format("Found open TCP connection by uid %d to %s"
+ " cookie %d",
diagMsg.inetDiagMsg.idiag_uid,
@@ -834,26 +824,20 @@
return false;
}
- private boolean isEndOfMessageOrError(int nlmsgType) {
- return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
- }
-
- private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
- int networkMask) {
- final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
+ private boolean isTargetTcpSocket(@NonNull InetDiagMessage diagMsg,
+ int networkMark, int networkMask) {
+ final int mark = readSocketDataAndReturnMark(diagMsg);
return (mark & networkMask) == networkMark;
}
- private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
- final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
+ private int readSocketDataAndReturnMark(@NonNull InetDiagMessage diagMsg) {
int mark = NetlinkUtils.INIT_MARK_VALUE;
// Get socket mark
- // TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
- // data.
- while (bytes.position() < nextMsgOffset) {
- final StructNlAttr nlattr = StructNlAttr.parse(bytes);
- if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
- mark = nlattr.getValueAsInteger();
+ for (StructNlAttr attr : diagMsg.nlAttrs) {
+ if (attr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
+ // The netlink attributes should contain only one INET_DIAG_MARK for each socket.
+ mark = attr.getValueAsInteger();
+ break;
}
}
return mark;
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 17de146..daaf91d 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -256,7 +256,7 @@
public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
try {
return new BpfMap<>(CLAT_INGRESS6_MAP_PATH,
- BpfMap.BPF_F_RDWR, ClatIngress6Key.class, ClatIngress6Value.class);
+ ClatIngress6Key.class, ClatIngress6Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create ingress6 map: " + e);
return null;
@@ -268,7 +268,7 @@
public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
try {
return new BpfMap<>(CLAT_EGRESS4_MAP_PATH,
- BpfMap.BPF_F_RDWR, ClatEgress4Key.class, ClatEgress4Value.class);
+ ClatEgress4Key.class, ClatEgress4Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create egress4 map: " + e);
return null;
@@ -280,7 +280,7 @@
public IBpfMap<CookieTagMapKey, CookieTagMapValue> getBpfCookieTagMap() {
try {
return new BpfMap<>(COOKIE_TAG_MAP_PATH,
- BpfMap.BPF_F_RDWR, CookieTagMapKey.class, CookieTagMapValue.class);
+ CookieTagMapKey.class, CookieTagMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open cookie tag map: " + e);
return null;
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 8d566b6..ea3878f 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -86,9 +86,9 @@
mAttachedIfaces = new HashSet<String>();
mIfaceIndexToPolicyIdBpfMapIndex = new HashMap<Integer, SparseIntArray>();
mBpfDscpIpv4Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,
- BpfMap.BPF_F_RDWR, Struct.S32.class, DscpPolicyValue.class);
+ Struct.S32.class, DscpPolicyValue.class);
mBpfDscpIpv6Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
- BpfMap.BPF_F_RDWR, Struct.S32.class, DscpPolicyValue.class);
+ Struct.S32.class, DscpPolicyValue.class);
}
private boolean isUnusedIndex(int index) {
diff --git a/service/src/com/android/server/connectivity/HandlerUtils.java b/service/src/com/android/server/connectivity/HandlerUtils.java
deleted file mode 100644
index 997ecbf..0000000
--- a/service/src/com/android/server/connectivity/HandlerUtils.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.connectivity;
-
-import android.annotation.NonNull;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
-
-/**
- * Helper class for Handler related utilities.
- *
- * @hide
- */
-public class HandlerUtils {
- // Note: @hide methods copied from android.os.Handler
- /**
- * Runs the specified task synchronously.
- * <p>
- * If the current thread is the same as the handler thread, then the runnable
- * runs immediately without being enqueued. Otherwise, posts the runnable
- * to the handler and waits for it to complete before returning.
- * </p><p>
- * This method is dangerous! Improper use can result in deadlocks.
- * Never call this method while any locks are held or use it in a
- * possibly re-entrant manner.
- * </p><p>
- * This method is occasionally useful in situations where a background thread
- * must synchronously await completion of a task that must run on the
- * handler's thread. However, this problem is often a symptom of bad design.
- * Consider improving the design (if possible) before resorting to this method.
- * </p><p>
- * One example of where you might want to use this method is when you just
- * set up a Handler thread and need to perform some initialization steps on
- * it before continuing execution.
- * </p><p>
- * If timeout occurs then this method returns <code>false</code> but the runnable
- * will remain posted on the handler and may already be in progress or
- * complete at a later time.
- * </p><p>
- * When using this method, be sure to use {@link Looper#quitSafely} when
- * quitting the looper. Otherwise {@link #runWithScissors} may hang indefinitely.
- * (TODO: We should fix this by making MessageQueue aware of blocking runnables.)
- * </p>
- *
- * @param h The target handler.
- * @param r The Runnable that will be executed synchronously.
- * @param timeout The timeout in milliseconds, or 0 to wait indefinitely.
- *
- * @return Returns true if the Runnable was successfully executed.
- * Returns false on failure, usually because the
- * looper processing the message queue is exiting.
- *
- * @hide This method is prone to abuse and should probably not be in the API.
- * If we ever do make it part of the API, we might want to rename it to something
- * less funny like runUnsafe().
- */
- public static boolean runWithScissors(@NonNull Handler h, @NonNull Runnable r, long timeout) {
- if (r == null) {
- throw new IllegalArgumentException("runnable must not be null");
- }
- if (timeout < 0) {
- throw new IllegalArgumentException("timeout must be non-negative");
- }
-
- if (Looper.myLooper() == h.getLooper()) {
- r.run();
- return true;
- }
-
- BlockingRunnable br = new BlockingRunnable(r);
- return br.postAndWait(h, timeout);
- }
-
- private static final class BlockingRunnable implements Runnable {
- private final Runnable mTask;
- private boolean mDone;
-
- BlockingRunnable(Runnable task) {
- mTask = task;
- }
-
- @Override
- public void run() {
- try {
- mTask.run();
- } finally {
- synchronized (this) {
- mDone = true;
- notifyAll();
- }
- }
- }
-
- public boolean postAndWait(Handler handler, long timeout) {
- if (!handler.post(this)) {
- return false;
- }
-
- synchronized (this) {
- if (timeout > 0) {
- final long expirationTime = SystemClock.uptimeMillis() + timeout;
- while (!mDone) {
- long delay = expirationTime - SystemClock.uptimeMillis();
- if (delay <= 0) {
- return false; // timeout
- }
- try {
- wait(delay);
- } catch (InterruptedException ex) {
- }
- }
- } else {
- while (!mDone) {
- try {
- wait();
- } catch (InterruptedException ex) {
- }
- }
- }
- }
- return true;
- }
- }
-}
diff --git a/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
new file mode 100644
index 0000000..4d5001b
--- /dev/null
+++ b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
@@ -0,0 +1,820 @@
+/*
+ * 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.connectivity;
+
+import static android.net.MulticastRoutingConfig.FORWARD_NONE;
+import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
+import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.EADDRINUSE;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_IPV6;
+import static android.system.OsConstants.SOCK_CLOEXEC;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOCK_RAW;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.MulticastRoutingConfig;
+import android.net.NetworkUtils;
+import android.os.Handler;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+import com.android.net.module.util.PacketReader;
+import com.android.net.module.util.SocketUtils;
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.RtNetlinkRouteMessage;
+import com.android.net.module.util.structs.StructMf6cctl;
+import com.android.net.module.util.structs.StructMif6ctl;
+import com.android.net.module.util.structs.StructMrt6Msg;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Class to coordinate multicast routing between network interfaces.
+ *
+ * <p>Supports IPv6 multicast routing.
+ *
+ * <p>Note that usage of this class is not thread-safe. All public methods must be called from the
+ * same thread that the handler from {@code dependencies.getHandler} is associated.
+ */
+public class MulticastRoutingCoordinatorService {
+ private static final String TAG = MulticastRoutingCoordinatorService.class.getSimpleName();
+ private static final int ICMP6_FILTER = 1;
+ private static final int MRT6_INIT = 200;
+ private static final int MRT6_ADD_MIF = 202;
+ private static final int MRT6_DEL_MIF = 203;
+ private static final int MRT6_ADD_MFC = 204;
+ private static final int MRT6_DEL_MFC = 205;
+ private static final int ONE = 1;
+
+ private final Dependencies mDependencies;
+
+ private final Handler mHandler;
+ private final MulticastNocacheUpcallListener mMulticastNoCacheUpcallListener;
+ @NonNull private final FileDescriptor mMulticastRoutingFd; // For multicast routing config
+ @NonNull private final MulticastSocket mMulticastSocket; // For join group and leave group
+
+ @VisibleForTesting public static final int MFC_INACTIVE_CHECK_INTERVAL_MS = 60_000;
+ @VisibleForTesting public static final int MFC_INACTIVE_TIMEOUT_MS = 300_000;
+ @VisibleForTesting public static final int MFC_MAX_NUMBER_OF_ENTRIES = 1_000;
+
+ // The kernel supports max 32 virtual interfaces per multicast routing table.
+ private static final int MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES = 32;
+
+ /** Tracks if checking for inactive MFC has been scheduled */
+ private boolean mMfcPollingScheduled = false;
+
+ /** Mapping from multicast virtual interface index to interface name */
+ private SparseArray<String> mVirtualInterfaces =
+ new SparseArray<>(MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES);
+ /** Mapping from physical interface index to interface name */
+ private SparseArray<String> mInterfaces =
+ new SparseArray<>(MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES);
+
+ /** Mapping of iif to PerInterfaceMulticastRoutingConfig */
+ private Map<String, PerInterfaceMulticastRoutingConfig> mMulticastRoutingConfigs =
+ new HashMap<String, PerInterfaceMulticastRoutingConfig>();
+
+ private static final class PerInterfaceMulticastRoutingConfig {
+ // mapping of oif name to MulticastRoutingConfig
+ public Map<String, MulticastRoutingConfig> oifConfigs =
+ new HashMap<String, MulticastRoutingConfig>();
+ }
+
+ /** Tracks the MFCs added to kernel. Using LinkedHashMap to keep the added order, so
+ // when the number of MFCs reaches the max limit then the earliest added one is removed. */
+ private LinkedHashMap<MfcKey, MfcValue> mMfcs = new LinkedHashMap<>();
+
+ public MulticastRoutingCoordinatorService(Handler h) {
+ this(h, new Dependencies());
+ }
+
+ @VisibleForTesting
+ /* @throws UnsupportedOperationException if multicast routing is not supported */
+ public MulticastRoutingCoordinatorService(Handler h, Dependencies dependencies) {
+ mDependencies = dependencies;
+ mMulticastRoutingFd = mDependencies.createMulticastRoutingSocket();
+ mMulticastSocket = mDependencies.createMulticastSocket();
+ mHandler = h;
+ mMulticastNoCacheUpcallListener =
+ new MulticastNocacheUpcallListener(mHandler, mMulticastRoutingFd);
+ mHandler.post(() -> mMulticastNoCacheUpcallListener.start());
+ }
+
+ private void checkOnHandlerThread() {
+ if (Looper.myLooper() != mHandler.getLooper()) {
+ throw new IllegalStateException(
+ "Not running on ConnectivityService thread (" + mHandler.getLooper() + ") : "
+ + Looper.myLooper());
+ }
+ }
+
+ private Integer getInterfaceIndex(String ifName) {
+ int mapIndex = mInterfaces.indexOfValue(ifName);
+ if (mapIndex < 0) return null;
+ return mInterfaces.keyAt(mapIndex);
+ }
+
+ /**
+ * Apply multicast routing configuration
+ *
+ * @param iifName name of the incoming interface
+ * @param oifName name of the outgoing interface
+ * @param newConfig the multicast routing configuration to be applied from iif to oif
+ * @throws MulticastRoutingException when failed to apply the config
+ */
+ public void applyMulticastRoutingConfig(
+ final String iifName, final String oifName, final MulticastRoutingConfig newConfig) {
+ checkOnHandlerThread();
+
+ if (newConfig.getForwardingMode() != FORWARD_NONE) {
+ // Make sure iif and oif are added as multicast forwarding interfaces
+ try {
+ maybeAddAndTrackInterface(iifName);
+ maybeAddAndTrackInterface(oifName);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to apply multicast routing config, ", e);
+ return;
+ }
+ }
+
+ final MulticastRoutingConfig oldConfig = getMulticastRoutingConfig(iifName, oifName);
+
+ if (oldConfig.equals(newConfig)) return;
+
+ int oldMode = oldConfig.getForwardingMode();
+ int newMode = newConfig.getForwardingMode();
+ Integer iifIndex = getInterfaceIndex(iifName);
+ if (iifIndex == null) {
+ // This cannot happen unless the new config has FORWARD_NONE but is not the same
+ // as the old config. This is not possible in current code.
+ Log.wtf(TAG, "Adding multicast configuration on null interface?");
+ return;
+ }
+
+ // When new addresses are added to FORWARD_SELECTED mode, join these multicast groups
+ // on their upstream interface, so upstream multicast routers know about the subscription.
+ // When addresses are removed from FORWARD_SELECTED mode, leave the multicast groups.
+ final Set<Inet6Address> oldListeningAddresses =
+ (oldMode == FORWARD_SELECTED)
+ ? oldConfig.getListeningAddresses()
+ : new ArraySet<>();
+ final Set<Inet6Address> newListeningAddresses =
+ (newMode == FORWARD_SELECTED)
+ ? newConfig.getListeningAddresses()
+ : new ArraySet<>();
+ final CompareResult<Inet6Address> addressDiff =
+ new CompareResult<>(oldListeningAddresses, newListeningAddresses);
+ joinGroups(iifIndex, addressDiff.added);
+ leaveGroups(iifIndex, addressDiff.removed);
+
+ setMulticastRoutingConfig(iifName, oifName, newConfig);
+ Log.d(
+ TAG,
+ "Applied multicast routing config for iif "
+ + iifName
+ + " to oif "
+ + oifName
+ + " with Config "
+ + newConfig);
+
+ // Update existing MFCs to make sure they align with the updated configuration
+ updateMfcs();
+
+ if (newConfig.getForwardingMode() == FORWARD_NONE) {
+ if (!hasActiveMulticastConfig(iifName)) {
+ removeInterfaceFromMulticastRouting(iifName);
+ }
+ if (!hasActiveMulticastConfig(oifName)) {
+ removeInterfaceFromMulticastRouting(oifName);
+ }
+ }
+ }
+
+ /**
+ * Removes an network interface from multicast routing.
+ *
+ * <p>Remove the network interface from multicast configs and remove it from the list of
+ * multicast routing interfaces in the kernel
+ *
+ * @param ifName name of the interface that should be removed
+ */
+ @VisibleForTesting
+ public void removeInterfaceFromMulticastRouting(final String ifName) {
+ checkOnHandlerThread();
+ final Integer virtualIndex = getVirtualInterfaceIndex(ifName);
+ if (virtualIndex == null) return;
+
+ updateMfcs();
+ mInterfaces.removeAt(mInterfaces.indexOfValue(ifName));
+ mVirtualInterfaces.remove(virtualIndex);
+ try {
+ mDependencies.setsockoptMrt6DelMif(mMulticastRoutingFd, virtualIndex);
+ Log.d(TAG, "Removed mifi " + virtualIndex + " from MIF");
+ } catch (ErrnoException e) {
+ Log.e(TAG, "failed to remove multicast virtual interface" + virtualIndex, e);
+ }
+ }
+
+ private int getNextAvailableVirtualIndex() {
+ if (mVirtualInterfaces.size() >= MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES) {
+ throw new IllegalStateException("Can't allocate new multicast virtual interface");
+ }
+ for (int i = 0; i < mVirtualInterfaces.size(); i++) {
+ if (!mVirtualInterfaces.contains(i)) {
+ return i;
+ }
+ }
+ return mVirtualInterfaces.size();
+ }
+
+ @VisibleForTesting
+ public Integer getVirtualInterfaceIndex(String ifName) {
+ int mapIndex = mVirtualInterfaces.indexOfValue(ifName);
+ if (mapIndex < 0) return null;
+ return mVirtualInterfaces.keyAt(mapIndex);
+ }
+
+ private Integer getVirtualInterfaceIndex(int physicalIndex) {
+ String ifName = mInterfaces.get(physicalIndex);
+ if (ifName == null) {
+ // This is only used to match MFCs from kernel to MFCs we know about.
+ // Unknown MFCs should be ignored.
+ return null;
+ }
+ return getVirtualInterfaceIndex(ifName);
+ }
+
+ private String getInterfaceName(int virtualIndex) {
+ return mVirtualInterfaces.get(virtualIndex);
+ }
+
+ private void maybeAddAndTrackInterface(String ifName) {
+ checkOnHandlerThread();
+ if (mVirtualInterfaces.indexOfValue(ifName) >= 0) return;
+
+ int nextVirtualIndex = getNextAvailableVirtualIndex();
+ int ifIndex = mDependencies.getInterfaceIndex(ifName);
+ final StructMif6ctl mif6ctl =
+ new StructMif6ctl(
+ nextVirtualIndex,
+ (short) 0 /* mif6c_flags */,
+ (short) 1 /* vifc_threshold */,
+ ifIndex,
+ 0 /* vifc_rate_limit */);
+ try {
+ mDependencies.setsockoptMrt6AddMif(mMulticastRoutingFd, mif6ctl);
+ Log.d(TAG, "Added mifi " + nextVirtualIndex + " to MIF");
+ } catch (ErrnoException e) {
+ Log.e(TAG, "failed to add multicast virtual interface", e);
+ return;
+ }
+ mVirtualInterfaces.put(nextVirtualIndex, ifName);
+ mInterfaces.put(ifIndex, ifName);
+ }
+
+ @VisibleForTesting
+ public MulticastRoutingConfig getMulticastRoutingConfig(String iifName, String oifName) {
+ PerInterfaceMulticastRoutingConfig configs = mMulticastRoutingConfigs.get(iifName);
+ final MulticastRoutingConfig defaultConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+ if (configs == null) {
+ return defaultConfig;
+ } else {
+ return configs.oifConfigs.getOrDefault(oifName, defaultConfig);
+ }
+ }
+
+ private void setMulticastRoutingConfig(
+ final String iifName, final String oifName, final MulticastRoutingConfig config) {
+ checkOnHandlerThread();
+ PerInterfaceMulticastRoutingConfig iifConfig = mMulticastRoutingConfigs.get(iifName);
+
+ if (config.getForwardingMode() == FORWARD_NONE) {
+ if (iifConfig != null) {
+ iifConfig.oifConfigs.remove(oifName);
+ }
+ if (iifConfig.oifConfigs.isEmpty()) {
+ mMulticastRoutingConfigs.remove(iifName);
+ }
+ return;
+ }
+
+ if (iifConfig == null) {
+ iifConfig = new PerInterfaceMulticastRoutingConfig();
+ mMulticastRoutingConfigs.put(iifName, iifConfig);
+ }
+ iifConfig.oifConfigs.put(oifName, config);
+ }
+
+ /** Returns whether an interface has multicast routing config */
+ private boolean hasActiveMulticastConfig(final String ifName) {
+ // FORWARD_NONE configs are not saved in the config tables, so
+ // any existing config is an active multicast routing config
+ if (mMulticastRoutingConfigs.containsKey(ifName)) return true;
+ for (var pic : mMulticastRoutingConfigs.values()) {
+ if (pic.oifConfigs.containsKey(ifName)) return true;
+ }
+ return false;
+ }
+
+ /**
+ * A multicast forwarding cache (MFC) entry holds a multicast forwarding route where packet from
+ * incoming interface(iif) with source address(S) to group address (G) are forwarded to outgoing
+ * interfaces(oifs).
+ *
+ * <p>iif, S and G identifies an MFC entry. For example an MFC1 is added: [iif1, S1, G1, oifs1]
+ * Adding another MFC2 of [iif1, S1, G1, oifs2] to the kernel overwrites MFC1.
+ */
+ private static final class MfcKey {
+ public final int mIifVirtualIdx;
+ public final Inet6Address mSrcAddr;
+ public final Inet6Address mDstAddr;
+
+ public MfcKey(int iif, Inet6Address src, Inet6Address dst) {
+ mIifVirtualIdx = iif;
+ mSrcAddr = src;
+ mDstAddr = dst;
+ }
+
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (!(other instanceof MfcKey)) {
+ return false;
+ } else {
+ MfcKey otherKey = (MfcKey) other;
+ return mIifVirtualIdx == otherKey.mIifVirtualIdx
+ && mSrcAddr.equals(otherKey.mSrcAddr)
+ && mDstAddr.equals(otherKey.mDstAddr);
+ }
+ }
+
+ public int hashCode() {
+ return Objects.hash(mIifVirtualIdx, mSrcAddr, mDstAddr);
+ }
+
+ public String toString() {
+ return "{iifVirtualIndex: "
+ + Integer.toString(mIifVirtualIdx)
+ + ", sourceAddress: "
+ + mSrcAddr.toString()
+ + ", destinationAddress: "
+ + mDstAddr.toString()
+ + "}";
+ }
+ }
+
+ private static final class MfcValue {
+ private Set<Integer> mOifVirtualIndices;
+ // timestamp of when the mfc was last used in the kernel
+ // (e.g. created, or used to forward a packet)
+ private Instant mLastUsedAt;
+
+ public MfcValue(Set<Integer> oifs, Instant timestamp) {
+ mOifVirtualIndices = oifs;
+ mLastUsedAt = timestamp;
+ }
+
+ public boolean hasSameOifsAs(MfcValue other) {
+ return this.mOifVirtualIndices.equals(other.mOifVirtualIndices);
+ }
+
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (!(other instanceof MfcValue)) {
+ return false;
+ } else {
+ MfcValue otherValue = (MfcValue) other;
+ return mOifVirtualIndices.equals(otherValue.mOifVirtualIndices)
+ && mLastUsedAt.equals(otherValue.mLastUsedAt);
+ }
+ }
+
+ public int hashCode() {
+ return Objects.hash(mOifVirtualIndices, mLastUsedAt);
+ }
+
+ public Set<Integer> getOifIndices() {
+ return mOifVirtualIndices;
+ }
+
+ public void setLastUsedAt(Instant timestamp) {
+ mLastUsedAt = timestamp;
+ }
+
+ public Instant getLastUsedAt() {
+ return mLastUsedAt;
+ }
+
+ public String toString() {
+ return "{oifVirtualIdxes: "
+ + mOifVirtualIndices.toString()
+ + ", lastUsedAt: "
+ + mLastUsedAt.toString()
+ + "}";
+ }
+ }
+
+ /**
+ * Returns the MFC value for the given MFC key according to current multicast routing config. If
+ * the MFC should be removed return null.
+ */
+ private MfcValue computeMfcValue(int iif, Inet6Address dst) {
+ final int dstScope = getGroupAddressScope(dst);
+ Set<Integer> forwardingOifs = new ArraySet<>();
+
+ PerInterfaceMulticastRoutingConfig iifConfig =
+ mMulticastRoutingConfigs.get(getInterfaceName(iif));
+
+ if (iifConfig == null) {
+ // An iif may have been removed from multicast routing, in this
+ // case remove the MFC directly
+ return null;
+ }
+
+ for (var config : iifConfig.oifConfigs.entrySet()) {
+ if ((config.getValue().getForwardingMode() == FORWARD_WITH_MIN_SCOPE
+ && config.getValue().getMinimumScope() <= dstScope)
+ || (config.getValue().getForwardingMode() == FORWARD_SELECTED
+ && config.getValue().getListeningAddresses().contains(dst))) {
+ forwardingOifs.add(getVirtualInterfaceIndex(config.getKey()));
+ }
+ }
+
+ return new MfcValue(forwardingOifs, Instant.now(mDependencies.getClock()));
+ }
+
+ /**
+ * Given the iif, source address and group destination address, add an MFC entry or update the
+ * existing MFC according to the multicast routing config. If such an MFC should not exist,
+ * return null for caller of the function to remove it.
+ *
+ * <p>Note that if a packet has no matching MFC entry in the kernel, kernel creates an
+ * unresolved route and notifies multicast socket with a NOCACHE upcall message. The unresolved
+ * route is kept for no less than 10s. If packets with the same source and destination arrives
+ * before the 10s timeout, they will not be notified. Thus we need to add a 'blocking' MFC which
+ * is an MFC with an empty oif list. When the multicast configs changes, the 'blocking' MFC
+ * will be updated to a 'forwarding' MFC so that corresponding multicast traffic can be
+ * forwarded instantly.
+ *
+ * @return {@code true} if the MFC is updated and no operation is needed from caller.
+ * {@code false} if the MFC should not be added, caller of the function should remove
+ * the MFC if needed.
+ */
+ private boolean addOrUpdateMfc(int vif, Inet6Address src, Inet6Address dst) {
+ checkOnHandlerThread();
+ final MfcKey key = new MfcKey(vif, src, dst);
+ final MfcValue value = mMfcs.get(key);
+ final MfcValue updatedValue = computeMfcValue(vif, dst);
+
+ if (updatedValue == null) {
+ return false;
+ }
+
+ if (value != null && value.hasSameOifsAs(updatedValue)) {
+ // no updates to make
+ return true;
+ }
+
+ final StructMf6cctl mf6cctl =
+ new StructMf6cctl(src, dst, vif, updatedValue.getOifIndices());
+ try {
+ mDependencies.setsockoptMrt6AddMfc(mMulticastRoutingFd, mf6cctl);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "failed to add MFC: " + e);
+ return false;
+ }
+ mMfcs.put(key, updatedValue);
+ String operation = (value == null ? "Added" : "Updated");
+ Log.d(TAG, operation + " MFC key: " + key + " value: " + updatedValue);
+ return true;
+ }
+
+ private void checkMfcsExpiration() {
+ checkOnHandlerThread();
+ // Check if there are inactive MFCs that can be removed
+ refreshMfcInactiveDuration();
+ maybeExpireMfcs();
+ if (mMfcs.size() > 0) {
+ mHandler.postDelayed(() -> checkMfcsExpiration(), MFC_INACTIVE_CHECK_INTERVAL_MS);
+ mMfcPollingScheduled = true;
+ } else {
+ mMfcPollingScheduled = false;
+ }
+ }
+
+ private void checkMfcEntriesLimit() {
+ checkOnHandlerThread();
+ // If the max number of MFC entries is reached, remove the first MFC entry. This can be
+ // any entry, as if this entry is needed again there will be a NOCACHE upcall to add it
+ // back.
+ if (mMfcs.size() == MFC_MAX_NUMBER_OF_ENTRIES) {
+ Log.w(TAG, "Reached max number of MFC entries " + MFC_MAX_NUMBER_OF_ENTRIES);
+ var iter = mMfcs.entrySet().iterator();
+ MfcKey firstMfcKey = iter.next().getKey();
+ removeMfcFromKernel(firstMfcKey);
+ iter.remove();
+ }
+ }
+
+ /**
+ * Reads multicast routes information from the kernel, and update the last used timestamp for
+ * each multicast route save in this class.
+ */
+ private void refreshMfcInactiveDuration() {
+ checkOnHandlerThread();
+ final List<RtNetlinkRouteMessage> multicastRoutes = NetlinkUtils.getIpv6MulticastRoutes();
+
+ for (var route : multicastRoutes) {
+ if (!route.isResolved()) {
+ continue; // Don't handle unresolved mfc, the kernel will recycle in 10s
+ }
+ Integer iif = getVirtualInterfaceIndex(route.getIifIndex());
+ if (iif == null) {
+ Log.e(TAG, "Can't find kernel returned IIF " + route.getIifIndex());
+ return;
+ }
+ final MfcKey key =
+ new MfcKey(
+ iif,
+ (Inet6Address) route.getSource().getAddress(),
+ (Inet6Address) route.getDestination().getAddress());
+ MfcValue value = mMfcs.get(key);
+ if (value == null) {
+ Log.e(TAG, "Can't find kernel returned MFC " + key);
+ continue;
+ }
+ value.setLastUsedAt(
+ Instant.now(mDependencies.getClock())
+ .minusMillis(route.getSinceLastUseMillis()));
+ }
+ }
+
+ /** Remove MFC entry from mMfcs map and the kernel if exists. */
+ private void removeMfcFromKernel(MfcKey key) {
+ checkOnHandlerThread();
+
+ final MfcValue value = mMfcs.get(key);
+ final Set<Integer> oifs = new ArraySet<>();
+ final StructMf6cctl mf6cctl =
+ new StructMf6cctl(key.mSrcAddr, key.mDstAddr, key.mIifVirtualIdx, oifs);
+ try {
+ mDependencies.setsockoptMrt6DelMfc(mMulticastRoutingFd, mf6cctl);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "failed to remove MFC: " + e);
+ return;
+ }
+ Log.d(TAG, "Removed MFC key: " + key + " value: " + value);
+ }
+
+ /**
+ * This is called every MFC_INACTIVE_CHECK_INTERVAL_MS milliseconds to remove any MFC that is
+ * inactive for more than MFC_INACTIVE_TIMEOUT_MS milliseconds.
+ */
+ private void maybeExpireMfcs() {
+ checkOnHandlerThread();
+
+ for (var it = mMfcs.entrySet().iterator(); it.hasNext(); ) {
+ var entry = it.next();
+ if (entry.getValue()
+ .getLastUsedAt()
+ .plusMillis(MFC_INACTIVE_TIMEOUT_MS)
+ .isBefore(Instant.now(mDependencies.getClock()))) {
+ removeMfcFromKernel(entry.getKey());
+ it.remove();
+ }
+ }
+ }
+
+ private void updateMfcs() {
+ checkOnHandlerThread();
+
+ for (Iterator<Map.Entry<MfcKey, MfcValue>> it = mMfcs.entrySet().iterator();
+ it.hasNext(); ) {
+ MfcKey key = it.next().getKey();
+ if (!addOrUpdateMfc(key.mIifVirtualIdx, key.mSrcAddr, key.mDstAddr)) {
+ removeMfcFromKernel(key);
+ it.remove();
+ }
+ }
+
+ refreshMfcInactiveDuration();
+ }
+
+ private void joinGroups(int ifIndex, List<Inet6Address> addresses) {
+ for (Inet6Address address : addresses) {
+ InetSocketAddress socketAddress = new InetSocketAddress(address, 0);
+ try {
+ mMulticastSocket.joinGroup(
+ socketAddress, mDependencies.getNetworkInterface(ifIndex));
+ } catch (IOException e) {
+ if (e.getCause() instanceof ErrnoException) {
+ ErrnoException ee = (ErrnoException) e.getCause();
+ if (ee.errno == EADDRINUSE) {
+ // The list of added address are calculated from address changes,
+ // repeated join group is unexpected
+ Log.e(TAG, "Already joined group" + e);
+ continue;
+ }
+ }
+ Log.e(TAG, "failed to join group: " + e);
+ }
+ }
+ }
+
+ private void leaveGroups(int ifIndex, List<Inet6Address> addresses) {
+ for (Inet6Address address : addresses) {
+ InetSocketAddress socketAddress = new InetSocketAddress(address, 0);
+ try {
+ mMulticastSocket.leaveGroup(
+ socketAddress, mDependencies.getNetworkInterface(ifIndex));
+ } catch (IOException e) {
+ Log.e(TAG, "failed to leave group: " + e);
+ }
+ }
+ }
+
+ private int getGroupAddressScope(Inet6Address address) {
+ return address.getAddress()[1] & 0xf;
+ }
+
+ /**
+ * Handles a NoCache upcall that indicates a multicast packet is received and requires
+ * a multicast forwarding cache to be added.
+ *
+ * A forwarding or blocking MFC is added according to the multicast config.
+ *
+ * The number of MFCs is checked to make sure it doesn't exceed the
+ * {@code MFC_MAX_NUMBER_OF_ENTRIES} limit.
+ */
+ @VisibleForTesting
+ public void handleMulticastNocacheUpcall(final StructMrt6Msg mrt6Msg) {
+ final int iifVid = mrt6Msg.mif;
+
+ // add MFC to forward the packet or add blocking MFC to not forward the packet
+ // If the packet comes from an interface the service doesn't care about, the
+ // addOrUpdateMfc function will return null and not MFC will be added.
+ if (!addOrUpdateMfc(iifVid, mrt6Msg.src, mrt6Msg.dst)) return;
+ // If the list of MFCs is not empty and there is no MFC check scheduled,
+ // schedule one now
+ if (!mMfcPollingScheduled) {
+ mHandler.postDelayed(() -> checkMfcsExpiration(), MFC_INACTIVE_CHECK_INTERVAL_MS);
+ mMfcPollingScheduled = true;
+ }
+
+ checkMfcEntriesLimit();
+ }
+
+ /**
+ * A packet reader that handles the packets sent to the multicast routing socket
+ */
+ private final class MulticastNocacheUpcallListener extends PacketReader {
+ private final FileDescriptor mFd;
+
+ public MulticastNocacheUpcallListener(Handler h, FileDescriptor fd) {
+ super(h);
+ mFd = fd;
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ return mFd;
+ }
+
+ @Override
+ protected void handlePacket(byte[] recvbuf, int length) {
+ final ByteBuffer buf = ByteBuffer.wrap(recvbuf);
+ final StructMrt6Msg mrt6Msg = StructMrt6Msg.parse(buf);
+ if (mrt6Msg.msgType != StructMrt6Msg.MRT6MSG_NOCACHE) {
+ return;
+ }
+ handleMulticastNocacheUpcall(mrt6Msg);
+ }
+ }
+
+ /** Dependencies of RoutingCoordinatorService, for test injections. */
+ @VisibleForTesting
+ public static class Dependencies {
+ private final Clock mClock = Clock.system(ZoneId.systemDefault());
+
+ /**
+ * Creates a socket to configure multicast routing in the kernel.
+ *
+ * <p>If the kernel doesn't support multicast routing, then the {@code setsockoptInt} with
+ * {@code MRT6_INIT} method would fail.
+ *
+ * @return the multicast routing socket, or null if it fails to be created/configured.
+ */
+ public FileDescriptor createMulticastRoutingSocket() {
+ FileDescriptor sock = null;
+ byte[] filter = new byte[32]; // filter all ICMPv6 messages
+ try {
+ sock = Os.socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_ICMPV6);
+ Os.setsockoptInt(sock, IPPROTO_IPV6, MRT6_INIT, ONE);
+ NetworkUtils.setsockoptBytes(sock, IPPROTO_ICMPV6, ICMP6_FILTER, filter);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "failed to create multicast socket: " + e);
+ if (sock != null) {
+ SocketUtils.closeSocketQuietly(sock);
+ }
+ throw new UnsupportedOperationException("Multicast routing is not supported ", e);
+ }
+ Log.i(TAG, "socket created for multicast routing: " + sock);
+ return sock;
+ }
+
+ public MulticastSocket createMulticastSocket() {
+ try {
+ return new MulticastSocket();
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to create multicast socket " + e);
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public void setsockoptMrt6AddMif(FileDescriptor fd, StructMif6ctl mif6ctl)
+ throws ErrnoException {
+ final byte[] bytes = mif6ctl.writeToBytes();
+ NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_ADD_MIF, bytes);
+ }
+
+ public void setsockoptMrt6DelMif(FileDescriptor fd, int virtualIfIndex)
+ throws ErrnoException {
+ Os.setsockoptInt(fd, IPPROTO_IPV6, MRT6_DEL_MIF, virtualIfIndex);
+ }
+
+ public void setsockoptMrt6AddMfc(FileDescriptor fd, StructMf6cctl mf6cctl)
+ throws ErrnoException {
+ final byte[] bytes = mf6cctl.writeToBytes();
+ NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_ADD_MFC, bytes);
+ }
+
+ public void setsockoptMrt6DelMfc(FileDescriptor fd, StructMf6cctl mf6cctl)
+ throws ErrnoException {
+ final byte[] bytes = mf6cctl.writeToBytes();
+ NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_DEL_MFC, bytes);
+ }
+
+ public Integer getInterfaceIndex(String ifName) {
+ try {
+ NetworkInterface ni = NetworkInterface.getByName(ifName);
+ return ni.getIndex();
+ } catch (NullPointerException | SocketException e) {
+ return null;
+ }
+ }
+
+ public NetworkInterface getNetworkInterface(int physicalIndex) {
+ try {
+ return NetworkInterface.getByIndex(physicalIndex);
+ } catch (SocketException e) {
+ return null;
+ }
+ }
+
+ public Clock getClock() {
+ return mClock;
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
new file mode 100644
index 0000000..ab3d315
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
@@ -0,0 +1,108 @@
+/*
+ * 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.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.SystemClock;
+
+import com.android.net.module.util.BitUtils;
+
+
+class NetworkRequestStateInfo {
+ private final NetworkRequest mNetworkRequest;
+ private final long mNetworkRequestReceivedTime;
+
+ private enum NetworkRequestState {
+ RECEIVED,
+ REMOVED
+ }
+ private NetworkRequestState mNetworkRequestState;
+ private int mNetworkRequestDurationMillis;
+ private final Dependencies mDependencies;
+
+ NetworkRequestStateInfo(NetworkRequest networkRequest,
+ Dependencies deps) {
+ mDependencies = deps;
+ mNetworkRequest = networkRequest;
+ mNetworkRequestReceivedTime = mDependencies.getElapsedRealtime();
+ mNetworkRequestDurationMillis = 0;
+ mNetworkRequestState = NetworkRequestState.RECEIVED;
+ }
+
+ NetworkRequestStateInfo(NetworkRequestStateInfo anotherNetworkRequestStateInfo) {
+ mDependencies = anotherNetworkRequestStateInfo.mDependencies;
+ mNetworkRequest = new NetworkRequest(anotherNetworkRequestStateInfo.mNetworkRequest);
+ mNetworkRequestReceivedTime = anotherNetworkRequestStateInfo.mNetworkRequestReceivedTime;
+ mNetworkRequestDurationMillis =
+ anotherNetworkRequestStateInfo.mNetworkRequestDurationMillis;
+ mNetworkRequestState = anotherNetworkRequestStateInfo.mNetworkRequestState;
+ }
+
+ public void setNetworkRequestRemoved() {
+ mNetworkRequestState = NetworkRequestState.REMOVED;
+ mNetworkRequestDurationMillis = (int) (
+ mDependencies.getElapsedRealtime() - mNetworkRequestReceivedTime);
+ }
+
+ public int getNetworkRequestStateStatsType() {
+ if (mNetworkRequestState == NetworkRequestState.RECEIVED) {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+ } else if (mNetworkRequestState == NetworkRequestState.REMOVED) {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+ } else {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+ }
+ }
+
+ public int getRequestId() {
+ return mNetworkRequest.requestId;
+ }
+
+ public int getPackageUid() {
+ return mNetworkRequest.networkCapabilities.getRequestorUid();
+ }
+
+ public int getTransportTypes() {
+ return (int) BitUtils.packBits(mNetworkRequest.networkCapabilities.getTransportTypes());
+ }
+
+ public boolean getNetCapabilityNotMetered() {
+ return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ }
+
+ public boolean getNetCapabilityInternet() {
+ return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ }
+
+ public int getNetworkRequestDurationMillis() {
+ return mNetworkRequestDurationMillis;
+ }
+
+ /** Dependency class */
+ public static class Dependencies {
+ // Returns a timestamp with the time base of SystemClock.elapsedRealtime to keep durations
+ // relative to start time and avoid timezone change, including time spent in deep sleep.
+ public long getElapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
new file mode 100644
index 0000000..1bc654a
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
@@ -0,0 +1,228 @@
+/*
+ * 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.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED;
+
+import android.annotation.NonNull;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+import java.util.ArrayDeque;
+
+/**
+ * A Connectivity Service helper class to push atoms capturing network requests have been received
+ * and removed and its metadata.
+ *
+ * Atom events are logged in the ConnectivityStatsLog. Network request id: network request metadata
+ * hashmap is stored to calculate network request duration when it is removed.
+ *
+ * Note that this class is not thread-safe. The instance of the class needs to be
+ * synchronized in the callers when being used in multiple threads.
+ */
+public class NetworkRequestStateStatsMetrics {
+
+ private static final String TAG = "NetworkRequestStateStatsMetrics";
+ private static final int CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC = 0;
+ private static final int CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC = 1;
+
+ @VisibleForTesting
+ static final int MAX_QUEUED_REQUESTS = 20;
+
+ // Stats logging frequency is limited to 10 ms at least, 500ms are taken as a safely margin
+ // for cases of longer periods of frequent network requests.
+ private static final int ATOM_INTERVAL_MS = 500;
+ private final StatsLoggingHandler mStatsLoggingHandler;
+
+ private final Dependencies mDependencies;
+
+ private final NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+ private final SparseArray<NetworkRequestStateInfo> mNetworkRequestsActive;
+
+ public NetworkRequestStateStatsMetrics() {
+ this(new Dependencies(), new NetworkRequestStateInfo.Dependencies());
+ }
+
+ @VisibleForTesting
+ NetworkRequestStateStatsMetrics(Dependencies deps,
+ NetworkRequestStateInfo.Dependencies nrStateInfoDeps) {
+ mNetworkRequestsActive = new SparseArray<>();
+ mDependencies = deps;
+ mNRStateInfoDeps = nrStateInfoDeps;
+ HandlerThread handlerThread = mDependencies.makeHandlerThread(TAG);
+ handlerThread.start();
+ mStatsLoggingHandler = new StatsLoggingHandler(handlerThread.getLooper());
+ }
+
+ /**
+ * Register network request receive event, push RECEIVE atom
+ *
+ * @param networkRequest network request received
+ */
+ public void onNetworkRequestReceived(NetworkRequest networkRequest) {
+ if (mNetworkRequestsActive.contains(networkRequest.requestId)) {
+ Log.w(TAG, "Received already registered network request, id = "
+ + networkRequest.requestId);
+ } else {
+ Log.d(TAG, "Registered nr with ID = " + networkRequest.requestId
+ + ", package_uid = " + networkRequest.networkCapabilities.getRequestorUid());
+ NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+ networkRequest, mNRStateInfoDeps);
+ mNetworkRequestsActive.put(networkRequest.requestId, networkRequestStateInfo);
+ mStatsLoggingHandler.sendMessage(Message.obtain(
+ mStatsLoggingHandler,
+ CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
+ networkRequestStateInfo));
+ }
+ }
+
+ /**
+ * Register network request remove event, push REMOVE atom
+ *
+ * @param networkRequest network request removed
+ */
+ public void onNetworkRequestRemoved(NetworkRequest networkRequest) {
+ NetworkRequestStateInfo networkRequestStateInfo = mNetworkRequestsActive.get(
+ networkRequest.requestId);
+ if (networkRequestStateInfo == null) {
+ Log.w(TAG, "This NR hasn't been registered. NR id = " + networkRequest.requestId);
+ } else {
+ Log.d(TAG, "Removed nr with ID = " + networkRequest.requestId);
+ mNetworkRequestsActive.remove(networkRequest.requestId);
+ networkRequestStateInfo = new NetworkRequestStateInfo(networkRequestStateInfo);
+ networkRequestStateInfo.setNetworkRequestRemoved();
+ mStatsLoggingHandler.sendMessage(Message.obtain(
+ mStatsLoggingHandler,
+ CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
+ networkRequestStateInfo));
+ }
+ }
+
+ /** Dependency class */
+ public static class Dependencies {
+ /**
+ * Creates a thread with provided tag.
+ *
+ * @param tag for the thread.
+ */
+ public HandlerThread makeHandlerThread(@NonNull final String tag) {
+ return new HandlerThread(tag);
+ }
+
+ /**
+ * @see Handler#sendMessageDelayed(Message, long)
+ */
+ public void sendMessageDelayed(@NonNull Handler handler, int what, long delayMillis) {
+ handler.sendMessageDelayed(Message.obtain(handler, what), delayMillis);
+ }
+
+ /**
+ * Gets number of millis since event.
+ *
+ * @param eventTimeMillis long timestamp in millis when the event occurred.
+ */
+ public long getMillisSinceEvent(long eventTimeMillis) {
+ return SystemClock.elapsedRealtime() - eventTimeMillis;
+ }
+
+ /**
+ * Writes a NETWORK_REQUEST_STATE_CHANGED event to ConnectivityStatsLog.
+ *
+ * @param networkRequestStateInfo NetworkRequestStateInfo containing network request info.
+ */
+ public void writeStats(NetworkRequestStateInfo networkRequestStateInfo) {
+ ConnectivityStatsLog.write(
+ NETWORK_REQUEST_STATE_CHANGED,
+ networkRequestStateInfo.getPackageUid(),
+ networkRequestStateInfo.getTransportTypes(),
+ networkRequestStateInfo.getNetCapabilityNotMetered(),
+ networkRequestStateInfo.getNetCapabilityInternet(),
+ networkRequestStateInfo.getNetworkRequestStateStatsType(),
+ networkRequestStateInfo.getNetworkRequestDurationMillis());
+ }
+ }
+
+ private class StatsLoggingHandler extends Handler {
+ private static final String TAG = "NetworkRequestsStateStatsLoggingHandler";
+
+ private final ArrayDeque<NetworkRequestStateInfo> mPendingState = new ArrayDeque<>();
+
+ private long mLastLogTime = 0;
+
+ StatsLoggingHandler(Looper looper) {
+ super(looper);
+ }
+
+ private void maybeEnqueueStatsMessage(NetworkRequestStateInfo networkRequestStateInfo) {
+ if (mPendingState.size() < MAX_QUEUED_REQUESTS) {
+ mPendingState.add(networkRequestStateInfo);
+ } else {
+ Log.w(TAG, "Too many network requests received within last " + ATOM_INTERVAL_MS
+ + " ms, dropping the last network request (id = "
+ + networkRequestStateInfo.getRequestId() + ") event");
+ return;
+ }
+ if (hasMessages(CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC)) {
+ return;
+ }
+ long millisSinceLastLog = mDependencies.getMillisSinceEvent(mLastLogTime);
+
+ if (millisSinceLastLog >= ATOM_INTERVAL_MS) {
+ sendMessage(
+ Message.obtain(this, CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC));
+ } else {
+ mDependencies.sendMessageDelayed(
+ this,
+ CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC,
+ ATOM_INTERVAL_MS - millisSinceLastLog);
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ NetworkRequestStateInfo loggingInfo;
+ switch (msg.what) {
+ case CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC:
+ maybeEnqueueStatsMessage((NetworkRequestStateInfo) msg.obj);
+ break;
+ case CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC:
+ mLastLogTime = SystemClock.elapsedRealtime();
+ if (!mPendingState.isEmpty()) {
+ loggingInfo = mPendingState.remove();
+ mDependencies.writeStats(loggingInfo);
+ if (!mPendingState.isEmpty()) {
+ mDependencies.sendMessageDelayed(
+ this,
+ CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC,
+ ATOM_INTERVAL_MS);
+ }
+ }
+ break;
+ default: // fall out
+ }
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
index 3350d2d..742a2cc 100644
--- a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
+++ b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
@@ -171,7 +171,8 @@
}
final ForwardingPair fwp = new ForwardingPair(fromIface, toIface);
if (mForwardedInterfaces.contains(fwp)) {
- throw new IllegalStateException("Forward already exists between ifaces "
+ // TODO: remove if no reports are observed from the below log
+ Log.wtf(TAG, "Forward already exists between ifaces "
+ fromIface + " → " + toIface);
}
mForwardedInterfaces.add(fwp);
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 9f1debc..8f018c0 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -43,6 +43,7 @@
"device/com/android/net/module/util/SharedLog.java",
"device/com/android/net/module/util/SocketUtils.java",
"device/com/android/net/module/util/FeatureVersions.java",
+ "device/com/android/net/module/util/HandlerUtils.java",
// This library is used by system modules, for which the system health impact of Kotlin
// has not yet been evaluated. Annotations may need jarjar'ing.
// "src_devicecommon/**/*.kt",
@@ -295,6 +296,10 @@
"-Xep:NullablePrimitive:ERROR",
],
},
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.tethering",
+ ],
}
java_library {
diff --git a/staticlibs/device/com/android/net/module/util/BpfBitmap.java b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
index acb3ca5..b62a430 100644
--- a/staticlibs/device/com/android/net/module/util/BpfBitmap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
@@ -38,8 +38,7 @@
* @param path The path of the BPF map.
*/
public BpfBitmap(@NonNull String path) throws ErrnoException {
- mBpfMap = new BpfMap<Struct.S32, Struct.S64>(path, BpfMap.BPF_F_RDWR,
- Struct.S32.class, Struct.S64.class);
+ mBpfMap = new BpfMap<>(path, Struct.S32.class, Struct.S64.class);
}
/**
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index e3ef0f0..da77ae8 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -27,7 +27,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.NoSuchElementException;
@@ -110,6 +109,17 @@
}
/**
+ * Create a R/W BpfMap map wrapper with "path" of filesystem.
+ *
+ * @throws ErrnoException if the BPF map associated with {@code path} cannot be retrieved.
+ * @throws NullPointerException if {@code path} is null.
+ */
+ public BpfMap(@NonNull final String path, final Class<K> key,
+ final Class<V> value) throws ErrnoException, NullPointerException {
+ this(path, BPF_F_RDWR, key, value);
+ }
+
+ /**
* Update an existing or create a new key -> value entry in an eBbpf map.
* (use insertOrReplaceEntry() if you need to know whether insert or replace happened)
*/
diff --git a/staticlibs/device/com/android/net/module/util/HandlerUtils.java b/staticlibs/device/com/android/net/module/util/HandlerUtils.java
new file mode 100644
index 0000000..c620368
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/HandlerUtils.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Helper class for Handler related utilities.
+ *
+ * @hide
+ */
+public class HandlerUtils {
+ /**
+ * Runs the specified task synchronously for dump method.
+ * <p>
+ * If the current thread is the same as the handler thread, then the runnable
+ * runs immediately without being enqueued. Otherwise, posts the runnable
+ * to the handler and waits for it to complete before returning.
+ * </p><p>
+ * This method is dangerous! Improper use can result in deadlocks.
+ * Never call this method while any locks are held or use it in a
+ * possibly re-entrant manner.
+ * </p><p>
+ * This method is made to let dump method access members on the handler thread to
+ * avoid concurrent access problems or races.
+ * </p><p>
+ * If timeout occurs then this method returns <code>false</code> but the runnable
+ * will remain posted on the handler and may already be in progress or
+ * complete at a later time.
+ * </p><p>
+ * When using this method, be sure to use {@link Looper#quitSafely} when
+ * quitting the looper. Otherwise {@link #runWithScissorsForDump} may hang indefinitely.
+ * (TODO: We should fix this by making MessageQueue aware of blocking runnables.)
+ * </p>
+ *
+ * @param h The target handler.
+ * @param r The Runnable that will be executed synchronously.
+ * @param timeout The timeout in milliseconds, or 0 to not wait at all.
+ *
+ * @return Returns true if the Runnable was successfully executed.
+ * Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ *
+ * @hide
+ */
+ public static boolean runWithScissorsForDump(@NonNull Handler h, @NonNull Runnable r,
+ long timeout) {
+ if (r == null) {
+ throw new IllegalArgumentException("runnable must not be null");
+ }
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout must be non-negative");
+ }
+ if (Looper.myLooper() == h.getLooper()) {
+ r.run();
+ return true;
+ }
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ // Don't crash in the handler if something in the runnable throws an exception,
+ // but try to propagate the exception to the caller.
+ AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
+ h.post(() -> {
+ try {
+ r.run();
+ } catch (RuntimeException e) {
+ exceptionRef.set(e);
+ }
+ latch.countDown();
+ });
+
+ try {
+ if (!latch.await(timeout, TimeUnit.MILLISECONDS)) {
+ return false;
+ }
+ } catch (InterruptedException e) {
+ exceptionRef.compareAndSet(null, new IllegalStateException("Thread interrupted", e));
+ }
+
+ final RuntimeException e = exceptionRef.get();
+ if (e != null) throw e;
+ return true;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
index d538221..497b8cb 100644
--- a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
+++ b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
@@ -166,6 +166,24 @@
}
/**
+ * Build an ICMPv6 Router Solicitation packet from the required specified parameters without
+ * ethernet header.
+ */
+ public static ByteBuffer buildRsPacket(
+ final Inet6Address srcIp, final Inet6Address dstIp, final ByteBuffer... options) {
+ final RsHeader rsHeader = new RsHeader((int) 0 /* reserved */);
+ final ByteBuffer[] payload =
+ buildIcmpv6Payload(
+ ByteBuffer.wrap(rsHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+ return buildIcmpv6Packet(
+ srcIp,
+ dstIp,
+ (byte) ICMPV6_ROUTER_SOLICITATION /* type */,
+ (byte) 0 /* code */,
+ payload);
+ }
+
+ /**
* Build an ICMPv6 Echo Request packet from the required specified parameters.
*/
public static ByteBuffer buildEchoRequestPacket(final MacAddress srcMac,
@@ -176,11 +194,21 @@
}
/**
- * Build an ICMPv6 Echo Reply packet without ethernet header.
+ * Build an ICMPv6 Echo Request packet from the required specified parameters without ethernet
+ * header.
*/
- public static ByteBuffer buildEchoReplyPacket(final Inet6Address srcIp,
+ public static ByteBuffer buildEchoRequestPacket(final Inet6Address srcIp,
final Inet6Address dstIp) {
final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
+ return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REQUEST_TYPE /* type */,
+ (byte) 0 /* code */,
+ payload);
+ }
+
+ /** Build an ICMPv6 Echo Reply packet without ethernet header. */
+ public static ByteBuffer buildEchoReplyPacket(
+ final Inet6Address srcIp, final Inet6Address dstIp) {
+ final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REPLY_TYPE /* type */,
(byte) 0 /* code */, payload);
}
diff --git a/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java b/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
index dab9694..bf447d3 100644
--- a/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
+++ b/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
@@ -45,14 +45,18 @@
public class ArpPacket {
private static final String TAG = "ArpPacket";
+ public final MacAddress destination;
+ public final MacAddress source;
public final short opCode;
public final Inet4Address senderIp;
public final Inet4Address targetIp;
public final MacAddress senderHwAddress;
public final MacAddress targetHwAddress;
- ArpPacket(short opCode, MacAddress senderHwAddress, Inet4Address senderIp,
- MacAddress targetHwAddress, Inet4Address targetIp) {
+ ArpPacket(MacAddress destination, MacAddress source, short opCode, MacAddress senderHwAddress,
+ Inet4Address senderIp, MacAddress targetHwAddress, Inet4Address targetIp) {
+ this.destination = destination;
+ this.source = source;
this.opCode = opCode;
this.senderHwAddress = senderHwAddress;
this.senderIp = senderIp;
@@ -145,7 +149,9 @@
buffer.get(targetHwAddress);
buffer.get(targetIp);
- return new ArpPacket(opCode, MacAddress.fromBytes(senderHwAddress),
+ return new ArpPacket(MacAddress.fromBytes(l2dst),
+ MacAddress.fromBytes(l2src), opCode,
+ MacAddress.fromBytes(senderHwAddress),
(Inet4Address) InetAddress.getByAddress(senderIp),
MacAddress.fromBytes(targetHwAddress),
(Inet4Address) InetAddress.getByAddress(targetIp));
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 4f76577..b980c7d 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -24,10 +24,9 @@
import static android.system.OsConstants.IPPROTO_UDP;
import static android.system.OsConstants.NETLINK_INET_DIAG;
-import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DESTROY;
import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
-import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
import static com.android.net.module.util.netlink.NetlinkConstants.stringForAddressFamily;
import static com.android.net.module.util.netlink.NetlinkConstants.stringForProtocol;
import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
@@ -59,8 +58,11 @@
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import java.util.function.Predicate;
/**
@@ -154,7 +156,8 @@
}
public StructInetDiagMsg inetDiagMsg;
-
+ // The netlink attributes.
+ public List<StructNlAttr> nlAttrs = new ArrayList<>();
@VisibleForTesting
public InetDiagMessage(@NonNull StructNlMsgHdr header) {
super(header);
@@ -172,6 +175,19 @@
if (msg.inetDiagMsg == null) {
return null;
}
+ final int payloadLength = header.nlmsg_len - SOCKDIAG_MSG_HEADER_SIZE;
+ final ByteBuffer payload = byteBuffer.slice();
+ while (payload.position() < payloadLength) {
+ final StructNlAttr attr = StructNlAttr.parse(payload);
+ // Stop parsing for truncated or malformed attribute
+ if (attr == null) {
+ Log.wtf(TAG, "Got truncated or malformed attribute");
+ return null;
+ }
+
+ msg.nlAttrs.add(attr);
+ }
+
return msg;
}
@@ -307,9 +323,8 @@
NetlinkUtils.receiveNetlinkAck(fd);
}
- private static void sendNetlinkDumpRequest(FileDescriptor fd, int proto, int states, int family)
- throws InterruptedIOException, ErrnoException {
- final byte[] dumpMsg = InetDiagMessage.inetDiagReqV2(
+ private static byte [] makeNetlinkDumpRequest(int proto, int states, int family) {
+ return InetDiagMessage.inetDiagReqV2(
proto,
null /* id */,
family,
@@ -318,51 +333,29 @@
0 /* pad */,
0 /* idiagExt */,
states);
- NetlinkUtils.sendMessage(fd, dumpMsg, 0, dumpMsg.length, IO_TIMEOUT_MS);
}
- private static int processNetlinkDumpAndDestroySockets(FileDescriptor dumpFd,
+ private static int processNetlinkDumpAndDestroySockets(byte[] dumpReq,
FileDescriptor destroyFd, int proto, Predicate<InetDiagMessage> filter)
- throws InterruptedIOException, ErrnoException {
- int destroyedSockets = 0;
-
- while (true) {
- final ByteBuffer buf = NetlinkUtils.recvMessage(
- dumpFd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
-
- while (buf.remaining() > 0) {
- final int position = buf.position();
- final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_INET_DIAG);
- if (nlMsg == null) {
- // Move to the position where parse started for error log.
- buf.position(position);
- Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
- break;
- }
-
- if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
- return destroyedSockets;
- }
-
- if (!(nlMsg instanceof InetDiagMessage)) {
- Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
- continue;
- }
-
- final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
- if (filter.test(diagMsg)) {
- try {
- sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
- destroyedSockets++;
- } catch (InterruptedIOException | ErrnoException e) {
- if (!(e instanceof ErrnoException
- && ((ErrnoException) e).errno == ENOENT)) {
- Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
- }
+ throws SocketException, InterruptedIOException, ErrnoException {
+ AtomicInteger destroyedSockets = new AtomicInteger(0);
+ Consumer<InetDiagMessage> handleNlDumpMsg = (diagMsg) -> {
+ if (filter.test(diagMsg)) {
+ try {
+ sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
+ destroyedSockets.getAndIncrement();
+ } catch (InterruptedIOException | ErrnoException e) {
+ if (!(e instanceof ErrnoException
+ && ((ErrnoException) e).errno == ENOENT)) {
+ Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
}
}
}
- }
+ };
+
+ NetlinkUtils.<InetDiagMessage>getAndProcessNetlinkDumpMessages(dumpReq,
+ NETLINK_INET_DIAG, InetDiagMessage.class, handleNlDumpMsg);
+ return destroyedSockets.get();
}
/**
@@ -420,31 +413,28 @@
private static void destroySockets(int proto, int states, Predicate<InetDiagMessage> filter)
throws ErrnoException, SocketException, InterruptedIOException {
- FileDescriptor dumpFd = null;
FileDescriptor destroyFd = null;
try {
- dumpFd = NetlinkUtils.createNetLinkInetDiagSocket();
destroyFd = NetlinkUtils.createNetLinkInetDiagSocket();
- connectToKernel(dumpFd);
connectToKernel(destroyFd);
for (int family : List.of(AF_INET, AF_INET6)) {
+ byte[] req = makeNetlinkDumpRequest(proto, states, family);
+
try {
- sendNetlinkDumpRequest(dumpFd, proto, states, family);
- } catch (InterruptedIOException | ErrnoException e) {
- Log.e(TAG, "Failed to send netlink dump request: " + e);
- continue;
- }
- final int destroyedSockets = processNetlinkDumpAndDestroySockets(
- dumpFd, destroyFd, proto, filter);
- Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+ final int destroyedSockets = processNetlinkDumpAndDestroySockets(
+ req, destroyFd, proto, filter);
+ Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+ ", proto=" + stringForProtocol(proto)
+ ", family=" + stringForAddressFamily(family)
+ ", states=" + states);
+ } catch (SocketException | InterruptedIOException | ErrnoException e) {
+ Log.e(TAG, "Failed to send netlink dump request or receive messages: " + e);
+ continue;
+ }
}
} finally {
- closeSocketQuietly(dumpFd);
closeSocketQuietly(destroyFd);
}
}
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 f6282fd..7c2be2c 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -31,10 +31,10 @@
import static android.system.OsConstants.SO_SNDTIMEO;
import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
-import android.net.ParseException;
import android.net.util.SocketUtils;
import android.system.ErrnoException;
import android.system.Os;
@@ -314,49 +314,20 @@
private NetlinkUtils() {}
- /**
- * Sends a netlink dump request and processes the returned dump messages
- *
- * @param <T> extends NetlinkMessage
- * @param dumpRequestMessage netlink dump request message to be sent
- * @param nlFamily netlink family
- * @param msgClass expected class of the netlink message
- * @param func function defined by caller to handle the dump messages
- * @throws SocketException when fails to create socket
- * @throws InterruptedIOException when fails to read the dumpFd
- * @throws ErrnoException when fails to send dump request
- * @throws ParseException when message can't be parsed
- */
- public static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessages(
- byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+ private static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessagesWithFd(
+ FileDescriptor fd, byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
Consumer<T> func)
- throws SocketException, InterruptedIOException, ErrnoException, ParseException {
- // Create socket and send dump request
- final FileDescriptor fd;
- try {
- fd = netlinkSocketForProto(nlFamily);
- } catch (ErrnoException e) {
- Log.e(TAG, "Failed to create netlink socket " + e);
- throw e.rethrowAsSocketException();
- }
+ throws SocketException, InterruptedIOException, ErrnoException {
+ // connecToKernel throws ErrnoException and SocketException, should be handled by caller
+ connectToKernel(fd);
- try {
- connectToKernel(fd);
- } catch (ErrnoException | SocketException e) {
- Log.e(TAG, "Failed to connect netlink socket to kernel " + e);
- closeSocketQuietly(fd);
- return;
- }
-
- try {
- sendMessage(fd, dumpRequestMessage, 0, dumpRequestMessage.length, IO_TIMEOUT_MS);
- } catch (InterruptedIOException | ErrnoException e) {
- Log.e(TAG, "Failed to send dump request " + e);
- closeSocketQuietly(fd);
- throw e;
- }
+ // sendMessage throws InterruptedIOException and ErrnoException,
+ // should be handled by caller
+ sendMessage(fd, dumpRequestMessage, 0, dumpRequestMessage.length, IO_TIMEOUT_MS);
while (true) {
+ // recvMessage throws ErrnoException, InterruptedIOException
+ // should be handled by caller
final ByteBuffer buf = recvMessage(
fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
@@ -367,17 +338,15 @@
// Move to the position where parse started for error log.
buf.position(position);
Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
- closeSocketQuietly(fd);
- throw new ParseException("Failed to parse netlink message");
+ break;
}
if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
- closeSocketQuietly(fd);
return;
}
if (!msgClass.isInstance(nlMsg)) {
- Log.e(TAG, "Received unexpected netlink message: " + nlMsg);
+ Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
continue;
}
@@ -386,6 +355,91 @@
}
}
}
+ /**
+ * Sends a netlink dump request and processes the returned dump messages
+ *
+ * @param <T> extends NetlinkMessage
+ * @param dumpRequestMessage netlink dump request message to be sent
+ * @param nlFamily netlink family
+ * @param msgClass expected class of the netlink message
+ * @param func function defined by caller to handle the dump messages
+ * @throws SocketException when fails to connect socket to kernel
+ * @throws InterruptedIOException when fails to read the dumpFd
+ * @throws ErrnoException when fails to create dump fd, send dump request
+ * or receive messages
+ */
+ public static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessages(
+ byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+ Consumer<T> func)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ // Create socket
+ final FileDescriptor fd = netlinkSocketForProto(nlFamily);
+ try {
+ getAndProcessNetlinkDumpMessagesWithFd(fd, dumpRequestMessage, nlFamily,
+ msgClass, func);
+ } finally {
+ closeSocketQuietly(fd);
+ }
+ }
+
+ /**
+ * Construct a RTM_GETROUTE message for dumping multicast IPv6 routes from kernel.
+ */
+ private static byte[] newIpv6MulticastRouteDumpRequest() {
+ final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+ nlmsghdr.nlmsg_type = NetlinkConstants.RTM_GETROUTE;
+ nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+ final short shortZero = 0;
+
+ // family must be RTNL_FAMILY_IP6MR to dump IPv6 multicast routes.
+ // dstLen, srcLen, tos and scope must be zero in FIB dump request.
+ // protocol, flags must be 0, and type must be RTN_MULTICAST (if not 0) for multicast
+ // dump request.
+ // table or RTA_TABLE attributes can be used to dump a specific routing table.
+ // RTA_OIF attribute can be used to dump only routes containing given oif.
+ // Here no attributes are set so the kernel can return all multicast routes.
+ final StructRtMsg rtMsg =
+ new StructRtMsg(RTNL_FAMILY_IP6MR /* family */, shortZero /* dstLen */,
+ shortZero /* srcLen */, shortZero /* tos */, shortZero /* table */,
+ shortZero /* protocol */, shortZero /* scope */, shortZero /* type */,
+ 0L /* flags */);
+ final RtNetlinkRouteMessage msg =
+ new RtNetlinkRouteMessage(nlmsghdr, rtMsg);
+
+ final int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructRtMsg.STRUCT_SIZE;
+ nlmsghdr.nlmsg_len = spaceRequired;
+ final byte[] bytes = new byte[NetlinkConstants.alignedLengthOf(spaceRequired)];
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+ byteBuffer.order(ByteOrder.nativeOrder());
+ msg.pack(byteBuffer);
+ return bytes;
+ }
+
+ /**
+ * Get the list of IPv6 multicast route messages from kernel.
+ */
+ public static List<RtNetlinkRouteMessage> getIpv6MulticastRoutes() {
+ final byte[] dumpMsg = newIpv6MulticastRouteDumpRequest();
+ List<RtNetlinkRouteMessage> routes = new ArrayList<>();
+ Consumer<RtNetlinkRouteMessage> handleNlDumpMsg = (msg) -> {
+ if (msg.getRtmFamily() == RTNL_FAMILY_IP6MR) {
+ // Sent rtmFamily RTNL_FAMILY_IP6MR in dump request to make sure ipv6
+ // multicast routes are included in netlink reply messages, the kernel
+ // may also reply with other kind of routes, so we filter them out here.
+ routes.add(msg);
+ }
+ };
+ try {
+ NetlinkUtils.<RtNetlinkRouteMessage>getAndProcessNetlinkDumpMessages(
+ dumpMsg, NETLINK_ROUTE, RtNetlinkRouteMessage.class,
+ handleNlDumpMsg);
+ } catch (SocketException | InterruptedIOException | ErrnoException e) {
+ Log.e(TAG, "Failed to dump multicast routes");
+ return routes;
+ }
+
+ return routes;
+ }
private static void closeSocketQuietly(final FileDescriptor fd) {
try {
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java
new file mode 100644
index 0000000..24e0a97
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import static android.system.OsConstants.AF_INET6;
+
+import com.android.net.module.util.Struct;
+import java.net.Inet6Address;
+import java.util.Set;
+
+/*
+ * Implements the mf6cctl structure which is used to add a multicast forwarding
+ * cache, see /usr/include/linux/mroute6.h
+ */
+public class StructMf6cctl extends Struct {
+ // struct sockaddr_in6 mf6cc_origin, added the fields directly as Struct
+ // doesn't support nested Structs
+ @Field(order = 0, type = Type.U16)
+ public final int originFamily; // AF_INET6
+ @Field(order = 1, type = Type.U16)
+ public final int originPort; // Transport layer port # of origin
+ @Field(order = 2, type = Type.U32)
+ public final long originFlowinfo; // IPv6 flow information
+ @Field(order = 3, type = Type.ByteArray, arraysize = 16)
+ public final byte[] originAddress; //the IPv6 address of origin
+ @Field(order = 4, type = Type.U32)
+ public final long originScopeId; // scope id, not used
+
+ // struct sockaddr_in6 mf6cc_mcastgrp
+ @Field(order = 5, type = Type.U16)
+ public final int groupFamily; // AF_INET6
+ @Field(order = 6, type = Type.U16)
+ public final int groupPort; // Transport layer port # of multicast group
+ @Field(order = 7, type = Type.U32)
+ public final long groupFlowinfo; // IPv6 flow information
+ @Field(order = 8, type = Type.ByteArray, arraysize = 16)
+ public final byte[] groupAddress; //the IPv6 address of multicast group
+ @Field(order = 9, type = Type.U32)
+ public final long groupScopeId; // scope id, not used
+
+ @Field(order = 10, type = Type.U16, padding = 2)
+ public final int mf6ccParent; // incoming interface
+ @Field(order = 11, type = Type.ByteArray, arraysize = 32)
+ public final byte[] mf6ccIfset; // outgoing interfaces
+
+ public StructMf6cctl(final Inet6Address origin, final Inet6Address group,
+ final int mf6ccParent, final Set<Integer> oifset) {
+ this(AF_INET6, 0, (long) 0, origin.getAddress(), (long) 0, AF_INET6,
+ 0, (long) 0, group.getAddress(), (long) 0, mf6ccParent,
+ getMf6ccIfsetBytes(oifset));
+ }
+
+ private StructMf6cctl(int originFamily, int originPort, long originFlowinfo,
+ byte[] originAddress, long originScopeId, int groupFamily, int groupPort,
+ long groupFlowinfo, byte[] groupAddress, long groupScopeId, int mf6ccParent,
+ byte[] mf6ccIfset) {
+ this.originFamily = originFamily;
+ this.originPort = originPort;
+ this.originFlowinfo = originFlowinfo;
+ this.originAddress = originAddress;
+ this.originScopeId = originScopeId;
+ this.groupFamily = groupFamily;
+ this.groupPort = groupPort;
+ this.groupFlowinfo = groupFlowinfo;
+ this.groupAddress = groupAddress;
+ this.groupScopeId = groupScopeId;
+ this.mf6ccParent = mf6ccParent;
+ this.mf6ccIfset = mf6ccIfset;
+ }
+
+ private static byte[] getMf6ccIfsetBytes(final Set<Integer> oifs)
+ throws IllegalArgumentException {
+ byte[] mf6ccIfset = new byte[32];
+ for (int oif : oifs) {
+ int idx = oif / 8;
+ if (idx >= 32) {
+ // invalid oif index, too big to fit in mf6ccIfset
+ throw new IllegalArgumentException("Invalid oif index" + oif);
+ }
+ int offset = oif % 8;
+ mf6ccIfset[idx] |= (byte) (1 << offset);
+ }
+ return mf6ccIfset;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java
new file mode 100644
index 0000000..626a170
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+
+/*
+ * Implements the mif6ctl structure which is used to add a multicast routing
+ * interface, see /usr/include/linux/mroute6.h
+ */
+public class StructMif6ctl extends Struct {
+ @Field(order = 0, type = Type.U16)
+ public final int mif6cMifi; // Index of MIF
+ @Field(order = 1, type = Type.U8)
+ public final short mif6cFlags; // MIFF_ flags
+ @Field(order = 2, type = Type.U8)
+ public final short vifcThreshold; // ttl limit
+ @Field(order = 3, type = Type.U16)
+ public final int mif6cPifi; //the index of the physical IF
+ @Field(order = 4, type = Type.U32, padding = 2)
+ public final long vifcRateLimit; // Rate limiter values (NI)
+
+ public StructMif6ctl(final int mif6cMifi, final short mif6cFlags, final short vifcThreshold,
+ final int mif6cPifi, final long vifcRateLimit) {
+ this.mif6cMifi = mif6cMifi;
+ this.mif6cFlags = mif6cFlags;
+ this.vifcThreshold = vifcThreshold;
+ this.mif6cPifi = mif6cPifi;
+ this.vifcRateLimit = vifcRateLimit;
+ }
+}
+
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java b/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java
new file mode 100644
index 0000000..569e361
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class StructMrt6Msg extends Struct {
+ public static final byte MRT6MSG_NOCACHE = 1;
+
+ @Field(order = 0, type = Type.S8)
+ public final byte mbz;
+ @Field(order = 1, type = Type.S8)
+ public final byte msgType; // message type
+ @Field(order = 2, type = Type.U16, padding = 4)
+ public final int mif; // mif received on
+ @Field(order = 3, type = Type.Ipv6Address)
+ public final Inet6Address src;
+ @Field(order = 4, type = Type.Ipv6Address)
+ public final Inet6Address dst;
+
+ public StructMrt6Msg(final byte mbz, final byte msgType, final int mif,
+ final Inet6Address source, final Inet6Address destination) {
+ this.mbz = mbz; // kernel should set it to 0
+ this.msgType = msgType;
+ this.mif = mif;
+ this.src = source;
+ this.dst = destination;
+ }
+
+ public static StructMrt6Msg parse(ByteBuffer byteBuffer) {
+ byteBuffer.order(ByteOrder.nativeOrder());
+ return Struct.parse(StructMrt6Msg.class, byteBuffer);
+ }
+}
+
diff --git a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
index cd1f31c..f6bee69 100644
--- a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
+++ b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
@@ -189,8 +189,9 @@
* @param message A message describing why the permission was checked. Only needed if this is
* not inside of a two-way binder call from the data receiver
*/
- public boolean checkCallersLocationPermission(String pkgName, @Nullable String featureId,
- int uid, boolean coarseForTargetSdkLessThanQ, @Nullable String message) {
+ public boolean checkCallersLocationPermission(@Nullable String pkgName,
+ @Nullable String featureId, int uid, boolean coarseForTargetSdkLessThanQ,
+ @Nullable String message) {
boolean isTargetSdkLessThanQ = isTargetSdkLessThan(pkgName, Build.VERSION_CODES.Q, uid);
diff --git a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
index 8315b8f..f167d3d 100644
--- a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
@@ -27,7 +27,9 @@
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.os.Binder;
+import android.os.UserHandle;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -183,4 +185,33 @@
}
return result;
}
+
+ /**
+ * Enforces that the given package name belongs to the given uid.
+ *
+ * @param context {@link android.content.Context} for the process.
+ * @param uid User ID to check the package ownership for.
+ * @param packageName Package name to verify.
+ * @throws SecurityException If the package does not belong to the specified uid.
+ */
+ public static void enforcePackageNameMatchesUid(
+ @NonNull Context context, int uid, @Nullable String packageName) {
+ final UserHandle user = UserHandle.getUserHandleForUid(uid);
+ if (getAppUid(context, packageName, user) != uid) {
+ throw new SecurityException(packageName + " does not belong to uid " + uid);
+ }
+ }
+
+ private static int getAppUid(Context context, final String app, final UserHandle user) {
+ final PackageManager pm =
+ context.createContextAsUser(user, 0 /* flags */).getPackageManager();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return pm.getPackageUid(app, 0 /* flags */);
+ } catch (PackageManager.NameNotFoundException e) {
+ return -1;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
}
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
index 3fede3c..1037beb 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
@@ -26,6 +26,8 @@
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
+#include <functional>
+
namespace android {
namespace bpf {
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 637a938..2b7e620 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -241,5 +241,17 @@
min_sdk_version: "30",
},
},
- versions: ["1"],
+ versions_with_info: [
+ {
+ version: "1",
+ imports: [],
+ },
+ {
+ version: "2",
+ imports: [],
+ },
+
+ ],
+ frozen: true,
+
}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash
new file mode 100644
index 0000000..785d42d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash
@@ -0,0 +1 @@
+0e5d9ad0664b8b3ec9d323534c42333cf6f6ed3d
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl
new file mode 100644
index 0000000..d31a327
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable DiscoveryInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ @utf8InCpp String domainName;
+ int interfaceIdx;
+ int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl
new file mode 100644
index 0000000..2049274
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable GetAddressInfo {
+ int id;
+ int result;
+ @utf8InCpp String hostname;
+ @utf8InCpp String address;
+ int interfaceIdx;
+ int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl
new file mode 100644
index 0000000..d84742b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl
@@ -0,0 +1,73 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+interface IMDns {
+ /**
+ * @deprecated unimplemented on V+.
+ */
+ void startDaemon();
+ /**
+ * @deprecated unimplemented on V+.
+ */
+ void stopDaemon();
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void stopOperation(int id);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl
new file mode 100644
index 0000000..187a3d2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -0,0 +1,62 @@
+/**
+ * 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.mdns.aidl;
+/* @hide */
+interface IMDnsEventListener {
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
+ const int SERVICE_DISCOVERY_FAILED = 602;
+ const int SERVICE_FOUND = 603;
+ const int SERVICE_LOST = 604;
+ const int SERVICE_REGISTRATION_FAILED = 605;
+ const int SERVICE_REGISTERED = 606;
+ const int SERVICE_RESOLUTION_FAILED = 607;
+ const int SERVICE_RESOLVED = 608;
+ const int SERVICE_GET_ADDR_FAILED = 611;
+ const int SERVICE_GET_ADDR_SUCCESS = 612;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl
new file mode 100644
index 0000000..185111b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable RegistrationInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ int port;
+ byte[] txtRecord;
+ int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl
new file mode 100644
index 0000000..4aa7d79
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl
@@ -0,0 +1,48 @@
+/*
+ * 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.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable ResolutionInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ @utf8InCpp String domain;
+ @utf8InCpp String serviceFullName;
+ @utf8InCpp String hostname;
+ int port;
+ byte[] txtRecord;
+ int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
index ecbe966..d84742b 100644
--- a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
@@ -34,13 +34,40 @@
package android.net.mdns.aidl;
/* @hide */
interface IMDns {
+ /**
+ * @deprecated unimplemented on V+.
+ */
void startDaemon();
+ /**
+ * @deprecated unimplemented on V+.
+ */
void stopDaemon();
+ /**
+ * @deprecated unimplemented on U+.
+ */
void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void stopOperation(int id);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
index 4625cac..187a3d2 100644
--- a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -34,9 +34,21 @@
package android.net.mdns.aidl;
/* @hide */
interface IMDnsEventListener {
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
const int SERVICE_DISCOVERY_FAILED = 602;
const int SERVICE_FOUND = 603;
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
index 255d70f..3bf1da8 100644
--- a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
@@ -28,6 +28,8 @@
* Start the MDNSResponder daemon.
*
* @throws ServiceSpecificException with unix errno EALREADY if daemon is already running.
+ * @throws UnsupportedOperationException on Android V and after.
+ * @deprecated unimplemented on V+.
*/
void startDaemon();
@@ -35,6 +37,8 @@
* Stop the MDNSResponder daemon.
*
* @throws ServiceSpecificException with unix errno EBUSY if daemon is still in use.
+ * @throws UnsupportedOperationException on Android V and after.
+ * @deprecated unimplemented on V+.
*/
void stopDaemon();
@@ -49,6 +53,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if registration fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void registerService(in RegistrationInfo info);
@@ -63,6 +69,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if discovery fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void discover(in DiscoveryInfo info);
@@ -77,6 +85,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if resolution fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void resolve(in ResolutionInfo info);
@@ -92,6 +102,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if getting address fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void getServiceAddress(in GetAddressInfo info);
@@ -101,6 +113,8 @@
* @param id the operation id to be stopped.
*
* @throws ServiceSpecificException with unix errno ESRCH if request id is not in use.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void stopOperation(int id);
@@ -112,6 +126,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EINVAL if listener is null.
* - Unix errno EEXIST if register duplicated listener.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void registerEventListener(in IMDnsEventListener listener);
@@ -121,6 +137,8 @@
* @param listener The listener to be unregistered.
*
* @throws ServiceSpecificException with unix errno EINVAL if listener is null.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void unregisterEventListener(in IMDnsEventListener listener);
}
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
index a202a26..f7f028b 100644
--- a/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -31,8 +31,8 @@
oneway interface IMDnsEventListener {
/**
* Types for MDNS operation result.
- * These are in sync with frameworks/libs/net/common/netd/libnetdutils/include/netdutils/\
- * ResponseCode.h
+ * These are in sync with packages/modules/Connectivity/staticlibs/netd/libnetdutils/include/\
+ * netdutils/ResponseCode.h
*/
const int SERVICE_DISCOVERY_FAILED = 602;
const int SERVICE_FOUND = 603;
@@ -46,21 +46,29 @@
/**
* Notify service registration status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceRegistrationStatus(in RegistrationInfo status);
/**
* Notify service discovery status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceDiscoveryStatus(in DiscoveryInfo status);
/**
* Notify service resolution status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceResolutionStatus(in ResolutionInfo status);
/**
* Notify getting service address status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onGettingServiceAddressStatus(in GetAddressInfo status);
}
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
index d10cec7..d662739 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
@@ -21,6 +21,7 @@
#include <stdint.h>
#include <cstring>
#include <limits>
+#include <memory>
#include <string>
#include "netdutils/NetworkConstants.h"
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Log.h b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
index 77ae649..d266cbc 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Log.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
@@ -19,6 +19,7 @@
#include <chrono>
#include <deque>
+#include <functional>
#include <shared_mutex>
#include <string>
#include <type_traits>
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Slice.h b/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
index 717fbd1..aa12927 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
@@ -22,6 +22,7 @@
#include <cstring>
#include <ostream>
#include <tuple>
+#include <type_traits>
#include <vector>
namespace android {
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java
index e25d554..29e84c9 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java
@@ -50,6 +50,8 @@
0x00, 0x1a, 0x11, 0x22, 0x33, 0x33 };
private static final byte[] TEST_TARGET_MAC_ADDR = new byte[] {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+ private static final MacAddress TEST_DESTINATION_MAC = MacAddress.fromBytes(ETHER_BROADCAST);
+ private static final MacAddress TEST_SOURCE_MAC = MacAddress.fromBytes(TEST_SENDER_MAC_ADDR);
private static final byte[] TEST_ARP_PROBE = new byte[] {
// dst mac address
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
@@ -163,6 +165,8 @@
@Test
public void testParseArpProbePacket() throws Exception {
final ArpPacket packet = ArpPacket.parseArpPacket(TEST_ARP_PROBE, TEST_ARP_PROBE.length);
+ assertEquals(packet.destination, TEST_DESTINATION_MAC);
+ assertEquals(packet.source, TEST_SOURCE_MAC);
assertEquals(packet.opCode, ARP_REQUEST);
assertEquals(packet.senderHwAddress, MacAddress.fromBytes(TEST_SENDER_MAC_ADDR));
assertEquals(packet.targetHwAddress, MacAddress.fromBytes(TEST_TARGET_MAC_ADDR));
@@ -174,6 +178,8 @@
public void testParseArpAnnouncePacket() throws Exception {
final ArpPacket packet = ArpPacket.parseArpPacket(TEST_ARP_ANNOUNCE,
TEST_ARP_ANNOUNCE.length);
+ assertEquals(packet.destination, TEST_DESTINATION_MAC);
+ assertEquals(packet.source, TEST_SOURCE_MAC);
assertEquals(packet.opCode, ARP_REQUEST);
assertEquals(packet.senderHwAddress, MacAddress.fromBytes(TEST_SENDER_MAC_ADDR));
assertEquals(packet.targetHwAddress, MacAddress.fromBytes(TEST_TARGET_MAC_ADDR));
diff --git a/tests/unit/java/com/android/server/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
similarity index 90%
rename from tests/unit/java/com/android/server/HandlerUtilsTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
index 62bb651..f2c902f 100644
--- a/tests/unit/java/com/android/server/HandlerUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.server
+package com.android.net.module.util
import android.os.HandlerThread
-import com.android.server.connectivity.HandlerUtils
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.DevSdkIgnoreRunner.MonitorThreadLeak
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.junit.After
@@ -27,6 +27,8 @@
const val THREAD_BLOCK_TIMEOUT_MS = 1000L
const val TEST_REPEAT_COUNT = 100
+
+@MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
class HandlerUtilsTest {
val handlerThread = HandlerThread("HandlerUtilsTestHandlerThread").also {
@@ -39,7 +41,7 @@
// Repeat the test a fair amount of times to ensure that it does not pass by chance.
repeat(TEST_REPEAT_COUNT) {
var result = false
- HandlerUtils.runWithScissors(handler, {
+ HandlerUtils.runWithScissorsForDump(handler, {
assertEquals(Thread.currentThread(), handlerThread)
result = true
}, THREAD_BLOCK_TIMEOUT_MS)
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
index c5a91a4..d5b43fb 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
@@ -19,6 +19,7 @@
import android.Manifest.permission.NETWORK_STACK
import android.content.Context
import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
import android.content.pm.PackageManager.PERMISSION_DENIED
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
@@ -28,6 +29,7 @@
import com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf
import com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission
import com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr
+import com.android.net.module.util.PermissionUtils.enforcePackageNameMatchesUid
import com.android.net.module.util.PermissionUtils.enforceSystemFeature
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
@@ -42,7 +44,10 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
import org.mockito.Mockito.mock
/** Tests for PermissionUtils */
@@ -53,6 +58,9 @@
val ignoreRule = DevSdkIgnoreRule()
private val TEST_PERMISSION1 = "android.permission.TEST_PERMISSION1"
private val TEST_PERMISSION2 = "android.permission.TEST_PERMISSION2"
+ private val TEST_UID1 = 1234
+ private val TEST_UID2 = 1235
+ private val TEST_PACKAGE_NAME = "test.package"
private val mockContext = mock(Context::class.java)
private val mockPackageManager = mock(PackageManager::class.java)
@@ -61,6 +69,7 @@
@Before
fun setup() {
doReturn(mockPackageManager).`when`(mockContext).packageManager
+ doReturn(mockContext).`when`(mockContext).createContextAsUser(any(), anyInt())
}
@Test
@@ -141,4 +150,24 @@
Assert.fail("Exception should have not been thrown with system feature enabled")
}
}
+
+ @Test
+ fun testEnforcePackageNameMatchesUid() {
+ // Verify name not found throws.
+ doThrow(NameNotFoundException()).`when`(mockPackageManager)
+ .getPackageUid(eq(TEST_PACKAGE_NAME), anyInt())
+ assertFailsWith<SecurityException> {
+ enforcePackageNameMatchesUid(mockContext, TEST_UID1, TEST_PACKAGE_NAME)
+ }
+
+ // Verify uid mismatch throws.
+ doReturn(TEST_UID1).`when`(mockPackageManager)
+ .getPackageUid(eq(TEST_PACKAGE_NAME), anyInt())
+ assertFailsWith<SecurityException> {
+ enforcePackageNameMatchesUid(mockContext, TEST_UID2, TEST_PACKAGE_NAME)
+ }
+
+ // Verify uid match passes.
+ enforcePackageNameMatchesUid(mockContext, TEST_UID1, TEST_PACKAGE_NAME)
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
index 65e99f8..b44e428 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
@@ -32,6 +32,7 @@
import static org.junit.Assert.assertArrayEquals;
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 static org.junit.Assert.fail;
@@ -345,28 +346,28 @@
// Hexadecimal representation of InetDiagMessage
private static final String INET_DIAG_MSG_HEX1 =
// struct nlmsghdr
- "58000000" + // length = 88
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0200" + // flags = NLM_F_MULTI
- "00000000" + // seqno
- "f5220000" + // pid
+ "58000000" // length = 88
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
// struct inet_diag_msg
- "0a" + // family = AF_INET6
- "01" + // idiag_state = 1
- "02" + // idiag_timer = 2
- "ff" + // idiag_retrans = 255
+ + "0a" // family = AF_INET6
+ + "01" // idiag_state = 1
+ + "02" // idiag_timer = 2
+ + "ff" // idiag_retrans = 255
// inet_diag_sockid
- "a817" + // idiag_sport = 43031
- "960f" + // idiag_dport = 38415
- "20010db8000000000000000000000001" + // idiag_src = 2001:db8::1
- "20010db8000000000000000000000002" + // idiag_dst = 2001:db8::2
- "07000000" + // idiag_if = 7
- "5800000000000000" + // idiag_cookie = 88
- "04000000" + // idiag_expires = 4
- "05000000" + // idiag_rqueue = 5
- "06000000" + // idiag_wqueue = 6
- "a3270000" + // idiag_uid = 10147
- "a57e19f0"; // idiag_inode = 4028202661
+ + "a817" // idiag_sport = 43031
+ + "960f" // idiag_dport = 38415
+ + "20010db8000000000000000000000001" // idiag_src = 2001:db8::1
+ + "20010db8000000000000000000000002" // idiag_dst = 2001:db8::2
+ + "07000000" // idiag_if = 7
+ + "5800000000000000" // idiag_cookie = 88
+ + "04000000" // idiag_expires = 4
+ + "05000000" // idiag_rqueue = 5
+ + "06000000" // idiag_wqueue = 6
+ + "a3270000" // idiag_uid = 10147
+ + "a57e19f0"; // idiag_inode = 4028202661
private void assertInetDiagMsg1(final NetlinkMessage msg) {
assertNotNull(msg);
@@ -394,33 +395,45 @@
assertEquals(6, inetDiagMsg.inetDiagMsg.idiag_wqueue);
assertEquals(10147, inetDiagMsg.inetDiagMsg.idiag_uid);
assertEquals(4028202661L, inetDiagMsg.inetDiagMsg.idiag_inode);
+
+ // Verify the length of attribute list is 0 as expected since message doesn't
+ // take any attributes
+ assertEquals(0, inetDiagMsg.nlAttrs.size());
}
// Hexadecimal representation of InetDiagMessage
private static final String INET_DIAG_MSG_HEX2 =
// struct nlmsghdr
- "58000000" + // length = 88
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0200" + // flags = NLM_F_MULTI
- "00000000" + // seqno
- "f5220000" + // pid
+ "6C000000" // length = 108
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
// struct inet_diag_msg
- "0a" + // family = AF_INET6
- "02" + // idiag_state = 2
- "10" + // idiag_timer = 16
- "20" + // idiag_retrans = 32
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
// inet_diag_sockid
- "a845" + // idiag_sport = 43077
- "01bb" + // idiag_dport = 443
- "20010db8000000000000000000000003" + // idiag_src = 2001:db8::3
- "20010db8000000000000000000000004" + // idiag_dst = 2001:db8::4
- "08000000" + // idiag_if = 8
- "6300000000000000" + // idiag_cookie = 99
- "30000000" + // idiag_expires = 48
- "40000000" + // idiag_rqueue = 64
- "50000000" + // idiag_wqueue = 80
- "39300000" + // idiag_uid = 12345
- "851a0000"; // idiag_inode = 6789
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000003" // idiag_src = 2001:db8::3
+ + "20010db8000000000000000000000004" // idiag_dst = 2001:db8::4
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "0400" // len = 4
+ + "0200"; // type = 2
private void assertInetDiagMsg2(final NetlinkMessage msg) {
assertNotNull(msg);
@@ -448,6 +461,104 @@
assertEquals(80, inetDiagMsg.inetDiagMsg.idiag_wqueue);
assertEquals(12345, inetDiagMsg.inetDiagMsg.idiag_uid);
assertEquals(6789, inetDiagMsg.inetDiagMsg.idiag_inode);
+
+ // Verify the number of nlAttr and their content.
+ assertEquals(3, inetDiagMsg.nlAttrs.size());
+
+ assertEquals(5, inetDiagMsg.nlAttrs.get(0).nla_len);
+ assertEquals(8, inetDiagMsg.nlAttrs.get(0).nla_type);
+ assertArrayEquals(
+ HexEncoding.decode("00".toCharArray(), false),
+ inetDiagMsg.nlAttrs.get(0).nla_value);
+ assertEquals(8, inetDiagMsg.nlAttrs.get(1).nla_len);
+ assertEquals(15, inetDiagMsg.nlAttrs.get(1).nla_type);
+ assertArrayEquals(
+ HexEncoding.decode("850A0C00".toCharArray(), false),
+ inetDiagMsg.nlAttrs.get(1).nla_value);
+ assertEquals(4, inetDiagMsg.nlAttrs.get(2).nla_len);
+ assertEquals(2, inetDiagMsg.nlAttrs.get(2).nla_type);
+ assertNull(inetDiagMsg.nlAttrs.get(2).nla_value);
+ }
+
+ // Hexadecimal representation of InetDiagMessage
+ private static final String INET_DIAG_MSG_HEX_MALFORMED =
+ // struct nlmsghdr
+ "6E000000" // length = 110
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
+ // struct inet_diag_msg
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
+ // inet_diag_sockid
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000005" // idiag_src = 2001:db8::5
+ + "20010db8000000000000000000000006" // idiag_dst = 2001:db8::6
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "0400" // len = 4
+ + "0200" // type = 2
+ + "0100" // len = 1, malformed value
+ + "0100"; // type = 1
+
+ @Test
+ public void testParseInetDiagResponseMalformedNlAttr() throws Exception {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(
+ HexEncoding.decode((INET_DIAG_MSG_HEX_MALFORMED).toCharArray(), false));
+ byteBuffer.order(ByteOrder.nativeOrder());
+ assertNull(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+ }
+
+ // Hexadecimal representation of InetDiagMessage
+ private static final String INET_DIAG_MSG_HEX_TRUNCATED =
+ // struct nlmsghdr
+ "5E000000" // length = 96
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
+ // struct inet_diag_msg
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
+ // inet_diag_sockid
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000005" // idiag_src = 2001:db8::5
+ + "20010db8000000000000000000000006" // idiag_dst = 2001:db8::6
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0800" // len = 8
+ + "0100" // type = 1
+ + "000000"; // data, less than the expected length
+
+ @Test
+ public void testParseInetDiagResponseTruncatedNlAttr() throws Exception {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(
+ HexEncoding.decode((INET_DIAG_MSG_HEX_TRUNCATED).toCharArray(), false));
+ byteBuffer.order(ByteOrder.nativeOrder());
+ assertNull(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
}
private static final byte[] INET_DIAG_MSG_BYTES =
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index 17d4e81..0958f11 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -21,7 +21,7 @@
import static android.system.OsConstants.AF_UNSPEC;
import static android.system.OsConstants.EACCES;
import static android.system.OsConstants.NETLINK_ROUTE;
-
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
@@ -191,4 +191,17 @@
return bytes;
}
+
+ @Test
+ public void testGetIpv6MulticastRoutes_doesNotThrow() {
+ var multicastRoutes = NetlinkUtils.getIpv6MulticastRoutes();
+
+ for (var route : multicastRoutes) {
+ assertNotNull(route);
+ assertEquals("Route is not IP6MR: " + route,
+ RTNL_FAMILY_IP6MR, route.getRtmFamily());
+ assertNotNull("Route doesn't contain source: " + route, route.getSource());
+ assertNotNull("Route doesn't contain destination: " + route, route.getDestination());
+ }
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java
new file mode 100644
index 0000000..a83fc36
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java
@@ -0,0 +1,102 @@
+package com.android.net.module.util.structs;
+
+import static android.system.OsConstants.AF_INET6;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.util.ArraySet;
+import androidx.test.runner.AndroidJUnit4;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMf6cctlTest {
+ private static final byte[] MSG_BYTES = new byte[] {
+ 10, 0, /* AF_INET6 */
+ 0, 0, /* originPort */
+ 0, 0, 0, 0, /* originFlowinfo */
+ 32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* originAddress */
+ 0, 0, 0, 0, /* originScopeId */
+ 10, 0, /* AF_INET6 */
+ 0, 0, /* groupPort */
+ 0, 0, 0, 0, /* groupFlowinfo*/
+ -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, /*groupAddress*/
+ 0, 0, 0, 0, /* groupScopeId*/
+ 1, 0, /* mf6ccParent */
+ 0, 0, /* padding */
+ 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 /* mf6ccIfset */
+ };
+
+ private static final int OIF = 10;
+ private static final byte[] OIFSET_BYTES = new byte[] {
+ 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ };
+
+ private static final Inet6Address SOURCE =
+ (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+ private static final Inet6Address DESTINATION =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+
+ @Test
+ public void testConstructor() {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(OIF);
+
+ StructMf6cctl mf6cctl = new StructMf6cctl(SOURCE, DESTINATION,
+ 1 /* mf6ccParent */, oifset);
+
+ assertTrue(Arrays.equals(SOURCE.getAddress(), mf6cctl.originAddress));
+ assertTrue(Arrays.equals(DESTINATION.getAddress(), mf6cctl.groupAddress));
+ assertEquals(1, mf6cctl.mf6ccParent);
+ assertArrayEquals(OIFSET_BYTES, mf6cctl.mf6ccIfset);
+ }
+
+ @Test
+ public void testConstructor_tooBigOifIndex_throwsIllegalArgumentException()
+ throws UnknownHostException {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(1000);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> new StructMf6cctl(SOURCE, DESTINATION, 1, oifset));
+ }
+
+ @Test
+ public void testParseMf6cctl() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ buf.order(ByteOrder.nativeOrder());
+ StructMf6cctl mf6cctl = StructMf6cctl.parse(StructMf6cctl.class, buf);
+
+ assertEquals(AF_INET6, mf6cctl.originFamily);
+ assertEquals(AF_INET6, mf6cctl.groupFamily);
+ assertArrayEquals(SOURCE.getAddress(), mf6cctl.originAddress);
+ assertArrayEquals(DESTINATION.getAddress(), mf6cctl.groupAddress);
+ assertEquals(1, mf6cctl.mf6ccParent);
+ assertArrayEquals("mf6ccIfset = " + Arrays.toString(mf6cctl.mf6ccIfset),
+ OIFSET_BYTES, mf6cctl.mf6ccIfset);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(OIF);
+
+ StructMf6cctl mf6cctl = new StructMf6cctl(SOURCE, DESTINATION,
+ 1 /* mf6ccParent */, oifset);
+ byte[] bytes = mf6cctl.writeToBytes();
+
+ assertArrayEquals("bytes = " + Arrays.toString(bytes), MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java
new file mode 100644
index 0000000..75196e4
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java
@@ -0,0 +1,70 @@
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.util.ArraySet;
+import androidx.test.runner.AndroidJUnit4;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMif6ctlTest {
+ private static final byte[] MSG_BYTES = new byte[] {
+ 1, 0, /* mif6cMifi */
+ 0, /* mif6cFlags */
+ 1, /* vifcThreshold*/
+ 20, 0, /* mif6cPifi */
+ 0, 0, 0, 0, /* vifcRateLimit */
+ 0, 0 /* padding */
+ };
+
+ @Test
+ public void testConstructor() {
+ StructMif6ctl mif6ctl = new StructMif6ctl(10 /* mif6cMifi */,
+ (short) 11 /* mif6cFlags */,
+ (short) 12 /* vifcThreshold */,
+ 13 /* mif6cPifi */,
+ 14L /* vifcRateLimit */);
+
+ assertEquals(10, mif6ctl.mif6cMifi);
+ assertEquals(11, mif6ctl.mif6cFlags);
+ assertEquals(12, mif6ctl.vifcThreshold);
+ assertEquals(13, mif6ctl.mif6cPifi);
+ assertEquals(14, mif6ctl.vifcRateLimit);
+ }
+
+ @Test
+ public void testParseMif6ctl() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ buf.order(ByteOrder.nativeOrder());
+ StructMif6ctl mif6ctl = StructMif6ctl.parse(StructMif6ctl.class, buf);
+
+ assertEquals(1, mif6ctl.mif6cMifi);
+ assertEquals(0, mif6ctl.mif6cFlags);
+ assertEquals(1, mif6ctl.vifcThreshold);
+ assertEquals(20, mif6ctl.mif6cPifi);
+ assertEquals(0, mif6ctl.vifcRateLimit);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ StructMif6ctl mif6ctl = new StructMif6ctl(1 /* mif6cMifi */,
+ (short) 0 /* mif6cFlags */,
+ (short) 1 /* vifcThreshold */,
+ 20 /* mif6cPifi */,
+ (long) 0 /* vifcRateLimit */);
+
+ byte[] bytes = mif6ctl.writeToBytes();
+
+ assertArrayEquals("bytes = " + Arrays.toString(bytes), MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java
new file mode 100644
index 0000000..f1b75a0
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java
@@ -0,0 +1,58 @@
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMrt6MsgTest {
+
+ private static final byte[] MSG_BYTES = new byte[] {
+ 0, /* mbz = 0 */
+ 1, /* message type = MRT6MSG_NOCACHE */
+ 1, 0, /* mif u16 = 1 */
+ 0, 0, 0, 0, /* padding */
+ 32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* source=2001:db8::1 */
+ -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, /* destination=ff05::1234 */
+ };
+
+ private static final Inet6Address SOURCE =
+ (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+ private static final Inet6Address GROUP =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+
+ @Test
+ public void testParseMrt6Msg() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ StructMrt6Msg mrt6Msg = StructMrt6Msg.parse(buf);
+
+ assertEquals(1, mrt6Msg.mif);
+ assertEquals(StructMrt6Msg.MRT6MSG_NOCACHE, mrt6Msg.msgType);
+ assertEquals(SOURCE, mrt6Msg.src);
+ assertEquals(GROUP, mrt6Msg.dst);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ StructMrt6Msg msg = new StructMrt6Msg((byte) 0 /* mbz must be 0 */,
+ StructMrt6Msg.MRT6MSG_NOCACHE,
+ 1 /* mif */,
+ SOURCE,
+ GROUP);
+ byte[] bytes = msg.writeToBytes();
+
+ assertArrayEquals(MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index a5c4fea..43853ee 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -91,6 +91,8 @@
"cts",
"mts-networking",
"mcts-networking",
+ "mts-tethering",
+ "mcts-tethering",
],
data: [":ConnectivityTestPreparer"],
}
diff --git a/staticlibs/testutils/devicetests/NSResponder.kt b/staticlibs/testutils/devicetests/NSResponder.kt
new file mode 100644
index 0000000..f7619cd
--- /dev/null
+++ b/staticlibs/testutils/devicetests/NSResponder.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.testutils
+
+import android.net.MacAddress
+import android.util.Log
+import com.android.net.module.util.Ipv6Utils
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA
+import com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
+import com.android.net.module.util.Struct
+import com.android.net.module.util.structs.Icmpv6Header
+import com.android.net.module.util.structs.Ipv6Header
+import com.android.net.module.util.structs.LlaOption
+import com.android.net.module.util.structs.NsHeader
+import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
+import java.lang.IllegalArgumentException
+import java.net.Inet6Address
+import java.nio.ByteBuffer
+
+private const val NS_TYPE = 135.toShort()
+
+/**
+ * A class that can be used to reply to Neighbor Solicitation packets on a [TapPacketReader].
+ */
+class NSResponder(
+ reader: TapPacketReader,
+ table: Map<Inet6Address, MacAddress>,
+ name: String = NSResponder::class.java.simpleName
+) : PacketResponder(reader, Icmpv6Filter(), name) {
+ companion object {
+ private val TAG = NSResponder::class.simpleName
+ }
+
+ // Copy the map if not already immutable (toMap) to make sure it is not modified
+ private val table = table.toMap()
+
+ override fun replyToPacket(packet: ByteArray, reader: TapPacketReader) {
+ if (packet.size < IPV6_HEADER_LENGTH) {
+ return
+ }
+ val buf = ByteBuffer.wrap(packet, ETHER_HEADER_LEN, packet.size - ETHER_HEADER_LEN)
+ val ipv6Header = parseOrLog(Ipv6Header::class.java, buf) ?: return
+ val icmpHeader = parseOrLog(Icmpv6Header::class.java, buf) ?: return
+ if (icmpHeader.type != NS_TYPE) {
+ return
+ }
+ val ns = parseOrLog(NsHeader::class.java, buf) ?: return
+ val replyMacAddr = table[ns.target] ?: return
+ val slla = parseOrLog(LlaOption::class.java, buf) ?: return
+ val requesterMac = slla.linkLayerAddress
+
+ val tlla = LlaOption.build(ICMPV6_ND_OPTION_TLLA.toByte(), replyMacAddr)
+ reader.sendResponse(Ipv6Utils.buildNaPacket(
+ replyMacAddr /* srcMac */,
+ requesterMac /* dstMac */,
+ ns.target /* srcIp */,
+ ipv6Header.srcIp /* dstIp */,
+ NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED,
+ ns.target,
+ tlla))
+ }
+
+ private fun <T> parseOrLog(clazz: Class<T>, buf: ByteBuffer): T? where T : Struct {
+ return try {
+ Struct.parse(clazz, buf)
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "Invalid ${clazz.simpleName} in ICMPv6 packet", e)
+ null
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
index 3d98cc3..68248ca 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
@@ -22,12 +22,12 @@
import android.util.Log
import com.android.modules.utils.build.SdkLevel
import com.android.testutils.FunctionalUtils.ThrowingRunnable
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
private val TAG = DeviceConfigRule::class.simpleName
@@ -147,11 +147,11 @@
return tryTest {
runAsShell(*readWritePermissions) {
DeviceConfig.addOnPropertiesChangedListener(
- DeviceConfig.NAMESPACE_CONNECTIVITY,
+ namespace,
inlineExecutor,
listener)
DeviceConfig.setProperty(
- DeviceConfig.NAMESPACE_CONNECTIVITY,
+ namespace,
key,
value,
false /* makeDefault */)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
deleted file mode 100644
index d7961a0..0000000
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import java.io.FileDescriptor
-import java.net.InetAddress
-
-/**
- * A class that forwards packets from the external {@link TestNetworkInterface} to the internal
- * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
- */
-class NatExternalPacketForwarder(
- srcFd: FileDescriptor,
- mtu: Int,
- dstFd: FileDescriptor,
- extAddr: InetAddress,
- natMap: PacketBridge.NatMap
-) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
-
- /**
- * Rewrite addresses, ports and fix up checksums for packets received on the external
- * interface.
- *
- * Incoming response from external interface which is being forwarded to the internal
- * interface with translated address, e.g. 1.2.3.4:80 -> 8.8.8.8:1234
- * will be translated into 8.8.8.8:80 -> 192.168.1.1:5678.
- *
- * For packets that are not an incoming response, do not forward them to the
- * internal interface.
- */
- override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
- val (addrPos, addrLen) = getAddressPositionAndLength(version)
-
- // TODO: support one external address per ip version.
- val extAddrBuf = mExtAddr.address
- if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
-
- // Get internal address by port.
- val transportOffset =
- if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
- else PacketReflector.IPV6_HEADER_LENGTH
- val dstPort = getPortAt(buf, transportOffset + DESTINATION_PORT_OFFSET)
- val intAddrInfo = synchronized(mNatMap) { mNatMap.fromExternalPort(dstPort) }
- // No mapping, skip. This usually happens if the connection is initiated directly on
- // the external interface, e.g. DNS64 resolution, network validation, etc.
- if (intAddrInfo == null) return
-
- val intAddrBuf = intAddrInfo.address.address
- val intPort = intAddrInfo.port
-
- // Copy the original destination to into the source address.
- for (i in 0 until addrLen) {
- buf[addrPos + i] = buf[addrPos + addrLen + i]
- }
-
- // Copy the internal address into the destination address.
- for (i in 0 until addrLen) {
- buf[addrPos + addrLen + i] = intAddrBuf[i]
- }
-
- // Copy the internal port into the destination port.
- setPortAt(intPort, buf, transportOffset + DESTINATION_PORT_OFFSET)
-
- // Fix IP and Transport layer checksum.
- fixPacketChecksum(buf, len, version, proto.toByte())
- }
-}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
deleted file mode 100644
index fa39d19..0000000
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import java.io.FileDescriptor
-import java.net.InetAddress
-
-/**
- * A class that forwards packets from the internal {@link TestNetworkInterface} to the external
- * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
- */
-class NatInternalPacketForwarder(
- srcFd: FileDescriptor,
- mtu: Int,
- dstFd: FileDescriptor,
- extAddr: InetAddress,
- natMap: PacketBridge.NatMap
-) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
-
- /**
- * Rewrite addresses, ports and fix up checksums for packets received on the internal
- * interface.
- *
- * Outgoing packet from the internal interface which is being forwarded to the
- * external interface with translated address, e.g. 192.168.1.1:5678 -> 8.8.8.8:80
- * will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
- *
- * The external port, e.g. 1234 in the above example, is the port number assigned by
- * the forwarder when creating the mapping to identify the source address and port when
- * the response is coming from the external interface. See {@link PacketBridge.NatMap}
- * for detail.
- */
- override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
- val (addrPos, addrLen) = getAddressPositionAndLength(version)
-
- // TODO: support one external address per ip version.
- val extAddrBuf = mExtAddr.address
- if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
-
- val srcAddr = getInetAddressAt(buf, addrPos, addrLen)
-
- // Copy the original destination to into the source address.
- for (i in 0 until addrLen) {
- buf[addrPos + i] = buf[addrPos + addrLen + i]
- }
-
- // Copy the external address into the destination address.
- for (i in 0 until addrLen) {
- buf[addrPos + addrLen + i] = extAddrBuf[i]
- }
-
- // Add an entry to NAT mapping table.
- val transportOffset =
- if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
- else PacketReflector.IPV6_HEADER_LENGTH
- val srcPort = getPortAt(buf, transportOffset)
- val extPort = synchronized(mNatMap) { mNatMap.toExternalPort(srcAddr, srcPort, proto) }
- // Copy the external port to into the source port.
- setPortAt(extPort, buf, transportOffset)
-
- // Fix IP and Transport layer checksum.
- fixPacketChecksum(buf, len, version, proto.toByte())
- }
-}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
index d50f78a..1a2cc88 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -16,6 +16,7 @@
package com.android.testutils
+import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import android.net.LinkAddress
@@ -31,29 +32,26 @@
import java.net.InetAddress
import libcore.io.IoUtils
-private const val MIN_PORT_NUMBER = 1025
-private const val MAX_PORT_NUMBER = 65535
-
/**
- * A class that set up two {@link TestNetworkInterface} with NAT, and forward packets between them.
+ * A class that set up two {@link TestNetworkInterface}, and forward packets between them.
*
- * See {@link NatPacketForwarder} for more detailed information.
+ * See {@link PacketForwarder} for more detailed information.
*/
class PacketBridge(
context: Context,
- internalAddr: LinkAddress,
- externalAddr: LinkAddress,
+ addresses: List<LinkAddress>,
dnsAddr: InetAddress
) {
- private val natMap = NatMap()
private val binder = Binder()
private val cm = context.getSystemService(ConnectivityManager::class.java)!!
private val tnm = context.getSystemService(TestNetworkManager::class.java)!!
- // Create test networks.
- private val internalIface = tnm.createTunInterface(listOf(internalAddr))
- private val externalIface = tnm.createTunInterface(listOf(externalAddr))
+ // Create test networks. The needed permissions should be supplied by the callers.
+ @SuppressLint("MissingPermission")
+ private val internalIface = tnm.createTunInterface(addresses)
+ @SuppressLint("MissingPermission")
+ private val externalIface = tnm.createTunInterface(addresses)
// Register test networks to ConnectivityService.
private val internalNetworkCallback: TestableNetworkCallback
@@ -61,32 +59,20 @@
val internalNetwork: Network
val externalNetwork: Network
init {
- val (inCb, inNet) = createTestNetwork(internalIface, internalAddr, dnsAddr)
- val (exCb, exNet) = createTestNetwork(externalIface, externalAddr, dnsAddr)
+ val (inCb, inNet) = createTestNetwork(internalIface, addresses, dnsAddr)
+ val (exCb, exNet) = createTestNetwork(externalIface, addresses, dnsAddr)
internalNetworkCallback = inCb
externalNetworkCallback = exCb
internalNetwork = inNet
externalNetwork = exNet
}
- // Setup the packet bridge.
+ // Set up the packet bridge.
private val internalFd = internalIface.fileDescriptor.fileDescriptor
private val externalFd = externalIface.fileDescriptor.fileDescriptor
- private val pr1 = NatInternalPacketForwarder(
- internalFd,
- 1500,
- externalFd,
- externalAddr.address,
- natMap
- )
- private val pr2 = NatExternalPacketForwarder(
- externalFd,
- 1500,
- internalFd,
- externalAddr.address,
- natMap
- )
+ private val pr1 = PacketForwarder(internalFd, 1500, externalFd)
+ private val pr2 = PacketForwarder(externalFd, 1500, internalFd)
fun start() {
IoUtils.setBlocking(internalFd, true /* blocking */)
@@ -107,7 +93,7 @@
*/
private fun createTestNetwork(
testIface: TestNetworkInterface,
- addr: LinkAddress,
+ addresses: List<LinkAddress>,
dnsAddr: InetAddress
): Pair<TestableNetworkCallback, Network> {
// Make a network request to hold the test network
@@ -120,7 +106,7 @@
cm.requestNetwork(nr, testCb)
val lp = LinkProperties().apply {
- addLinkAddress(addr)
+ setLinkAddresses(addresses)
interfaceName = testIface.interfaceName
addDnsServer(dnsAddr)
}
@@ -130,44 +116,4 @@
val network = testCb.expect<Available>().network
return testCb to network
}
-
- /**
- * A helper class to maintain the mappings between internal addresses/ports and external
- * ports.
- *
- * This class assigns an unused external port number if the mapping between
- * srcaddress:srcport:protocol and the external port does not exist yet.
- *
- * Note that this class is not thread-safe. The instance of the class needs to be
- * synchronized in the callers when being used in multiple threads.
- */
- class NatMap {
- data class AddressInfo(val address: InetAddress, val port: Int, val protocol: Int)
-
- private val mToExternalPort = HashMap<AddressInfo, Int>()
- private val mFromExternalPort = HashMap<Int, AddressInfo>()
-
- // Skip well-known port 0~1024.
- private var nextExternalPort = MIN_PORT_NUMBER
-
- fun toExternalPort(addr: InetAddress, port: Int, protocol: Int): Int {
- val info = AddressInfo(addr, port, protocol)
- val extPort: Int
- if (!mToExternalPort.containsKey(info)) {
- extPort = nextExternalPort++
- if (nextExternalPort > MAX_PORT_NUMBER) {
- throw IllegalStateException("Available ports are exhausted")
- }
- mToExternalPort[info] = extPort
- mFromExternalPort[extPort] = info
- } else {
- extPort = mToExternalPort[info]!!
- }
- return extPort
- }
-
- fun fromExternalPort(port: Int): AddressInfo? {
- return mFromExternalPort[port]
- }
- }
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java
similarity index 62%
rename from staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
rename to staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java
index 0a2b5d4..d8efb7d 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java
@@ -30,16 +30,13 @@
import android.system.Os;
import android.util.Log;
-import androidx.annotation.GuardedBy;
-
import java.io.FileDescriptor;
import java.io.IOException;
-import java.net.InetAddress;
import java.util.Objects;
/**
* A class that forwards packets from a {@link TestNetworkInterface} to another
- * {@link TestNetworkInterface} with NAT.
+ * {@link TestNetworkInterface}.
*
* For testing purposes, a {@link TestNetworkInterface} provides a {@link FileDescriptor}
* which allows content injection on the test network. However, this could be hard to use
@@ -54,30 +51,14 @@
*
* To make it work, an internal interface and an external interface are defined, where
* the client might send packets from the internal interface which are originated from
- * multiple addresses to a server that listens on the external address.
- *
- * When forwarding the outgoing packet on the internal interface, a simple NAT mechanism
- * is implemented during forwarding, which will swap the source and destination,
- * but replacing the source address with the external address,
- * e.g. 192.168.1.1:1234 -> 8.8.8.8:80 will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
- *
- * For the above example, a client who sends http request will have a hallucination that
- * it is talking to a remote server at 8.8.8.8. Also, the server listens on 1.2.3.4 will
- * have a different hallucination that the request is sent from a remote client at 8.8.8.8,
- * to a local address 1.2.3.4.
- *
- * And a NAT mapping is created at the time when the outgoing packet is forwarded.
- * With a different internal source port, the instance learned that when a response with the
- * destination port 1234, it should forward the packet to the internal address 192.168.1.1.
+ * multiple addresses to a server that listens on the different port.
*
* For the incoming packet received from external interface, for example a http response sent
* from the http server, the same mechanism is applied but in a different direction,
- * where the source and destination will be swapped, and the source address will be replaced
- * with the internal address, which is obtained from the NAT mapping described above.
+ * where the source and destination will be swapped.
*/
-public abstract class NatPacketForwarderBase extends Thread {
- private static final String TAG = "NatPacketForwarder";
- static final int DESTINATION_PORT_OFFSET = 2;
+public class PacketForwarder extends Thread {
+ private static final String TAG = "PacketForwarder";
// The source fd to read packets from.
@NonNull
@@ -88,27 +69,12 @@
// The destination fd to write packets to.
@NonNull
final FileDescriptor mDstFd;
- // The NAT mapping table shared between two NatPacketForwarder instances to map from
- // the source port to the associated internal address. The map can be read/write from two
- // different threads on any given time whenever receiving packets on the
- // {@link TestNetworkInterface}. Thus, synchronize on the object when reading/writing is needed.
- @GuardedBy("mNatMap")
- @NonNull
- final PacketBridge.NatMap mNatMap;
- // The address of the external interface. See {@link NatPacketForwarder}.
- @NonNull
- final InetAddress mExtAddr;
/**
- * Construct a {@link NatPacketForwarderBase}.
+ * Construct a {@link PacketForwarder}.
*
* This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
- * forwards them to the {@code dstFd} of another {@link TestNetworkInterface} with
- * NAT applied. See {@link NatPacketForwarderBase}.
- *
- * To apply NAT, the address of the external interface needs to be supplied through
- * {@code extAddr} to identify the external interface. And a shared NAT mapping table,
- * {@code natMap} is needed to be shared between these two instances.
+ * forwards them to the {@code dstFd} of another {@link TestNetworkInterface}.
*
* Note that this class is not useful if the instance is not managed by a
* {@link PacketBridge} to set up a two-way communication.
@@ -116,29 +82,15 @@
* @param srcFd {@link FileDescriptor} to read packets from.
* @param mtu MTU of the test network.
* @param dstFd {@link FileDescriptor} to write packets to.
- * @param extAddr the external address, which is the address of the external interface.
- * See {@link NatPacketForwarderBase}.
- * @param natMap the NAT mapping table shared between two {@link NatPacketForwarderBase}
- * instance.
*/
- public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
- @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
- @NonNull PacketBridge.NatMap natMap) {
+ public PacketForwarder(@NonNull FileDescriptor srcFd, int mtu,
+ @NonNull FileDescriptor dstFd) {
super(TAG);
mSrcFd = Objects.requireNonNull(srcFd);
mBuf = new byte[mtu];
mDstFd = Objects.requireNonNull(dstFd);
- mExtAddr = Objects.requireNonNull(extAddr);
- mNatMap = Objects.requireNonNull(natMap);
}
- /**
- * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
- * which includes re-write addresses, ports and fix up checksums.
- * Subclasses should override this method to implement a simple NAT.
- */
- abstract void preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);
-
private void forwardPacket(@NonNull byte[] buf, int len) {
try {
Os.write(mDstFd, buf, 0, len);
@@ -190,8 +142,9 @@
if (len < ipHdrLen + transportHdrLen) {
throw new IllegalStateException("Unexpected buffer length: " + len);
}
- // Re-write addresses, ports and fix up checksums.
- preparePacketForForwarding(mBuf, len, version, proto);
+ // Swap addresses.
+ PacketReflectorUtil.swapAddresses(mBuf, version);
+
// Send the packet to the destination fd.
forwardPacket(mBuf, len);
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
index 69392d4..ce20d67 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
@@ -87,31 +87,6 @@
mBuf = new byte[mtu];
}
- private static void swapBytes(@NonNull byte[] buf, int pos1, int pos2, int len) {
- for (int i = 0; i < len; i++) {
- byte b = buf[pos1 + i];
- buf[pos1 + i] = buf[pos2 + i];
- buf[pos2 + i] = b;
- }
- }
-
- private static void swapAddresses(@NonNull byte[] buf, int version) {
- int addrPos, addrLen;
- switch (version) {
- case 4:
- addrPos = IPV4_ADDR_OFFSET;
- addrLen = IPV4_ADDR_LENGTH;
- break;
- case 6:
- addrPos = IPV6_ADDR_OFFSET;
- addrLen = IPV6_ADDR_LENGTH;
- break;
- default:
- throw new IllegalArgumentException();
- }
- swapBytes(buf, addrPos, addrPos + addrLen, addrLen);
- }
-
// Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
// This is used by the test to "connect to itself" through the VPN.
private void processTcpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
@@ -120,7 +95,7 @@
}
// Swap src and dst IP addresses.
- swapAddresses(buf, version);
+ PacketReflectorUtil.swapAddresses(buf, version);
// Send the packet back.
writePacket(buf, len);
@@ -134,11 +109,11 @@
}
// Swap src and dst IP addresses.
- swapAddresses(buf, version);
+ PacketReflectorUtil.swapAddresses(buf, version);
// Swap dst and src ports.
int portOffset = hdrLen;
- swapBytes(buf, portOffset, portOffset + 2, 2);
+ PacketReflectorUtil.swapBytes(buf, portOffset, portOffset + 2, 2);
// Send the packet back.
writePacket(buf, len);
@@ -160,7 +135,7 @@
// Swap src and dst IP addresses, and send the packet back.
// This effectively pings the device to see if it replies.
- swapAddresses(buf, version);
+ PacketReflectorUtil.swapAddresses(buf, version);
writePacket(buf, len);
// The device should have replied, and buf should now contain a ping response.
@@ -202,7 +177,7 @@
}
// Now swap the addresses again and reflect the packet. This sends a ping reply.
- swapAddresses(buf, version);
+ PacketReflectorUtil.swapAddresses(buf, version);
writePacket(buf, len);
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
index 498b1a3..ad259c5 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
@@ -112,3 +112,28 @@
else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
}
}
+
+fun swapBytes(buf: ByteArray, pos1: Int, pos2: Int, len: Int) {
+ for (i in 0 until len) {
+ val b = buf[pos1 + i]
+ buf[pos1 + i] = buf[pos2 + i]
+ buf[pos2 + i] = b
+ }
+}
+
+fun swapAddresses(buf: ByteArray, version: Int) {
+ val addrPos: Int
+ val addrLen: Int
+ when (version) {
+ 4 -> {
+ addrPos = PacketReflector.IPV4_ADDR_OFFSET
+ addrLen = PacketReflector.IPV4_ADDR_LENGTH
+ }
+ 6 -> {
+ addrPos = PacketReflector.IPV6_ADDR_OFFSET
+ addrLen = PacketReflector.IPV6_ADDR_LENGTH
+ }
+ else -> throw java.lang.IllegalArgumentException()
+ }
+ swapBytes(buf, addrPos, addrPos + addrLen, addrLen)
+}
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
index 1bb6d68..a73a58a 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
@@ -110,6 +110,12 @@
override fun test(t: ByteArray) = impl.test(t)
}
+class Icmpv6Filter : Predicate<ByteArray> {
+ private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x86.toByte(), 0xdd.toByte() /* IPv6 */).and(
+ OffsetFilter(IPV6_PROTOCOL_OFFSET, 58 /* ICMPv6 */))
+ override fun test(t: ByteArray) = impl.test(t)
+}
+
/**
* A [Predicate] that matches ethernet-encapped DHCP packets sent from a DHCP client.
*/
diff --git a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
index 585157f..57602f1 100644
--- a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
+++ b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
@@ -133,7 +133,16 @@
}
@Test
- fun testReadFromRecorder_manyUids() {
+ fun testReadFromRecorder_manyUids_useDataInput() {
+ doTestReadFromRecorder_manyUids(useFastDataInput = false)
+ }
+
+ @Test
+ fun testReadFromRecorder_manyUids_useFastDataInput() {
+ doTestReadFromRecorder_manyUids(useFastDataInput = true)
+ }
+
+ fun doTestReadFromRecorder_manyUids(useFastDataInput: Boolean) {
val mockObserver = mock<NonMonotonicObserver<String>>()
val mockDropBox = mock<DropBoxManager>()
testFilesAssets.forEach {
@@ -146,7 +155,9 @@
PREFIX_UID,
UID_COLLECTION_BUCKET_DURATION_MS,
false /* includeTags */,
- false /* wipeOnError */
+ false /* wipeOnError */,
+ useFastDataInput /* useFastDataInput */,
+ it
)
recorder.orLoadCompleteLocked
}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index e55ba63..a0aafc6 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -43,6 +43,8 @@
test_suites: [
"cts",
"general-tests",
+ "mcts-tethering",
+ "mts-tethering",
"sts"
],
data: [
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
index 04d054d..0d7365f 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
@@ -59,13 +59,13 @@
setBatterySaverMode(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
setBatterySaverMode(true);
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
// Although it should not have access while the screen is off.
turnScreenOff();
assertBackgroundNetworkAccess(false);
turnScreenOn();
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
// Goes back to background state.
finishActivity();
@@ -75,7 +75,7 @@
setBatterySaverMode(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
setBatterySaverMode(true);
- assertForegroundNetworkAccess();
+ assertForegroundServiceNetworkAccess();
stopForegroundService();
assertBackgroundNetworkAccess(false);
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
index e0ce4ea..b037953 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
@@ -16,6 +16,8 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+
import static com.android.cts.net.hostside.Property.DOZE_MODE;
import static com.android.cts.net.hostside.Property.NOT_LOW_RAM_DEVICE;
@@ -62,9 +64,9 @@
setDozeMode(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
setDozeMode(true);
- assertForegroundNetworkAccess();
+ assertForegroundServiceNetworkAccess();
stopForegroundService();
- assertBackgroundState();
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
assertBackgroundNetworkAccess(false);
}
@@ -136,6 +138,6 @@
protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception {
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
stopForegroundService();
- assertBackgroundState();
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 198b009..29aac3c 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -16,6 +16,9 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP;
import static android.app.job.JobScheduler.RESULT_SUCCESS;
import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
import static android.os.BatteryManager.BATTERY_PLUGGED_ANY;
@@ -38,7 +41,6 @@
import static org.junit.Assert.fail;
import android.annotation.NonNull;
-import android.app.ActivityManager;
import android.app.Instrumentation;
import android.app.NotificationManager;
import android.app.job.JobInfo;
@@ -67,6 +69,7 @@
import com.android.compatibility.common.util.AmUtils;
import com.android.compatibility.common.util.BatteryUtils;
import com.android.compatibility.common.util.DeviceConfigStateHelper;
+import com.android.compatibility.common.util.ThrowingRunnable;
import org.junit.Rule;
import org.junit.rules.RuleChain;
@@ -76,6 +79,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
/**
* Superclass for tests related to background network restrictions.
@@ -126,8 +130,6 @@
private static final int SECOND_IN_MS = 1000;
static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
- private static int PROCESS_STATE_FOREGROUND_SERVICE;
-
private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
private static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
@@ -171,9 +173,6 @@
.around(new MeterednessConfigurationRule());
protected void setUp() throws Exception {
- // TODO: Annotate these constants with @TestApi instead of obtaining them using reflection
- PROCESS_STATE_FOREGROUND_SERVICE = (Integer) ActivityManager.class
- .getDeclaredField("PROCESS_STATE_FOREGROUND_SERVICE").get(null);
mInstrumentation = getInstrumentation();
mContext = getContext();
mCm = getConnectivityManager();
@@ -284,44 +283,20 @@
restrictBackgroundValueToString(Integer.parseInt(status)));
}
- protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
- assertBackgroundNetworkAccess(expectAllowed, null);
- }
-
/**
- * Asserts whether the active network is available or not for the background app. If the network
- * is unavailable, also checks whether it is blocked by the expected error.
- *
- * @param expectAllowed expect background network access to be allowed or not.
- * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
- * meaningful only when the {@code expectAllowed} is 'false'.
- * Throws an IllegalArgumentException when {@code expectAllowed}
- * is true and this parameter is not null. When the
- * {@code expectAllowed} is 'false' and this parameter is null,
- * this function does not compare error type of the networking
- * access failure.
+ * @deprecated The definition of "background" can be ambiguous. Use separate calls to
+ * {@link #assertProcessStateBelow(int)} with
+ * {@link #assertNetworkAccess(boolean, boolean, String)} to be explicit, instead.
*/
- protected void assertBackgroundNetworkAccess(boolean expectAllowed,
- @Nullable final String expectedUnavailableError) throws Exception {
- assertBackgroundState();
- if (expectAllowed && expectedUnavailableError != null) {
- throw new IllegalArgumentException("expectedUnavailableError is not null");
- }
- assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */,
- expectedUnavailableError);
+ @Deprecated
+ protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+ assertNetworkAccess(expectAllowed, false, null);
}
- protected void assertForegroundNetworkAccess() throws Exception {
- assertForegroundNetworkAccess(true);
- }
-
- protected void assertForegroundNetworkAccess(boolean expectAllowed) throws Exception {
- assertForegroundState();
- // We verified that app is in foreground state but if the screen turns-off while
- // verifying for network access, the app will go into background state (in case app's
- // foreground status was due to top activity). So, turn the screen on when verifying
- // network connectivity.
- assertNetworkAccess(expectAllowed /* expectAvailable */, true /* needScreenOn */);
+ protected void assertTopNetworkAccess(boolean expectAllowed) throws Exception {
+ assertTopState();
+ assertNetworkAccess(expectAllowed, true /* needScreenOn */);
}
protected void assertForegroundServiceNetworkAccess() throws Exception {
@@ -355,75 +330,65 @@
finishExpeditedJob();
}
- protected final void assertBackgroundState() throws Exception {
- final int maxTries = 30;
- ProcessState state = null;
- for (int i = 1; i <= maxTries; i++) {
- state = getProcessStateByUid(mUid);
- Log.v(TAG, "assertBackgroundState(): status for app2 (" + mUid + ") on attempt #" + i
- + ": " + state);
- if (isBackground(state.state)) {
- return;
- }
- Log.d(TAG, "App not on background state (" + state + ") on attempt #" + i
- + "; sleeping 1s before trying again");
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(SECOND_IN_MS);
- }
- }
- fail("App2 (" + mUid + ") is not on background state after "
- + maxTries + " attempts: " + state);
+ /**
+ * Asserts that the process state of the test app is below, in priority, to the given
+ * {@link android.app.ActivityManager.ProcessState}.
+ */
+ protected final void assertProcessStateBelow(int processState) throws Exception {
+ assertProcessState(ps -> ps.state > processState, null);
}
- protected final void assertForegroundState() throws Exception {
- final int maxTries = 30;
- ProcessState state = null;
- for (int i = 1; i <= maxTries; i++) {
- state = getProcessStateByUid(mUid);
- Log.v(TAG, "assertForegroundState(): status for app2 (" + mUid + ") on attempt #" + i
- + ": " + state);
- if (!isBackground(state.state)) {
- return;
- }
- Log.d(TAG, "App not on foreground state on attempt #" + i
- + "; sleeping 1s before trying again");
- turnScreenOn();
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(SECOND_IN_MS);
- }
- }
- fail("App2 (" + mUid + ") is not on foreground state after "
- + maxTries + " attempts: " + state);
+ protected final void assertTopState() throws Exception {
+ assertProcessState(ps -> ps.state == PROCESS_STATE_TOP, () -> turnScreenOn());
}
protected final void assertForegroundServiceState() throws Exception {
+ assertProcessState(ps -> ps.state == PROCESS_STATE_FOREGROUND_SERVICE, null);
+ }
+
+ private void assertProcessState(Predicate<ProcessState> statePredicate,
+ ThrowingRunnable onRetry) throws Exception {
final int maxTries = 30;
ProcessState state = null;
for (int i = 1; i <= maxTries; i++) {
+ if (onRetry != null) {
+ onRetry.run();
+ }
state = getProcessStateByUid(mUid);
- Log.v(TAG, "assertForegroundServiceState(): status for app2 (" + mUid + ") on attempt #"
- + i + ": " + state);
- if (state.state == PROCESS_STATE_FOREGROUND_SERVICE) {
+ Log.v(TAG, "assertProcessState(): status for app2 (" + mUid + ") on attempt #" + i
+ + ": " + state);
+ if (statePredicate.test(state)) {
return;
}
- Log.d(TAG, "App not on foreground service state on attempt #" + i
+ Log.i(TAG, "App not in desired process state on attempt #" + i
+ "; sleeping 1s before trying again");
- // No sleep after the last turn
if (i < maxTries) {
SystemClock.sleep(SECOND_IN_MS);
}
}
- fail("App2 (" + mUid + ") is not on foreground service state after "
- + maxTries + " attempts: " + state);
+ fail("App2 (" + mUid + ") is not in the desired process state after " + maxTries
+ + " attempts: " + state);
}
/**
- * Returns whether an app state should be considered "background" for restriction purposes.
+ * Asserts whether the active network is available or not. If the network is unavailable, also
+ * checks whether it is blocked by the expected error.
+ *
+ * @param expectAllowed expect background network access to be allowed or not.
+ * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
+ * meaningful only when the {@code expectAllowed} is 'false'.
+ * Throws an IllegalArgumentException when {@code expectAllowed}
+ * is true and this parameter is not null. When the
+ * {@code expectAllowed} is 'false' and this parameter is null,
+ * this function does not compare error type of the networking
+ * access failure.
*/
- protected boolean isBackground(int state) {
- return state > PROCESS_STATE_FOREGROUND_SERVICE;
+ protected void assertNetworkAccess(boolean expectAllowed, String expectedUnavailableError)
+ throws Exception {
+ if (expectAllowed && expectedUnavailableError != null) {
+ throw new IllegalArgumentException("expectedUnavailableError is not null");
+ }
+ assertNetworkAccess(expectAllowed, false, expectedUnavailableError);
}
/**
@@ -958,7 +923,7 @@
} else if (resultCode == INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE) {
Log.d(TAG, resultData);
// App didn't come to foreground when the activity is started, so try again.
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
} else {
fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
index 10775d0..4004789 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
@@ -17,6 +17,8 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getUiDevice;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
@@ -95,7 +97,7 @@
Log.i(TAG, testName + " start #" + i);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
getUiDevice().pressHome();
- assertBackgroundState();
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
Log.i(TAG, testName + " end #" + i);
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
index 2f30536..790e031 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
@@ -108,7 +108,7 @@
setRestrictBackground(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
setRestrictBackground(true);
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
// Although it should not have access while the screen is off.
turnScreenOff();
@@ -119,7 +119,7 @@
if (isTV()) {
startActivity();
}
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
// Goes back to background state.
finishActivity();
@@ -129,7 +129,7 @@
setRestrictBackground(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
setRestrictBackground(true);
- assertForegroundNetworkAccess();
+ assertForegroundServiceNetworkAccess();
stopForegroundService();
assertBackgroundNetworkAccess(false);
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index 82f4a65..eb2347d 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -16,6 +16,7 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
@@ -203,6 +204,7 @@
// Initial state
setBatterySaverMode(false);
setRestrictBackground(false);
+ setAppIdle(false);
// Get transports of the active network, this has to be done before changing meteredness,
// since wifi will be disconnected when changing from non-metered to metered.
@@ -312,7 +314,8 @@
// Enable Power Saver
setBatterySaverMode(true);
if (SdkLevel.isAtLeastT()) {
- assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+ assertNetworkAccess(false, "java.net.UnknownHostException");
} else {
assertBackgroundNetworkAccess(false);
}
@@ -336,7 +339,8 @@
// Enable Power Saver
setBatterySaverMode(true);
if (SdkLevel.isAtLeastT()) {
- assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+ assertNetworkAccess(false, "java.net.UnknownHostException");
} else {
assertBackgroundNetworkAccess(false);
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
index a0d88c9..7aeca77 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
@@ -16,6 +16,7 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
import static android.os.Process.SYSTEM_UID;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
@@ -137,13 +138,13 @@
// Make TEST_APP2_PKG go to foreground and mUid will be allowed temporarily.
launchActivity();
- assertForegroundState();
+ assertTopState();
assertNetworkingBlockedStatusForUid(mUid, METERED,
false /* expectedResult */); // Match NTWK_ALLOWED_TMP_ALLOWLIST
// Back to background.
finishActivity();
- assertBackgroundState();
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
assertNetworkingBlockedStatusForUid(mUid, METERED,
true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
} finally {
@@ -219,11 +220,11 @@
// Make TEST_APP2_PKG go to foreground and isUidRestrictedOnMeteredNetworks() will
// return false.
launchActivity();
- assertForegroundState();
+ assertTopState();
assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
// Back to background.
finishActivity();
- assertBackgroundState();
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
// Add mUid into restrict background whitelist and isUidRestrictedOnMeteredNetworks()
// will return false.
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
index 35f1f1c..4777bf4 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
@@ -38,7 +38,7 @@
// go to foreground state and enable restricted mode
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
setRestrictedNetworkingMode(true);
- assertForegroundNetworkAccess(false);
+ assertTopNetworkAccess(false);
// go to background state
finishActivity();
@@ -47,7 +47,7 @@
// disable restricted mode and assert network access in foreground and background states
setRestrictedNetworkingMode(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- assertForegroundNetworkAccess(true);
+ assertTopNetworkAccess(true);
// go to background state
finishActivity();
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 aa90f5f..fa68e3e 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -191,6 +191,6 @@
String path = "/proc/sys/net/ipv4/tcp_congestion_control";
String value = mDevice.executeAdbCommand("shell", "cat", path).trim();
- assertEquals(value, "cubic");
+ assertEquals("cubic", value);
}
}
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index da4fe28..a9e3715 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -49,5 +49,7 @@
"general-tests",
"mts-dnsresolver",
"mts-networking",
+ "mcts-dnsresolver",
+ "mcts-networking",
],
}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index e0fe929..ceb48d4 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -298,17 +298,6 @@
},
android.Manifest.permission.MODIFY_PHONE_STATE);
- // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
- // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
- // permissions are updated.
- runWithShellPermissionIdentity(
- () -> mConnectivityManager.requestNetwork(
- CELLULAR_NETWORK_REQUEST, testNetworkCallback),
- android.Manifest.permission.CONNECTIVITY_INTERNAL);
-
- final Network network = testNetworkCallback.waitForAvailable();
- assertNotNull(network);
-
assertTrue("Didn't receive broadcast for ACTION_CARRIER_CONFIG_CHANGED for subId=" + subId,
carrierConfigReceiver.waitForCarrierConfigChanged());
@@ -324,6 +313,17 @@
Thread.sleep(5_000);
+ // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
+ // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
+ // permissions are updated.
+ runWithShellPermissionIdentity(
+ () -> mConnectivityManager.requestNetwork(
+ CELLULAR_NETWORK_REQUEST, testNetworkCallback),
+ android.Manifest.permission.CONNECTIVITY_INTERNAL);
+
+ final Network network = testNetworkCallback.waitForAvailable();
+ assertNotNull(network);
+
// TODO(b/217559768): Receiving carrier config change and immediately checking carrier
// privileges is racy, as the CP status is updated after receiving the same signal. Move
// the CP check after sleep to temporarily reduce the flakiness. This will soon be fixed
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 6b7954a..f6a025a 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -648,7 +648,7 @@
testIpv6Only, requiresValidation, testSessionKey , testIkeTunConnParams)));
}
- @Test
+ @Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV4() throws Exception {
doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
@@ -660,7 +660,7 @@
false /* testSessionKey */, false /* testIkeTunConnParams */);
}
- @Test
+ @Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV6() throws Exception {
doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
diff --git a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
index eef3f87..5ba6c4c 100644
--- a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
+++ b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
@@ -23,11 +23,15 @@
import com.android.net.module.util.ArrayTrackRecord
import com.android.net.module.util.DnsPacket
import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_DST_ADDR_OFFSET
import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
import com.android.net.module.util.TrackRecord
import com.android.testutils.IPv6UdpFilter
import com.android.testutils.TapPacketReader
+import java.net.Inet6Address
+import java.net.InetAddress
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@@ -236,19 +240,28 @@
private fun getMdnsPayload(packet: ByteArray) = packet.copyOfRange(
ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, packet.size)
+private fun getDstAddr(packet: ByteArray): Inet6Address {
+ val v6AddrPos = ETHER_HEADER_LEN + IPV6_DST_ADDR_OFFSET
+ return Inet6Address.getByAddress(packet.copyOfRange(v6AddrPos, v6AddrPos + IPV6_ADDR_LEN))
+ as Inet6Address
+}
+
fun TapPacketReader.pollForMdnsPacket(
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS,
predicate: (TestDnsPacket) -> Boolean
): TestDnsPacket? {
val mdnsProbeFilter = IPv6UdpFilter(srcPort = MDNS_PORT, dstPort = MDNS_PORT).and {
+ val dst = getDstAddr(it)
val mdnsPayload = getMdnsPayload(it)
try {
- predicate(TestDnsPacket(mdnsPayload))
+ predicate(TestDnsPacket(mdnsPayload, dst))
} catch (e: DnsPacket.ParseException) {
false
}
}
- return poll(timeoutMs, mdnsProbeFilter)?.let { TestDnsPacket(getMdnsPayload(it)) }
+ return poll(timeoutMs, mdnsProbeFilter)?.let {
+ TestDnsPacket(getMdnsPayload(it), getDstAddr(it))
+ }
}
fun TapPacketReader.pollForProbe(
@@ -281,7 +294,7 @@
it.isReplyFor("$serviceName.$serviceType.local")
}
-class TestDnsPacket(data: ByteArray) : DnsPacket(data) {
+class TestDnsPacket(data: ByteArray, val dstAddr: InetAddress) : DnsPacket(data) {
val header: DnsHeader
get() = mHeader
val records: Array<List<DnsRecord>>
@@ -290,9 +303,10 @@
it.dName == name && it.nsType == DnsResolver.TYPE_ANY
}
- fun isReplyFor(name: String): Boolean = mRecords[ANSECTION].any {
- it.dName == name && it.nsType == DnsResolver.TYPE_SRV
- }
+ fun isReplyFor(name: String, type: Int = DnsResolver.TYPE_SRV): Boolean =
+ mRecords[ANSECTION].any {
+ it.dName == name && it.nsType == type
+ }
fun isQueryFor(name: String, vararg requiredTypes: Int): Boolean = requiredTypes.all { type ->
mRecords[QDSECTION].any {
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index f374181..1b1f367 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -62,7 +62,7 @@
@Test
fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
- assumeFalse(isInterfaceForTetheringAvailable)
+ assumeFalse(isInterfaceForTetheringAvailable())
var downstreamIface: TestNetworkInterface? = null
var tetheringEventCallback: MyTetheringEventCallback? = null
@@ -104,7 +104,7 @@
@Test
fun testMdnsDiscoveryWorkOnTetheringInterface() {
- assumeFalse(isInterfaceForTetheringAvailable)
+ assumeFalse(isInterfaceForTetheringAvailable())
setIncludeTestInterfaces(true)
var downstreamIface: TestNetworkInterface? = null
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index e0387c9..1309e79 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -61,6 +61,7 @@
import android.os.Handler
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
@@ -69,6 +70,7 @@
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
@@ -78,12 +80,14 @@
import com.android.modules.utils.build.SdkLevel.isAtLeastU
import com.android.net.module.util.DnsPacket
import com.android.net.module.util.HexDump
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
import com.android.net.module.util.PacketBuilder
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.DeviceConfigRule
+import com.android.testutils.NSResponder
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TapPacketReader
@@ -133,6 +137,7 @@
private const val TEST_PORT = 12345
private const val MDNS_PORT = 5353.toShort()
private val multicastIpv6Addr = parseNumericAddress("ff02::fb") as Inet6Address
+private val testSrcAddr = parseNumericAddress("2001:db8::123") as Inet6Address
@AppModeFull(reason = "Socket cannot bind in instant app mode")
@RunWith(DevSdkIgnoreRunner::class)
@@ -144,6 +149,9 @@
@get:Rule
val ignoreRule = DevSdkIgnoreRule()
+ @get:Rule
+ val deviceConfigRule = DeviceConfigRule()
+
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val nsdManager by lazy {
context.getSystemService(NsdManager::class.java) ?: fail("Could not get NsdManager service")
@@ -682,7 +690,7 @@
assertEquals(OffloadEngine.OFFLOAD_TYPE_REPLY.toLong(), serviceInfo.offloadType)
val offloadPayload = serviceInfo.offloadPayload
assertNotNull(offloadPayload)
- val dnsPacket = TestDnsPacket(offloadPayload)
+ val dnsPacket = TestDnsPacket(offloadPayload, dstAddr = multicastIpv6Addr)
assertEquals(0x8400, dnsPacket.header.flags)
assertEquals(0, dnsPacket.records[DnsPacket.QDSECTION].size)
assertTrue(dnsPacket.records[DnsPacket.ANSECTION].size >= 5)
@@ -1064,6 +1072,32 @@
}
@Test
+ fun testMultipleSubTypeAdvertisingAndDiscovery_withUpdate() {
+ val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype1"
+ }
+ val si2 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype2"
+ }
+ val registrationRecord = NsdRegistrationRecord()
+ val subtype3DiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si1)
+ updateService(registrationRecord, si2)
+ nsdManager.discoverServices("_subtype2.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD, testNetwork1.network,
+ { it.run() }, subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.waitForServiceDiscovered(serviceName,
+ serviceType, testNetwork1.network)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() {
val si = makeTestServiceInfo(network = testNetwork1.network)
// Sets 101 subtypes in total
@@ -1260,7 +1294,8 @@
// Resolve service on testNetwork1
val resolveRecord = NsdResolveRecord()
val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */
+ )
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
@@ -1323,6 +1358,68 @@
serviceResolved.serviceInfo.hostAddresses.toSet())
}
+ @Test
+ fun testUnicastReplyUsedWhenQueryUnicastFlagSet() {
+ // The flag may be removed in the future but unicast replies should be enabled by default
+ // in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+ val si = makeTestServiceInfo(testNetwork1.network)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ var nsResponder: NSResponder? = null
+ tryTest {
+ registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ /*
+ Send a "query unicast" query.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR', qclass=0x8001)
+ )).hex()
+ */
+ val mdnsPayload = HexDump.hexStringToByteArray("0000000000010000000000000d5f6e6d74313" +
+ "233343536373839045f746370056c6f63616c00000c8001")
+ replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
+
+ val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+ nsResponder = NSResponder(packetReader, mapOf(
+ testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+ )).apply { start() }
+
+ packetReader.sendResponse(buildMdnsPacket(mdnsPayload, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply)
+ } cleanup {
+ nsResponder?.stop()
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
+ private fun makeLinkLocalAddressOfOtherDeviceOnPrefix(network: Network): Inet6Address {
+ val lp = cm.getLinkProperties(network) ?: fail("No LinkProperties for net $network")
+ // Expect to have a /64 link-local address
+ val linkAddr = lp.linkAddresses.firstOrNull {
+ it.isIPv6 && it.scope == RT_SCOPE_LINK && it.prefixLength == 64
+ } ?: fail("No /64 link-local address found in ${lp.linkAddresses} for net $network")
+
+ // Add one to the device address to simulate the address of another device on the prefix
+ val addrBytes = linkAddr.address.address
+ addrBytes[IPV6_ADDR_LEN - 1]++
+ return Inet6Address.getByAddress(addrBytes) as Inet6Address
+ }
+
private fun buildConflictingAnnouncement(): ByteBuffer {
/*
Generated with:
@@ -1367,7 +1464,10 @@
replaceAll(buffer, source, replacement)
}
- private fun buildMdnsPacket(mdnsPayload: ByteArray): ByteBuffer {
+ private fun buildMdnsPacket(
+ mdnsPayload: ByteArray,
+ srcAddr: Inet6Address = testSrcAddr
+ ): ByteBuffer {
val packetBuffer = PacketBuilder.allocate(true /* hasEther */, IPPROTO_IPV6,
IPPROTO_UDP, mdnsPayload.size)
val packetBuilder = PacketBuilder(packetBuffer)
@@ -1382,7 +1482,7 @@
0x60000000, // version=6, traffic class=0x0, flowlabel=0x0
IPPROTO_UDP.toByte(),
64 /* hop limit */,
- parseNumericAddress("2001:db8::123") as Inet6Address /* srcIp */,
+ srcAddr,
multicastIpv6Addr /* dstIp */)
packetBuilder.writeUdpHeader(MDNS_PORT /* srcPort */, MDNS_PORT /* dstPort */)
packetBuffer.put(mdnsPayload)
@@ -1404,6 +1504,18 @@
return cb.serviceInfo
}
+ /**
+ * Update a service.
+ */
+ private fun updateService(
+ record: NsdRegistrationRecord,
+ si: NsdServiceInfo,
+ executor: Executor = Executor { it.run() }
+ ) {
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, executor, record)
+ // TODO: add the callback check for the update.
+ }
+
private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
val record = NsdResolveRecord()
nsdManager.resolveService(discoveredInfo, Executor { it.run() }, record)
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 496d163..9b1bf6e 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -37,28 +37,34 @@
import android.net.Uri
import android.net.metrics.IpConnectivityLog
import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
import android.os.IBinder
import android.os.SystemConfigManager
import android.os.UserHandle
import android.os.VintfRuntimeInfo
+import android.telephony.TelephonyManager
import android.testing.TestableContext
import android.util.Log
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.SystemUtil
import com.android.connectivity.resources.R
import com.android.net.module.util.BpfUtils
+import com.android.networkstack.apishim.TelephonyManagerShimImpl
import com.android.server.BpfNetMaps
import com.android.server.ConnectivityService
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.ProxyTracker
+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.tryTest
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@@ -74,12 +80,10 @@
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations
import org.mockito.Spy
@@ -90,7 +94,8 @@
* Test that exercises an instrumented version of ConnectivityService against an instrumented
* NetworkStack in a different test process.
*/
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
class ConnectivityServiceIntegrationTest {
// lateinit used here for mocks as they need to be reinitialized between each test and the test
// should crash if they are used before being initialized.
@@ -117,6 +122,8 @@
private lateinit var service: ConnectivityService
private lateinit var cm: ConnectivityManager
+ private val handlerThreads = mutableListOf<HandlerThread>()
+
companion object {
// lateinit for this binder token, as it must be initialized before any test code is run
// and use of it before init should crash the test.
@@ -197,7 +204,7 @@
networkStackClient = TestNetworkStackClient(realContext)
networkStackClient.start()
- service = TestConnectivityService(makeDependencies())
+ service = TestConnectivityService(TestDependencies())
cm = ConnectivityManager(context, service)
context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm)
context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager)
@@ -208,31 +215,51 @@
private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
context, dnsResolver, log, netd, deps)
- private fun makeDependencies(): ConnectivityService.Dependencies {
- val deps = spy(ConnectivityService.Dependencies())
- doReturn(networkStackClient).`when`(deps).networkStack
- doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any())
- doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties
- doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager()
- doReturn(mock(BpfNetMaps::class.java)).`when`(deps).getBpfNetMaps(any(), any())
- doAnswer { inv ->
- MultinetworkPolicyTracker(inv.getArgument(0),
- inv.getArgument(1),
- inv.getArgument(2),
- object : MultinetworkPolicyTracker.Dependencies() {
- override fun getResourcesForActiveSubId(
- connResources: ConnectivityResources,
- activeSubId: Int
- ) = resources
- })
- }.`when`(deps).makeMultinetworkPolicyTracker(any(), any(), any())
- return deps
+ private inner class TestDependencies : ConnectivityService.Dependencies() {
+ override fun getNetworkStack() = networkStackClient
+ override fun makeProxyTracker(context: Context, connServiceHandler: Handler) =
+ mock(ProxyTracker::class.java)
+ override fun getSystemProperties() = mock(MockableSystemProperties::class.java)
+ override fun makeNetIdManager() = TestNetIdManager()
+ override fun getBpfNetMaps(context: Context?, netd: INetd?) = mock(BpfNetMaps::class.java)
+
+ override fun makeMultinetworkPolicyTracker(
+ c: Context,
+ h: Handler,
+ r: Runnable
+ ) = MultinetworkPolicyTracker(c, h, r,
+ object : MultinetworkPolicyTracker.Dependencies() {
+ override fun getResourcesForActiveSubId(
+ connResources: ConnectivityResources,
+ activeSubId: Int
+ ) = resources
+ })
+
+ override fun makeHandlerThread(tag: String): HandlerThread =
+ super.makeHandlerThread(tag).also { handlerThreads.add(it) }
+
+ override fun makeCarrierPrivilegeAuthenticator(
+ context: Context,
+ tm: TelephonyManager
+ ): CarrierPrivilegeAuthenticator {
+ return CarrierPrivilegeAuthenticator(context,
+ object : CarrierPrivilegeAuthenticator.Dependencies() {
+ override fun makeHandlerThread(): HandlerThread =
+ super.makeHandlerThread().also { handlerThreads.add(it) }
+ },
+ tm, TelephonyManagerShimImpl.newInstance(tm))
+ }
}
@After
fun tearDown() {
nsInstrumentation.clearAllState()
ConnectivityResources.setResourcesContextForTest(null)
+ handlerThreads.forEach {
+ it.quitSafely()
+ it.join()
+ }
+ handlerThreads.clear()
}
@Test
@@ -254,13 +281,18 @@
na.addCapability(NET_CAPABILITY_INTERNET)
na.connect()
- testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
- val requestedSize = nsInstrumentation.getRequestUrls().size
- if (requestedSize == 2 || (requestedSize == 1 &&
- nsInstrumentation.getRequestUrls()[0] == httpsProbeUrl)) {
- return
+ tryTest {
+ testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
+ val requestedSize = nsInstrumentation.getRequestUrls().size
+ if (requestedSize == 2 || (requestedSize == 1 &&
+ nsInstrumentation.getRequestUrls()[0] == httpsProbeUrl)
+ ) {
+ return@tryTest
+ }
+ fail("Unexpected request urls: ${nsInstrumentation.getRequestUrls()}")
+ } cleanup {
+ na.destroy()
}
- fail("Unexpected request urls: ${nsInstrumentation.getRequestUrls()}")
}
@Test
@@ -292,24 +324,32 @@
val lp = LinkProperties()
lp.captivePortalApiUrl = Uri.parse(apiUrl)
val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, lp, null /* ncTemplate */, context)
- networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
- na.addCapability(NET_CAPABILITY_INTERNET)
- na.connect()
+ tryTest {
+ networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
- testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS)
+ na.addCapability(NET_CAPABILITY_INTERNET)
+ na.connect()
- val capportData = testCb.expect<LinkPropertiesChanged>(na, TEST_TIMEOUT_MS) {
- it.lp.captivePortalData != null
- }.lp.captivePortalData
- assertNotNull(capportData)
- assertTrue(capportData.isCaptive)
- assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
- assertEquals(Uri.parse("https://venueinfo.capport.android.com"), capportData.venueInfoUrl)
+ testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS)
- testCb.expectCaps(na, TEST_TIMEOUT_MS) {
- it.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) &&
- !it.hasCapability(NET_CAPABILITY_VALIDATED)
+ val capportData = testCb.expect<LinkPropertiesChanged>(na, TEST_TIMEOUT_MS) {
+ it.lp.captivePortalData != null
+ }.lp.captivePortalData
+ assertNotNull(capportData)
+ assertTrue(capportData.isCaptive)
+ assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
+ assertEquals(
+ Uri.parse("https://venueinfo.capport.android.com"),
+ capportData.venueInfoUrl
+ )
+
+ testCb.expectCaps(na, TEST_TIMEOUT_MS) {
+ it.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) &&
+ !it.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ } cleanup {
+ na.destroy()
}
}
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index ec09f9e..960c6ca 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -36,6 +36,7 @@
import static org.junit.Assert.fail;
import android.annotation.NonNull;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
@@ -51,6 +52,7 @@
import android.os.ConditionVariable;
import android.os.HandlerThread;
import android.os.Message;
+import android.util.CloseGuard;
import android.util.Log;
import android.util.Range;
@@ -65,11 +67,14 @@
import java.util.function.Consumer;
public class NetworkAgentWrapper implements TestableNetworkCallback.HasNetwork {
+ private static final long DESTROY_TIMEOUT_MS = 10_000L;
+
// Note : Please do not add any new instrumentation here. If you need new instrumentation,
// please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more
// tools in ConnectivityServiceTest.
private final NetworkCapabilities mNetworkCapabilities;
private final HandlerThread mHandlerThread;
+ private final CloseGuard mCloseGuard;
private final Context mContext;
private final String mLogTag;
private final NetworkAgentConfig mNetworkAgentConfig;
@@ -157,6 +162,8 @@
mLogTag = "Mock-" + typeName;
mHandlerThread = new HandlerThread(mLogTag);
mHandlerThread.start();
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("destroy");
// extraInfo is set to "" by default in NetworkAgentConfig.
final String extraInfo = (transport == TRANSPORT_CELLULAR) ? "internet.apn" : "";
@@ -359,6 +366,35 @@
mNetworkAgent.unregister();
}
+ /**
+ * Destroy the network agent and stop its looper.
+ *
+ * <p>This must always be called.
+ */
+ public void destroy() {
+ mHandlerThread.quitSafely();
+ try {
+ mHandlerThread.join(DESTROY_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ Log.e(mLogTag, "Interrupted when waiting for handler thread on destroy", e);
+ }
+ mCloseGuard.close();
+ }
+
+ @SuppressLint("Finalize") // Follows the recommended pattern for CloseGuard
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ // Note that mCloseGuard could be null if the constructor threw.
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
@Override
public Network getNetwork() {
return mNetworkAgent.getNetwork();
diff --git a/tests/native/connectivity_native_test/connectivity_native_test.cpp b/tests/native/connectivity_native_test/connectivity_native_test.cpp
index 27a9d35..f62a30b 100644
--- a/tests/native/connectivity_native_test/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test/connectivity_native_test.cpp
@@ -41,13 +41,14 @@
void SetUp() override {
restoreBlockedPorts = false;
+
// Skip test case if not on U.
- if (!android::modules::sdklevel::IsAtLeastU()) GTEST_SKIP() <<
- "Should be at least T device.";
+ if (!android::modules::sdklevel::IsAtLeastU())
+ GTEST_SKIP() << "Should be at least U device.";
// Skip test case if not on 5.4 kernel which is required by bpf prog.
- if (!android::bpf::isAtLeastKernelVersion(5, 4, 0)) GTEST_SKIP() <<
- "Kernel should be at least 5.4.";
+ if (!android::bpf::isAtLeastKernelVersion(5, 4, 0))
+ GTEST_SKIP() << "Kernel should be at least 5.4.";
// Necessary to use dlopen/dlsym since the lib is only available on U and there
// is no Sdk34ModuleController in tradefed yet.
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index a6e9e95..81557f8 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -64,6 +64,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
@@ -90,7 +91,8 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public class NetworkStatsCollectionTest {
-
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
private static final String TEST_FILE = "test.bin";
private static final String TEST_IMSI = "310260000000000";
private static final int TEST_SUBID = 1;
@@ -199,6 +201,33 @@
77017831L, 100995L, 35436758L, 92344L);
}
+ private InputStream getUidInputStreamFromRes(int uidRes) throws Exception {
+ final File testFile =
+ new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE);
+ stageFile(uidRes, testFile);
+
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+ collection.readLegacyUid(testFile, true);
+
+ // now export into a unified format
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ collection.write(bos);
+ return new ByteArrayInputStream(bos.toByteArray());
+ }
+
+ @Test
+ public void testFastDataInputRead() throws Exception {
+ final NetworkStatsCollection legacyCollection =
+ new NetworkStatsCollection(30 * MINUTE_IN_MILLIS, false /* useFastDataInput */);
+ final NetworkStatsCollection fastReadCollection =
+ new NetworkStatsCollection(30 * MINUTE_IN_MILLIS, true /* useFastDataInput */);
+ final InputStream bis = getUidInputStreamFromRes(R.raw.netstats_uid_v4);
+ legacyCollection.read(bis);
+ bis.reset();
+ fastReadCollection.read(bis);
+ assertCollectionEntries(legacyCollection.getEntries(), fastReadCollection);
+ }
+
@Test
public void testStartEndAtomicBuckets() throws Exception {
final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS);
diff --git a/tests/unit/java/android/net/NetworkStatsRecorderTest.java b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
index fad11a3..7d039b6 100644
--- a/tests/unit/java/android/net/NetworkStatsRecorderTest.java
+++ b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
@@ -16,8 +16,17 @@
package com.android.server.net;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.mockito.Mockito.any;
@@ -29,21 +38,31 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.annotation.NonNull;
+import android.net.NetworkIdentity;
+import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
+import android.net.NetworkStatsCollection;
import android.os.DropBoxManager;
import androidx.test.filters.SmallTest;
import com.android.internal.util.FileRotator;
+import com.android.metrics.NetworkStatsMetricsLogger;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import libcore.testing.io.TestIoUtils;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
@RunWith(DevSdkIgnoreRunner.class)
@@ -53,6 +72,8 @@
private static final String TAG = NetworkStatsRecorderTest.class.getSimpleName();
private static final String TEST_PREFIX = "test";
+ private static final int TEST_UID1 = 1234;
+ private static final int TEST_UID2 = 1235;
@Mock private DropBoxManager mDropBox;
@Mock private NetworkStats.NonMonotonicObserver mObserver;
@@ -64,7 +85,8 @@
private NetworkStatsRecorder buildRecorder(FileRotator rotator, boolean wipeOnError) {
return new NetworkStatsRecorder(rotator, mObserver, mDropBox, TEST_PREFIX,
- HOUR_IN_MILLIS, false /* includeTags */, wipeOnError);
+ HOUR_IN_MILLIS, false /* includeTags */, wipeOnError,
+ false /* useFastDataInput */, null /* baseDir */);
}
@Test
@@ -85,4 +107,110 @@
// Verify that the rotator won't delete files.
verify(rotator, never()).deleteAll();
}
+
+ @Test
+ public void testFileReadingMetrics_empty() {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_XT, 888, null /* statsDir */, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT,
+ 1 /* readIndex */,
+ 888 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+
+ // Write second time, verify the index increases.
+ logger.logRecorderFileReading(PREFIX_XT, 567, null /* statsDir */, collection,
+ true /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT,
+ 2 /* readIndex */,
+ 567 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ true /* useFastDataInput */
+ );
+ }
+
+ @Test
+ public void testFileReadingMetrics() {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+ final NetworkIdentitySet identSet = new NetworkIdentitySet();
+ identSet.add(new NetworkIdentity.Builder().build());
+ // Empty entries will be skipped, put some ints to make sure they can be recorded.
+ entry.rxBytes = 1;
+
+ collection.recordData(identSet, TEST_UID1, SET_DEFAULT, TAG_NONE, 0, 60, entry);
+ collection.recordData(identSet, TEST_UID2, SET_DEFAULT, TAG_NONE, 0, 60, entry);
+ collection.recordData(identSet, TEST_UID2, SET_FOREGROUND, TAG_NONE, 30, 60, entry);
+
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_UID, 123, null /* statsDir */, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID,
+ 1 /* readIndex */,
+ 123 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 3 /* keys */,
+ 2 /* uids */,
+ 5 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+ }
+
+ @Test
+ public void testFileReadingMetrics_fileAttributes() throws IOException {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+
+ // Create files for testing. Only the first and the third files should be counted,
+ // with total 26 (each char takes 2 bytes) bytes in the content.
+ final File statsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+ write(statsDir, "uid_tag.1024-2048", "wanted");
+ write(statsDir, "uid_tag.1024-2048.backup", "");
+ write(statsDir, "uid_tag.2048-", "wanted2");
+ write(statsDir, "uid.2048-4096", "unwanted");
+ write(statsDir, "uid.2048-4096.backup", "unwanted2");
+
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_UID_TAG, 678, statsDir, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG,
+ 1 /* readIndex */,
+ 678 /* readLatencyMillis */,
+ 2 /* fileCount */,
+ 26 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+ }
+
+ private void write(@NonNull File baseDir, @NonNull String name,
+ @NonNull String value) throws IOException {
+ final DataOutputStream out = new DataOutputStream(
+ new FileOutputStream(new File(baseDir, name)));
+ out.writeChars(value);
+ out.close();
+ }
}
diff --git a/tests/unit/java/android/net/NetworkUtilsTest.java b/tests/unit/java/android/net/NetworkUtilsTest.java
index 5d789b4..2bf2211 100644
--- a/tests/unit/java/android/net/NetworkUtilsTest.java
+++ b/tests/unit/java/android/net/NetworkUtilsTest.java
@@ -21,8 +21,14 @@
import static android.system.OsConstants.SOCK_DGRAM;
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_RCVTIMEO;
+
+import static com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel;
+
import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
import android.os.Build;
import android.system.ErrnoException;
import android.system.Os;
@@ -38,7 +44,6 @@
import org.junit.runner.RunWith;
import java.io.FileDescriptor;
-import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -167,4 +172,10 @@
assertEquals(writeTimeval, readTimeval);
SocketUtils.closeSocketQuietly(sock);
}
+
+ @Test
+ public void testIsKernel64Bit() {
+ assumeTrue(getVsrApiLevel() > Build.VERSION_CODES.TIRAMISU);
+ assertTrue(NetworkUtils.isKernel64Bit());
+ }
}
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 550a9ee..461ead8 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -38,6 +38,7 @@
import androidx.test.filters.SmallTest;
+import com.android.modules.utils.build.SdkLevel;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.FunctionalUtils.ThrowingConsumer;
@@ -86,73 +87,81 @@
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testResolveServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestResolveService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testResolveServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestResolveService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testDiscoverServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestDiscoverService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testDiscoverServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestDiscoverService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testParallelResolveServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestParallelResolveService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testParallelResolveServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestParallelResolveService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testInvalidCallsS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestInvalidCalls();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testInvalidCallsPreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestInvalidCalls();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testRegisterServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestRegisterService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testRegisterServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestRegisterService();
}
+ private void verifyDaemonStarted(boolean targetSdkPreS) throws Exception {
+ if (targetSdkPreS && !SdkLevel.isAtLeastV()) {
+ verify(mServiceConn).startDaemon();
+ } else {
+ verify(mServiceConn, never()).startDaemon();
+ }
+ }
+
private void doTestResolveService() throws Exception {
NsdManager manager = mManager;
@@ -196,6 +205,22 @@
verify(listener2, timeout(mTimeoutMs).times(1)).onServiceResolved(reply);
}
+ @Test
+ public void testRegisterServiceWithAdvertisingRequest() throws Exception {
+ final NsdManager manager = mManager;
+ final NsdServiceInfo request = new NsdServiceInfo("another_name2", "another_type2");
+ request.setPort(2203);
+ final AdvertisingRequest advertisingRequest = new AdvertisingRequest.Builder(request,
+ PROTOCOL).build();
+ final NsdManager.RegistrationListener listener = mock(
+ NsdManager.RegistrationListener.class);
+
+ manager.registerService(advertisingRequest, Runnable::run, listener);
+ int key4 = getRequestKey(req -> verify(mServiceConn).registerService(req.capture(), any()));
+ mCallback.onRegisterServiceSucceeded(key4, request);
+ verify(listener, timeout(mTimeoutMs).times(1)).onServiceRegistered(request);
+ }
+
private void doTestRegisterService() throws Exception {
NsdManager manager = mManager;
@@ -346,8 +371,19 @@
NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
- NsdServiceInfo validService = new NsdServiceInfo("a_name", "a_type");
+ NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo otherServiceWithSubtype = new NsdServiceInfo("b_name", "_a_type._tcp,_sub1");
+ NsdServiceInfo validServiceDuplicate = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo validServiceSubtypeUpdate = new NsdServiceInfo("a_name",
+ "_a_type._tcp,_sub1,_s2");
+ NsdServiceInfo otherSubtypeUpdate = new NsdServiceInfo("a_name", "_a_type._tcp,_sub1,_s3");
+ NsdServiceInfo dotSyntaxSubtypeUpdate = new NsdServiceInfo("a_name", "_sub1._a_type._tcp");
validService.setPort(2222);
+ otherServiceWithSubtype.setPort(2222);
+ validServiceDuplicate.setPort(2222);
+ validServiceSubtypeUpdate.setPort(2222);
+ otherSubtypeUpdate.setPort(2222);
+ dotSyntaxSubtypeUpdate.setPort(2222);
// Service registration
// - invalid arguments
@@ -358,7 +394,21 @@
mustFail(() -> { manager.registerService(validService, -1, listener1); });
mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
manager.registerService(validService, PROTOCOL, listener1);
- // - listener already registered
+ // - update without subtype is not allowed
+ mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
+ // - update with subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - re-updating to the same subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - updating to other subtypes is allowed
+ manager.registerService(otherSubtypeUpdate, PROTOCOL, listener1);
+ // - update back to the service without subtype is allowed
+ manager.registerService(validService, PROTOCOL, listener1);
+ // - updating to a subtype with _sub._type syntax is not allowed
+ mustFail(() -> { manager.registerService(dotSyntaxSubtypeUpdate, PROTOCOL, listener1); });
+ // - updating to a different service name is not allowed
+ mustFail(() -> { manager.registerService(otherServiceWithSubtype, PROTOCOL, listener1); });
+ // - listener already registered, and not using subtypes
mustFail(() -> { manager.registerService(validService, PROTOCOL, listener1); });
manager.unregisterService(listener1);
// TODO: make listener immediately reusable
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 8f5fd7c..7f821dd 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -502,6 +502,7 @@
// to enable faster testing of smaller groups of functionality.
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
+@DevSdkIgnoreRunner.MonitorThreadLeak
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public class ConnectivityServiceTest {
private static final String TAG = "ConnectivityServiceTest";
@@ -589,6 +590,7 @@
private TestNetworkAgentWrapper mWiFiAgent;
private TestNetworkAgentWrapper mCellAgent;
private TestNetworkAgentWrapper mEthernetAgent;
+ private final List<TestNetworkAgentWrapper> mCreatedAgents = new ArrayList<>();
private MockVpn mMockVpn;
private Context mContext;
private NetworkPolicyCallback mPolicyCallback;
@@ -1092,6 +1094,7 @@
NetworkCapabilities ncTemplate, NetworkProvider provider,
NetworkAgentWrapper.Callbacks callbacks) throws Exception {
super(transport, linkProperties, ncTemplate, provider, callbacks, mServiceContext);
+ mCreatedAgents.add(this);
// Waits for the NetworkAgent to be registered, which includes the creation of the
// NetworkMonitor.
@@ -2404,6 +2407,11 @@
FakeSettingsProvider.clearSettingsProvider();
ConnectivityResources.setResourcesContextForTest(null);
+ for (TestNetworkAgentWrapper agent : mCreatedAgents) {
+ agent.destroy();
+ }
+ mCreatedAgents.clear();
+
mCsHandlerThread.quitSafely();
mCsHandlerThread.join();
mAlarmManagerThread.quitSafely();
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 4dc96f1..87e7967 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -150,6 +150,9 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NsdServiceTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
private static final long CLEANUP_DELAY_MS = 500;
private static final long TIMEOUT_MS = 500;
@@ -255,6 +258,8 @@
}
}
+ // Native mdns provided by Netd is removed after U.
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
@DisableCompatChanges({
RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER,
@@ -287,6 +292,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testNoDaemonStartedWhenClientsConnect() throws Exception {
// Creating an NsdManager will not cause daemon startup.
connectClient(mService);
@@ -322,6 +328,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testClientRequestsAreGCedAtDisconnection() throws Exception {
final NsdManager client = connectClient(mService);
final INsdManagerCallback cb1 = getCallback();
@@ -366,6 +373,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testCleanupDelayNoRequestActive() throws Exception {
final NsdManager client = connectClient(mService);
@@ -402,6 +410,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testDiscoverOnTetheringDownstream() throws Exception {
final NsdManager client = connectClient(mService);
final int interfaceIdx = 123;
@@ -500,6 +509,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testDiscoverOnBlackholeNetwork() throws Exception {
final NsdManager client = connectClient(mService);
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -532,6 +542,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceRegistrationSuccessfulAndFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -586,6 +597,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceDiscoveryFailed() throws Exception {
final NsdManager client = connectClient(mService);
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -618,6 +630,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceResolutionFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -653,6 +666,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testGettingAddressFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -704,6 +718,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testNoCrashWhenProcessResolutionAfterBinderDied() throws Exception {
final NsdManager client = connectClient(mService);
final INsdManagerCallback cb = getCallback();
@@ -724,6 +739,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopServiceResolution() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -750,6 +766,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopResolutionFailed() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -775,6 +792,7 @@
@Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopResolutionDuringGettingAddress() throws RemoteException {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -956,6 +974,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testMdnsDiscoveryManagerFeature() {
// Create NsdService w/o feature enabled.
final NsdManager client = connectClient(mService);
@@ -1203,6 +1222,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testMdnsAdvertiserFeatureFlagging() {
// Create NsdService w/o feature enabled.
final NsdManager client = connectClient(mService);
@@ -1241,6 +1261,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testTypeSpecificFeatureFlagging() {
doReturn("_type1._tcp:flag1,_type2._tcp:flag2").when(mDeps).getTypeAllowlistFlags();
doReturn(true).when(mDeps).isFeatureEnabled(any(),
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 10a0982..6cc301d 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -72,9 +72,7 @@
import android.os.SystemClock;
import android.telephony.SubscriptionManager;
import android.test.suitebuilder.annotation.SmallTest;
-import android.util.ArraySet;
import android.util.Log;
-import android.util.Range;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -104,9 +102,7 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
-import java.util.Set;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@@ -142,81 +138,81 @@
// Hexadecimal representation of a SOCK_DIAG response with tcp info.
private static final String SOCK_DIAG_TCP_INET_HEX =
// struct nlmsghdr.
- "14010000" + // length = 276
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0301" + // flags = NLM_F_REQUEST | NLM_F_DUMP
- "00000000" + // seqno
- "00000000" + // pid (0 == kernel)
+ "14010000" // length = 276
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0301" // flags = NLM_F_REQUEST | NLM_F_DUMP
+ + "00000000" // seqno
+ + "00000000" // pid (0 == kernel)
// struct inet_diag_req_v2
- "02" + // family = AF_INET
- "06" + // state
- "00" + // timer
- "00" + // retrans
+ + "02" // family = AF_INET
+ + "06" // state
+ + "00" // timer
+ + "00" // retrans
// inet_diag_sockid
- "DEA5" + // idiag_sport = 42462
- "71B9" + // idiag_dport = 47473
- "0a006402000000000000000000000000" + // idiag_src = 10.0.100.2
- "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8
- "00000000" + // idiag_if
- "34ED000076270000" + // idiag_cookie = 43387759684916
- "00000000" + // idiag_expires
- "00000000" + // idiag_rqueue
- "00000000" + // idiag_wqueue
- "00000000" + // idiag_uid
- "00000000" + // idiag_inode
+ + "DEA5" // idiag_sport = 42462
+ + "71B9" // idiag_dport = 47473
+ + "0a006402000000000000000000000000" // idiag_src = 10.0.100.2
+ + "08080808000000000000000000000000" // idiag_dst = 8.8.8.8
+ + "00000000" // idiag_if
+ + "34ED000076270000" // idiag_cookie = 43387759684916
+ + "00000000" // idiag_expires
+ + "00000000" // idiag_rqueue
+ + "00000000" // idiag_wqueue
+ + "39300000" // idiag_uid = 12345
+ + "00000000" // idiag_inode
// rtattr
- "0500" + // len = 5
- "0800" + // type = 8
- "00000000" + // data
- "0800" + // len = 8
- "0F00" + // type = 15(INET_DIAG_MARK)
- "850A0C00" + // data, socket mark=789125
- "AC00" + // len = 172
- "0200" + // type = 2(INET_DIAG_INFO)
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "AC00" // len = 172
+ + "0200" // type = 2(INET_DIAG_INFO)
// tcp_info
- "01" + // state = TCP_ESTABLISHED
- "00" + // ca_state = TCP_CA_OPEN
- "05" + // retransmits = 5
- "00" + // probes = 0
- "00" + // backoff = 0
- "07" + // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
- "88" + // wscale = 8
- "00" + // delivery_rate_app_limited = 0
- "4A911B00" + // rto = 1806666
- "00000000" + // ato = 0
- "2E050000" + // sndMss = 1326
- "18020000" + // rcvMss = 536
- "00000000" + // unsacked = 0
- "00000000" + // acked = 0
- "00000000" + // lost = 0
- "00000000" + // retrans = 0
- "00000000" + // fackets = 0
- "BB000000" + // lastDataSent = 187
- "00000000" + // lastAckSent = 0
- "BB000000" + // lastDataRecv = 187
- "BB000000" + // lastDataAckRecv = 187
- "DC050000" + // pmtu = 1500
- "30560100" + // rcvSsthresh = 87600
- "3E2C0900" + // rttt = 601150
- "1F960400" + // rttvar = 300575
- "78050000" + // sndSsthresh = 1400
- "0A000000" + // sndCwnd = 10
- "A8050000" + // advmss = 1448
- "03000000" + // reordering = 3
- "00000000" + // rcvrtt = 0
- "30560100" + // rcvspace = 87600
- "00000000" + // totalRetrans = 0
- "53AC000000000000" + // pacingRate = 44115
- "FFFFFFFFFFFFFFFF" + // maxPacingRate = 18446744073709551615
- "0100000000000000" + // bytesAcked = 1
- "0000000000000000" + // bytesReceived = 0
- "0A000000" + // SegsOut = 10
- "00000000" + // SegsIn = 0
- "00000000" + // NotSentBytes = 0
- "3E2C0900" + // minRtt = 601150
- "00000000" + // DataSegsIn = 0
- "00000000" + // DataSegsOut = 0
- "0000000000000000"; // deliverRate = 0
+ + "01" // state = TCP_ESTABLISHED
+ + "00" // ca_state = TCP_CA_OPEN
+ + "05" // retransmits = 5
+ + "00" // probes = 0
+ + "00" // backoff = 0
+ + "07" // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
+ + "88" // wscale = 8
+ + "00" // delivery_rate_app_limited = 0
+ + "4A911B00" // rto = 1806666
+ + "00000000" // ato = 0
+ + "2E050000" // sndMss = 1326
+ + "18020000" // rcvMss = 536
+ + "00000000" // unsacked = 0
+ + "00000000" // acked = 0
+ + "00000000" // lost = 0
+ + "00000000" // retrans = 0
+ + "00000000" // fackets = 0
+ + "BB000000" // lastDataSent = 187
+ + "00000000" // lastAckSent = 0
+ + "BB000000" // lastDataRecv = 187
+ + "BB000000" // lastDataAckRecv = 187
+ + "DC050000" // pmtu = 1500
+ + "30560100" // rcvSsthresh = 87600
+ + "3E2C0900" // rttt = 601150
+ + "1F960400" // rttvar = 300575
+ + "78050000" // sndSsthresh = 1400
+ + "0A000000" // sndCwnd = 10
+ + "A8050000" // advmss = 1448
+ + "03000000" // reordering = 3
+ + "00000000" // rcvrtt = 0
+ + "30560100" // rcvspace = 87600
+ + "00000000" // totalRetrans = 0
+ + "53AC000000000000" // pacingRate = 44115
+ + "FFFFFFFFFFFFFFFF" // maxPacingRate = 18446744073709551615
+ + "0100000000000000" // bytesAcked = 1
+ + "0000000000000000" // bytesReceived = 0
+ + "0A000000" // SegsOut = 10
+ + "00000000" // SegsIn = 0
+ + "00000000" // NotSentBytes = 0
+ + "3E2C0900" // minRtt = 601150
+ + "00000000" // DataSegsIn = 0
+ + "00000000" // DataSegsOut = 0
+ + "0000000000000000"; // deliverRate = 0
private static final String SOCK_DIAG_NO_TCP_INET_HEX =
// struct nlmsghdr
"14000000" // length = 20
@@ -236,9 +232,6 @@
private static final byte[] TEST_RESPONSE_BYTES =
HexEncoding.decode(TEST_RESPONSE_HEX.toCharArray(), false);
- private static final Set<Range<Integer>> TEST_UID_RANGES =
- new ArraySet<>(Arrays.asList(new Range<>(10000, 99999)));
-
private static class TestKeepaliveInfo {
private static List<Socket> sOpenSockets = new ArrayList<>();
@@ -416,28 +409,28 @@
public void testIsAnyTcpSocketConnected_runOnNonHandlerThread() throws Exception {
setupResponseWithSocketExisting();
assertThrows(IllegalStateException.class,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
}
@Test
public void testIsAnyTcpSocketConnected_withTargetNetId() throws Exception {
setupResponseWithSocketExisting();
assertTrue(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
}
@Test
public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
setupResponseWithSocketExisting();
assertFalse(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID, TEST_UID_RANGES)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
}
@Test
public void testIsAnyTcpSocketConnected_noSocketExists() throws Exception {
setupResponseWithoutSocketExisting();
assertFalse(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
}
private void triggerEventKeepalive(int slot, int reason) {
@@ -481,16 +474,14 @@
setupResponseWithoutSocketExisting();
visibleOnHandlerThread(
mTestHandler,
- () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(
- autoKi, TEST_NETID, TEST_UID_RANGES));
+ () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(autoKi, TEST_NETID));
}
private void doResumeKeepalive(AutomaticOnOffKeepalive autoKi) throws Exception {
setupResponseWithSocketExisting();
visibleOnHandlerThread(
mTestHandler,
- () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(
- autoKi, TEST_NETID, TEST_UID_RANGES));
+ () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(autoKi, TEST_NETID));
}
private void doStopKeepalive(AutomaticOnOffKeepalive autoKi) throws Exception {
diff --git a/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt
new file mode 100644
index 0000000..6c2c256
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt
@@ -0,0 +1,499 @@
+/*
+ * 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.connectivity
+
+import android.net.MulticastRoutingConfig
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.ParcelFileDescriptor
+import android.os.SystemClock
+import android.os.test.TestLooper
+import android.system.Os
+import android.system.OsConstants.AF_INET6
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import android.util.Log
+import androidx.test.filters.LargeTest
+import com.android.net.module.util.structs.StructMf6cctl
+import com.android.net.module.util.structs.StructMrt6Msg
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.InetAddress
+import java.net.Inet6Address
+import java.net.InetSocketAddress
+import java.net.MulticastSocket
+import java.net.NetworkInterface
+import java.time.Clock
+import java.time.Instant
+import java.time.ZoneId
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+private const val TIMEOUT_MS = 2_000L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class MulticastRoutingCoordinatorServiceTest {
+
+ // mocks are lateinit as they need to be setup between tests
+ @Mock private lateinit var mDeps: MulticastRoutingCoordinatorService.Dependencies
+ @Mock private lateinit var mMulticastSocket: MulticastSocket
+
+ val mSock = DatagramSocket()
+ val mPfd = ParcelFileDescriptor.fromDatagramSocket(mSock)
+ val mFd = mPfd.getFileDescriptor()
+ val mIfName1 = "interface1"
+ val mIfName2 = "interface2"
+ val mIfName3 = "interface3"
+ val mIfPhysicalIndex1 = 10
+ val mIfPhysicalIndex2 = 11
+ val mIfPhysicalIndex3 = 12
+ val mSourceAddress = Inet6Address.getByName("2000::8888") as Inet6Address
+ val mGroupAddressScope5 = Inet6Address.getByName("ff05::1234") as Inet6Address
+ val mGroupAddressScope4 = Inet6Address.getByName("ff04::1234") as Inet6Address
+ val mGroupAddressScope3 = Inet6Address.getByName("ff03::1234") as Inet6Address
+ val mSocketAddressScope5 = InetSocketAddress(mGroupAddressScope5, 0)
+ val mSocketAddressScope4 = InetSocketAddress(mGroupAddressScope4, 0)
+ val mEmptyOifs = setOf<Int>()
+ val mClock = FakeClock()
+ val mNetworkInterface1 = createEmptyNetworkInterface()
+ val mNetworkInterface2 = createEmptyNetworkInterface()
+ // MulticastRoutingCoordinatorService needs to be initialized after the dependencies
+ // are mocked.
+ lateinit var mService: MulticastRoutingCoordinatorService
+ lateinit var mLooper: TestLooper
+
+ class FakeClock() : Clock() {
+ private var offsetMs = 0L
+
+ fun fastForward(ms: Long) {
+ offsetMs += ms
+ }
+
+ override fun instant(): Instant {
+ return Instant.now().plusMillis(offsetMs)
+ }
+
+ override fun getZone(): ZoneId {
+ throw RuntimeException("Not implemented");
+ }
+
+ override fun withZone(zone: ZoneId): Clock {
+ throw RuntimeException("Not implemented");
+ }
+
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ doReturn(mClock).`when`(mDeps).getClock()
+ doReturn(mFd).`when`(mDeps).createMulticastRoutingSocket()
+ doReturn(mMulticastSocket).`when`(mDeps).createMulticastSocket()
+ doReturn(mIfPhysicalIndex1).`when`(mDeps).getInterfaceIndex(mIfName1)
+ doReturn(mIfPhysicalIndex2).`when`(mDeps).getInterfaceIndex(mIfName2)
+ doReturn(mIfPhysicalIndex3).`when`(mDeps).getInterfaceIndex(mIfName3)
+ doReturn(mNetworkInterface1).`when`(mDeps).getNetworkInterface(mIfPhysicalIndex1)
+ doReturn(mNetworkInterface2).`when`(mDeps).getNetworkInterface(mIfPhysicalIndex2)
+ }
+
+ @After
+ fun tearDown() {
+ mSock.close()
+ }
+
+ // Functions under @Before and @Test run in different threads,
+ // (i.e. androidx.test.runner.AndroidJUnitRunner vs Time-limited test)
+ // MulticastRoutingCoordinatorService requires the jobs are run on the thread looper,
+ // so TestLooper needs to be created inside each test case to install the
+ // correct looper.
+ fun prepareService() {
+ mLooper = TestLooper()
+ val handler = Handler(mLooper.getLooper())
+
+ mService = MulticastRoutingCoordinatorService(handler, mDeps)
+ }
+
+ private fun createEmptyNetworkInterface(): NetworkInterface {
+ val constructor = NetworkInterface::class.java.getDeclaredConstructor()
+ constructor.isAccessible = true
+ return constructor.newInstance()
+ }
+
+ private fun createStructMf6cctl(src: Inet6Address, dst: Inet6Address, iifIdx: Int,
+ oifSet: Set<Int>): StructMf6cctl {
+ return StructMf6cctl(src, dst, iifIdx, oifSet)
+ }
+
+ // Send a MRT6MSG_NOCACHE packet to sock, to indicate a packet has arrived without matching MulticastRoutingCache
+ private fun sendMrt6msgNocachePacket(interfaceVirtualIndex: Int,
+ source: Inet6Address, destination: Inet6Address) {
+ mLooper.dispatchAll() // let MulticastRoutingCoordinatorService handle all msgs first to
+ // apply any possible multicast routing config changes
+ val mrt6Msg = StructMrt6Msg(0 /* mbz must be 0 */, StructMrt6Msg.MRT6MSG_NOCACHE,
+ interfaceVirtualIndex, source, destination)
+ mLooper.getNewExecutor().execute({ mService.handleMulticastNocacheUpcall(mrt6Msg) })
+ mLooper.dispatchAll()
+ }
+
+ private fun applyMulticastForwardNone(fromIf: String, toIf: String) {
+ val configNone = MulticastRoutingConfig.CONFIG_FORWARD_NONE
+
+ mService.applyMulticastRoutingConfig(fromIf, toIf, configNone)
+ }
+
+ private fun applyMulticastForwardMinimumScope(fromIf: String, toIf: String, minScope: Int) {
+ val configMinimumScope = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, minScope).build()
+
+ mService.applyMulticastRoutingConfig(fromIf, toIf, configMinimumScope)
+ }
+
+ private fun applyMulticastForwardSelected(fromIf: String, toIf: String) {
+ val configSelected = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_SELECTED)
+ .addListeningAddress(mGroupAddressScope5).build()
+
+ mService.applyMulticastRoutingConfig(fromIf, toIf, configSelected)
+ }
+
+ @Test
+ fun testConstructor_multicastRoutingSocketIsCreated() {
+ prepareService()
+ verify(mDeps).createMulticastRoutingSocket()
+ }
+
+ @Test
+ fun testMulticastRouting_applyForwardNone() {
+ prepareService()
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ // Both interfaces are not added as multicast routing interfaces
+ verify(mDeps, never()).setsockoptMrt6AddMif(eq(mFd), any())
+ // No MFC should be added for FORWARD_NONE
+ verify(mDeps, never()).setsockoptMrt6AddMfc(eq(mFd), any())
+ assertEquals(MulticastRoutingConfig.CONFIG_FORWARD_NONE,
+ mService.getMulticastRoutingConfig(mIfName1, mIfName2));
+ }
+
+ @Test
+ fun testMulticastRouting_applyForwardMinimumScope() {
+ prepareService()
+
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ mLooper.dispatchAll()
+
+ // No MFC is added for FORWARD_WITH_MIN_SCOPE
+ verify(mDeps, never()).setsockoptMrt6AddMfc(eq(mFd), any())
+ assertEquals(MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE,
+ mService.getMulticastRoutingConfig(mIfName1, mIfName2).getForwardingMode())
+ assertEquals(4, mService.getMulticastRoutingConfig(mIfName1, mIfName2).getMinimumScope())
+ }
+
+ @Test
+ fun testMulticastRouting_addressScopelargerThanMinScope_allowMfcIsAdded() {
+ prepareService()
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ mLooper.dispatchAll()
+ val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2))
+ val mf6cctl = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), oifs)
+
+ // simulate a MRT6MSG_NOCACHE upcall for a packet sent to group address of scope 5
+ sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5)
+
+ // an MFC is added for the packet
+ verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctl))
+ }
+
+ @Test
+ fun testMulticastRouting_addressScopeSmallerThanMinScope_blockingMfcIsAdded() {
+ prepareService()
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4)
+ val mf6cctl = createStructMf6cctl(mSourceAddress, mGroupAddressScope3,
+ mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+ // simulate a MRT6MSG_NOCACHE upcall when a packet should not be forwarded
+ sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope3)
+
+ // a blocking MFC is added
+ verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctl))
+ }
+
+ @Test
+ fun testMulticastRouting_applyForwardSelected_joinsGroup() {
+ prepareService()
+
+ applyMulticastForwardSelected(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+ assertEquals(MulticastRoutingConfig.FORWARD_SELECTED,
+ mService.getMulticastRoutingConfig(mIfName1, mIfName2).getForwardingMode())
+ }
+
+ @Test
+ fun testMulticastRouting_addListeningAddressInForwardSelected_joinsGroup() {
+ prepareService()
+
+ val configSelectedNoAddress = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_SELECTED).build()
+ mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedNoAddress)
+ mLooper.dispatchAll()
+
+ val configSelectedWithAddresses = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_SELECTED)
+ .addListeningAddress(mGroupAddressScope5)
+ .addListeningAddress(mGroupAddressScope4)
+ .build()
+ mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWithAddresses)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1))
+ }
+
+ @Test
+ fun testMulticastRouting_removeListeningAddressInForwardSelected_leavesGroup() {
+ prepareService()
+ val configSelectedWith2Addresses = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_SELECTED)
+ .addListeningAddress(mGroupAddressScope5)
+ .addListeningAddress(mGroupAddressScope4)
+ .build()
+ mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWith2Addresses)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1))
+
+ // remove the scope4 address
+ val configSelectedWith1Address = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_SELECTED)
+ .addListeningAddress(mGroupAddressScope5)
+ .build()
+ mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWith1Address)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).leaveGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1))
+ verify(mMulticastSocket, never())
+ .leaveGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+ }
+
+ @Test
+ fun testMulticastRouting_fromForwardSelectedToForwardNone_leavesGroup() {
+ prepareService()
+ applyMulticastForwardSelected(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).leaveGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+ assertEquals(MulticastRoutingConfig.CONFIG_FORWARD_NONE,
+ mService.getMulticastRoutingConfig(mIfName1, mIfName2));
+ }
+
+ @Test
+ fun testMulticastRouting_fromFowardSelectedToForwardNone_removesMulticastInterfaces() {
+ prepareService()
+
+ applyMulticastForwardSelected(mIfName1, mIfName2)
+ applyMulticastForwardSelected(mIfName1, mIfName3)
+ mLooper.dispatchAll()
+
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName1))
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName2))
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName3))
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName1))
+ assertNull(mService.getVirtualInterfaceIndex(mIfName2))
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName3))
+ }
+
+ @Test
+ fun testMulticastRouting_addMulticastRoutingInterfaces() {
+ prepareService()
+
+ applyMulticastForwardSelected(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName1))
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName2))
+ assertNotEquals(mService.getVirtualInterfaceIndex(mIfName1),
+ mService.getVirtualInterfaceIndex(mIfName2))
+ }
+
+ @Test
+ fun testMulticastRouting_removeMulticastRoutingInterfaces() {
+ prepareService()
+
+ applyMulticastForwardSelected(mIfName1, mIfName2)
+ mService.removeInterfaceFromMulticastRouting(mIfName1)
+ mLooper.dispatchAll()
+
+ assertNull(mService.getVirtualInterfaceIndex(mIfName1))
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName2))
+ }
+
+ @Test
+ fun testMulticastRouting_applyConfigNone_removesMfc() {
+ prepareService()
+
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ applyMulticastForwardSelected(mIfName1, mIfName3)
+
+ sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5)
+ val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2),
+ mService.getVirtualInterfaceIndex(mIfName3))
+ val oifsUpdate = setOf(mService.getVirtualInterfaceIndex(mIfName3))
+ val mf6cctlAdd = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), oifs)
+ val mf6cctlUpdate = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), oifsUpdate)
+ val mf6cctlDel = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+ verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlAdd))
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlUpdate))
+
+ applyMulticastForwardNone(mIfName1, mIfName3)
+ mLooper.dispatchAll()
+
+ verify(mDeps, timeout(TIMEOUT_MS).times(1)).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel))
+ }
+
+ @Test
+ @LargeTest
+ fun testMulticastRouting_maxNumberOfMfcs() {
+ prepareService()
+
+ // add MFC_MAX_NUMBER_OF_ENTRIES MFCs
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ for (i in 1..MulticastRoutingCoordinatorService.MFC_MAX_NUMBER_OF_ENTRIES) {
+ val groupAddress =
+ Inet6Address.getByName("ff05::" + Integer.toHexString(i)) as Inet6Address
+ sendMrt6msgNocachePacket(0, mSourceAddress, groupAddress)
+ }
+ val mf6cctlDel = createStructMf6cctl(mSourceAddress,
+ Inet6Address.getByName("ff05::1" ) as Inet6Address,
+ mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+ verify(mDeps, times(MulticastRoutingCoordinatorService.MFC_MAX_NUMBER_OF_ENTRIES)).
+ setsockoptMrt6AddMfc(eq(mFd), any())
+ // when number of mfcs reaches the max value, one mfc should be removed
+ verify(mDeps).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel))
+ }
+
+ @Test
+ fun testMulticastRouting_interfaceWithoutActiveConfig_isRemoved() {
+ prepareService()
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ mLooper.dispatchAll()
+ val virtualIndexIf1 = mService.getVirtualInterfaceIndex(mIfName1)
+ val virtualIndexIf2 = mService.getVirtualInterfaceIndex(mIfName2)
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf1))
+ verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf2))
+ }
+
+ @Test
+ fun testMulticastRouting_interfaceWithActiveConfig_isNotRemoved() {
+ prepareService()
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ applyMulticastForwardMinimumScope(mIfName2, mIfName3, 4 /* minScope */)
+ mLooper.dispatchAll()
+ val virtualIndexIf1 = mService.getVirtualInterfaceIndex(mIfName1)
+ val virtualIndexIf2 = mService.getVirtualInterfaceIndex(mIfName2)
+ val virtualIndexIf3 = mService.getVirtualInterfaceIndex(mIfName3)
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf1))
+ verify(mDeps, never()).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf2))
+ verify(mDeps, never()).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf3))
+ }
+
+ @Test
+ fun testMulticastRouting_unusedMfc_isRemovedAfterTimeout() {
+ prepareService()
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5)
+ val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2))
+ val mf6cctlAdd = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), oifs)
+ val mf6cctlDel = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+ // An MFC is added
+ verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlAdd))
+
+ repeat(MulticastRoutingCoordinatorService.MFC_INACTIVE_TIMEOUT_MS /
+ MulticastRoutingCoordinatorService.MFC_INACTIVE_CHECK_INTERVAL_MS + 1) {
+ mClock.fastForward(MulticastRoutingCoordinatorService
+ .MFC_INACTIVE_CHECK_INTERVAL_MS.toLong())
+ mLooper.moveTimeForward(MulticastRoutingCoordinatorService
+ .MFC_INACTIVE_CHECK_INTERVAL_MS.toLong())
+ mLooper.dispatchAll();
+ }
+
+ verify(mDeps).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel))
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
new file mode 100644
index 0000000..44a645a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class NetworkRequestStateInfoTest {
+
+ @Mock
+ private NetworkRequestStateInfo.Dependencies mDependencies;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+ @Test
+ public void testSetNetworkRequestRemoved() {
+ final long nrStartTime = 1L;
+ final long nrEndTime = 101L;
+
+ NetworkRequest notMeteredWifiNetworkRequest = new NetworkRequest(
+ new NetworkCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true),
+ 0, 1, NetworkRequest.Type.REQUEST
+ );
+
+ // This call will be used to calculate NR received time
+ Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrStartTime);
+ NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+ notMeteredWifiNetworkRequest, mDependencies);
+
+ // This call will be used to calculate NR removed time
+ Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrEndTime);
+ networkRequestStateInfo.setNetworkRequestRemoved();
+ assertEquals(
+ nrEndTime - nrStartTime,
+ networkRequestStateInfo.getNetworkRequestDurationMillis());
+ assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(),
+ NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED);
+ }
+
+ @Test
+ public void testCheckInitialState() {
+ NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+ new NetworkRequest(new NetworkCapabilities(), 0, 1, NetworkRequest.Type.REQUEST),
+ mDependencies);
+ assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(),
+ NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED);
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
new file mode 100644
index 0000000..8dc0528
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.testutils.HandlerUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkRequestStateStatsMetricsTest {
+ @Mock
+ private NetworkRequestStateStatsMetrics.Dependencies mNRStateStatsDeps;
+ @Mock
+ private NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+ @Captor
+ private ArgumentCaptor<Handler> mHandlerCaptor;
+ @Captor
+ private ArgumentCaptor<Integer> mMessageWhatCaptor;
+
+ private NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+ private HandlerThread mHandlerThread;
+ private static final int TEST_REQUEST_ID = 10;
+ private static final int TEST_PACKAGE_UID = 20;
+ private static final int TIMEOUT_MS = 30_000;
+ private static final NetworkRequest NOT_METERED_WIFI_NETWORK_REQUEST = new NetworkRequest(
+ new NetworkCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET, false)
+ .setRequestorUid(TEST_PACKAGE_UID),
+ 0, TEST_REQUEST_ID, NetworkRequest.Type.REQUEST
+ );
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mHandlerThread = new HandlerThread("NetworkRequestStateStatsMetrics");
+ Mockito.when(mNRStateStatsDeps.makeHandlerThread("NetworkRequestStateStatsMetrics"))
+ .thenReturn(mHandlerThread);
+ Mockito.when(mNRStateStatsDeps.getMillisSinceEvent(anyLong())).thenReturn(0L);
+ Mockito.doAnswer(invocation -> {
+ mHandlerCaptor.getValue().sendMessage(
+ Message.obtain(mHandlerCaptor.getValue(), mMessageWhatCaptor.getValue()));
+ return null;
+ }).when(mNRStateStatsDeps).sendMessageDelayed(
+ mHandlerCaptor.capture(), mMessageWhatCaptor.capture(), anyLong());
+ mNetworkRequestStateStatsMetrics = new NetworkRequestStateStatsMetrics(
+ mNRStateStatsDeps, mNRStateInfoDeps);
+ }
+
+ @Test
+ public void testNetworkRequestReceivedRemoved() {
+ final long nrStartTime = 1L;
+ final long nrEndTime = 101L;
+ // This call will be used to calculate NR received time
+ Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrStartTime);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+
+ ArgumentCaptor<NetworkRequestStateInfo> networkRequestStateInfoCaptor =
+ ArgumentCaptor.forClass(NetworkRequestStateInfo.class);
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+ .writeStats(networkRequestStateInfoCaptor.capture());
+
+ NetworkRequestStateInfo nrStateInfoSent = networkRequestStateInfoCaptor.getValue();
+ assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED,
+ nrStateInfoSent.getNetworkRequestStateStatsType());
+ assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+ assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+ assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+ assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+ assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+ assertEquals(0, nrStateInfoSent.getNetworkRequestDurationMillis());
+
+ clearInvocations(mNRStateStatsDeps);
+ // This call will be used to calculate NR removed time
+ Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrEndTime);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+ .writeStats(networkRequestStateInfoCaptor.capture());
+
+ nrStateInfoSent = networkRequestStateInfoCaptor.getValue();
+ assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
+ nrStateInfoSent.getNetworkRequestStateStatsType());
+ assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+ assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+ assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+ assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+ assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+ assertEquals(nrEndTime - nrStartTime, nrStateInfoSent.getNetworkRequestDurationMillis());
+ }
+
+ @Test
+ public void testUnreceivedNetworkRequestRemoved() {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ verify(mNRStateStatsDeps, never())
+ .writeStats(any(NetworkRequestStateInfo.class));
+ }
+
+ @Test
+ public void testNoMessagesWhenNetworkRequestReceived() {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+ .writeStats(any(NetworkRequestStateInfo.class));
+
+ clearInvocations(mNRStateStatsDeps);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ verify(mNRStateStatsDeps, never())
+ .writeStats(any(NetworkRequestStateInfo.class));
+ }
+
+ @Test
+ public void testMessageQueueSizeLimitNotExceeded() {
+ // Imitate many events (MAX_QUEUED_REQUESTS) are coming together at once while
+ // the other event is being processed.
+ final ConditionVariable cv = new ConditionVariable();
+ mHandlerThread.getThreadHandler().post(() -> cv.block());
+ for (int i = 0; i < NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS / 2; i++) {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(new NetworkRequest(
+ new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+ 0, i + 1, NetworkRequest.Type.REQUEST));
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(new NetworkRequest(
+ new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+ 0, i + 1, NetworkRequest.Type.REQUEST));
+ }
+
+ // When event queue is full, all other events should be dropped.
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(new NetworkRequest(
+ new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+ 0, 2 * NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS + 1,
+ NetworkRequest.Type.REQUEST));
+
+ cv.open();
+
+ // Check only first MAX_QUEUED_REQUESTS events are logged.
+ ArgumentCaptor<NetworkRequestStateInfo> networkRequestStateInfoCaptor =
+ ArgumentCaptor.forClass(NetworkRequestStateInfo.class);
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS).times(
+ NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS))
+ .writeStats(networkRequestStateInfoCaptor.capture());
+ for (int i = 0; i < NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS; i++) {
+ NetworkRequestStateInfo nrStateInfoSent =
+ networkRequestStateInfoCaptor.getAllValues().get(i);
+ assertEquals(i / 2 + 1, nrStateInfoSent.getRequestId());
+ assertEquals(
+ (i % 2 == 0)
+ ? NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED
+ : NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
+ nrStateInfoSent.getNetworkRequestStateStatsType());
+ }
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
index 12758c6..4e15d5f 100644
--- a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
@@ -18,14 +18,17 @@
import android.net.INetd
import android.os.Build
+import android.util.Log
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
-import kotlin.test.assertFailsWith
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@@ -46,9 +49,15 @@
inOrder.verify(mNetd).tetherAddForward("from2", "to1")
inOrder.verify(mNetd).ipfwdAddInterfaceForward("from2", "to1")
- assertFailsWith<IllegalStateException> {
- // Can't add the same pair again
+ val hasFailed = AtomicBoolean(false)
+ val prevHandler = Log.setWtfHandler { tag, what, system ->
+ hasFailed.set(true)
+ }
+ tryTest {
mService.addInterfaceForward("from2", "to1")
+ assertTrue(hasFailed.get())
+ } cleanup {
+ Log.setWtfHandler(prevHandler)
}
mService.removeInterfaceForward("from1", "to1")
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index 0c04bff..ee0bd1a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -35,6 +35,7 @@
import java.net.InetSocketAddress
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
+import kotlin.test.assertNotSame
import kotlin.test.assertTrue
import org.junit.After
import org.junit.Before
@@ -213,7 +214,12 @@
packetHandler.handlePacket(query, query.size, src)
val packetCaptor = ArgumentCaptor.forClass(MdnsPacket::class.java)
- verify(repository).getReply(packetCaptor.capture(), eq(src))
+ val srcCaptor = ArgumentCaptor.forClass(InetSocketAddress::class.java)
+ verify(repository).getReply(packetCaptor.capture(), srcCaptor.capture())
+
+ assertEquals(src, srcCaptor.value)
+ assertNotSame(src, srcCaptor.value, "src will be reused by the packetHandler, references " +
+ "to it should not be used outside of handlePacket.")
packetCaptor.value.let {
assertEquals(1, it.questions.size)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index ad30ce0..9474464 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -101,11 +101,17 @@
private SocketCallback expectSocketCallback(MdnsServiceBrowserListener listener,
Network requestedNetwork) {
+ return expectSocketCallback(listener, requestedNetwork, mSocketCreationCallback,
+ 1 /* requestSocketCount */);
+ }
+
+ private SocketCallback expectSocketCallback(MdnsServiceBrowserListener listener,
+ Network requestedNetwork, SocketCreationCallback callback, int requestSocketCount) {
final ArgumentCaptor<SocketCallback> callbackCaptor =
ArgumentCaptor.forClass(SocketCallback.class);
mHandler.post(() -> mSocketClient.notifyNetworkRequested(
- listener, requestedNetwork, mSocketCreationCallback));
- verify(mProvider, timeout(DEFAULT_TIMEOUT))
+ listener, requestedNetwork, callback));
+ verify(mProvider, timeout(DEFAULT_TIMEOUT).times(requestSocketCount))
.requestSocket(eq(requestedNetwork), callbackCaptor.capture());
return callbackCaptor.getValue();
}
@@ -365,4 +371,40 @@
callback.onInterfaceDestroyed(otherSocketKey, otherSocket);
verify(mSocketCreationCallback).onSocketDestroyed(otherSocketKey);
}
+
+ @Test
+ public void testSocketDestroyed_MultipleCallbacks() {
+ final MdnsInterfaceSocket socket2 = mock(MdnsInterfaceSocket.class);
+ final SocketKey socketKey2 = new SocketKey(1001 /* interfaceIndex */);
+ final SocketCreationCallback creationCallback1 = mock(SocketCreationCallback.class);
+ final SocketCreationCallback creationCallback2 = mock(SocketCreationCallback.class);
+ final SocketCreationCallback creationCallback3 = mock(SocketCreationCallback.class);
+ final SocketCallback callback1 = expectSocketCallback(
+ mock(MdnsServiceBrowserListener.class), mNetwork, creationCallback1,
+ 1 /* requestSocketCount */);
+ final SocketCallback callback2 = expectSocketCallback(
+ mock(MdnsServiceBrowserListener.class), mNetwork, creationCallback2,
+ 2 /* requestSocketCount */);
+ final SocketCallback callback3 = expectSocketCallback(
+ mock(MdnsServiceBrowserListener.class), null /* requestedNetwork */,
+ creationCallback3, 1 /* requestSocketCount */);
+
+ doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+ callback1.onSocketCreated(mSocketKey, mSocket, List.of());
+ callback2.onSocketCreated(mSocketKey, mSocket, List.of());
+ callback3.onSocketCreated(mSocketKey, mSocket, List.of());
+ callback3.onSocketCreated(socketKey2, socket2, List.of());
+ verify(creationCallback1).onSocketCreated(mSocketKey);
+ verify(creationCallback2).onSocketCreated(mSocketKey);
+ verify(creationCallback3).onSocketCreated(mSocketKey);
+ verify(creationCallback3).onSocketCreated(socketKey2);
+
+ callback1.onInterfaceDestroyed(mSocketKey, mSocket);
+ callback2.onInterfaceDestroyed(mSocketKey, mSocket);
+ callback3.onInterfaceDestroyed(mSocketKey, mSocket);
+ verify(creationCallback1).onSocketDestroyed(mSocketKey);
+ verify(creationCallback2).onSocketDestroyed(mSocketKey);
+ verify(creationCallback3).onSocketDestroyed(mSocketKey);
+ verify(creationCallback3, never()).onSocketDestroyed(socketKey2);
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 85e361d..1edc806 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -22,6 +22,11 @@
import android.os.Build
import android.os.HandlerThread
import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_A
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_AAAA
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_PTR
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_SRV
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_TXT
import com.android.server.connectivity.mdns.MdnsRecordRepository.Dependencies
import com.android.server.connectivity.mdns.MdnsRecordRepository.getReverseDnsAddress
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry
@@ -36,7 +41,9 @@
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
+import kotlin.test.assertNull
import kotlin.test.assertTrue
+import kotlin.test.fail
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -48,6 +55,13 @@
private const val TEST_PORT = 12345
private const val TEST_SUBTYPE = "_subtype"
private const val TEST_SUBTYPE2 = "_subtype2"
+// RFC6762 10. Resource Record TTL Values and Cache Coherency
+// The recommended TTL value for Multicast DNS resource records with a host name as the resource
+// record's name (e.g., A, AAAA, HINFO) or a host name contained within the resource record's rdata
+// (e.g., SRV, reverse mapping PTR record) SHOULD be 120 seconds. The recommended TTL value for
+// other Multicast DNS resource records is 75 minutes.
+private const val LONG_TTL = 4_500_000L
+private const val SHORT_TTL = 120_000L
private val TEST_HOSTNAME = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
private val TEST_ADDRESSES = listOf(
LinkAddress(parseNumericAddress("192.0.2.111"), 24),
@@ -80,7 +94,6 @@
override fun getInterfaceInetAddresses(iface: NetworkInterface) =
Collections.enumeration(TEST_ADDRESSES.map { it.address })
}
- private val flags = MdnsFeatureFlags.newBuilder().build()
@Before
fun setUp() {
@@ -93,9 +106,19 @@
thread.join()
}
+ private fun makeFlags(
+ includeInetAddressesInProbing: Boolean = false,
+ isKnownAnswerSuppressionEnabled: Boolean = false,
+ unicastReplyEnabled: Boolean = true
+ ) = MdnsFeatureFlags.Builder()
+ .setIncludeInetAddressRecordsInProbing(includeInetAddressesInProbing)
+ .setIsKnownAnswerSuppressionEnabled(isKnownAnswerSuppressionEnabled)
+ .setIsUnicastReplyEnabled(unicastReplyEnabled)
+ .build()
+
@Test
fun testAddServiceAndProbe() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
assertEquals(0, repository.servicesCount)
assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
@@ -120,7 +143,7 @@
assertEquals(MdnsServiceRecord(expectedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
0 /* servicePriority */, 0 /* serviceWeight */,
TEST_PORT, TEST_HOSTNAME), packet.authorityRecords[0])
@@ -129,7 +152,7 @@
@Test
fun testAddAndConflicts() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(NameConflictException::class) {
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1)
@@ -141,7 +164,7 @@
@Test
fun testAddAndUpdates() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
@@ -175,7 +198,7 @@
@Test
fun testInvalidReuseOfServiceId() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
@@ -184,7 +207,7 @@
@Test
fun testHasActiveService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
@@ -201,7 +224,7 @@
@Test
fun testExitAnnouncements() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@@ -231,7 +254,7 @@
@Test
fun testExitAnnouncements_WithSubtypes() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@@ -273,7 +296,7 @@
@Test
fun testExitingServiceReAdded() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
repository.exitService(TEST_SERVICE_ID_1)
@@ -288,7 +311,7 @@
@Test
fun testOnProbingSucceeded() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@@ -390,37 +413,37 @@
true /* cacheFlush */,
120000L /* ttlMillis */,
v4AddrRev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(TEST_HOSTNAME,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
TEST_HOSTNAME,
- intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
+ intArrayOf(TYPE_A, TYPE_AAAA)),
MdnsNsecRecord(v6Addr1Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr1Rev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(v6Addr2Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr2Rev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName,
- intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV))
+ intArrayOf(TYPE_TXT, TYPE_SRV))
), packet.additionalRecords)
}
@Test
fun testGetOffloadPacket() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val serviceType = arrayOf("_testservice", "_tcp", "local")
@@ -482,7 +505,7 @@
@Test
fun testGetReplyCaseInsensitive() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val questionsCaseInSensitive = listOf(
MdnsPointerRecord(arrayOf("_TESTSERVICE", "_TCP", "local"), false /* isUnicast */))
@@ -496,102 +519,281 @@
assertEquals(7, replyCaseInsensitive.additionalAnswers.size)
}
- @Test
- fun testGetReply() {
- doGetReplyTest(queryWithSubtype = false)
+ /**
+ * Creates mDNS query packet with given query names and types.
+ */
+ private fun makeQuery(vararg queries: Pair<Int, Array<String>>): MdnsPacket {
+ val questions = queries.map { (type, name) -> makeQuestionRecord(name, type) }
+ return MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
+ listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ }
+
+ private fun makeQuestionRecord(name: Array<String>, type: Int): MdnsRecord {
+ when (type) {
+ TYPE_PTR -> return MdnsPointerRecord(name, false /* isUnicast */)
+ TYPE_SRV -> return MdnsServiceRecord(name, false /* isUnicast */)
+ TYPE_TXT -> return MdnsTextRecord(name, false /* isUnicast */)
+ TYPE_A, TYPE_AAAA -> return MdnsInetAddressRecord(name, type, false /* isUnicast */)
+ else -> fail("Unexpected question type: $type")
+ }
}
@Test
- fun testGetReply_WithSubtype() {
- doGetReplyTest(queryWithSubtype = true)
- }
-
- private fun doGetReplyTest(queryWithSubtype: Boolean) {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
- val queriedName = if (!queryWithSubtype) arrayOf("_testservice", "_tcp", "local")
- else arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
-
- val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
- val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
- listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ fun testGetReply_singlePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
val reply = repository.getReply(query, src)
assertNotNull(reply)
- // Source address is IPv4
- assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
- assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
-
- // TTLs as per RFC6762 10.
- val longTtl = 4_500_000L
- val shortTtl = 120_000L
- val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
-
assertEquals(listOf(
MdnsPointerRecord(
- queriedName,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- longTtl,
- serviceName),
- ), reply.answers)
-
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
assertEquals(listOf(
- MdnsTextRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- longTtl,
- listOf() /* entries */),
- MdnsServiceRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- 0 /* servicePriority */,
- 0 /* serviceWeight */,
- TEST_PORT,
- TEST_HOSTNAME),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_ADDRESSES[0].address),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_ADDRESSES[1].address),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_ADDRESSES[2].address),
- MdnsNsecRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- longTtl,
- serviceName /* nextDomain */,
- intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
- MdnsNsecRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_HOSTNAME /* nextDomain */,
- intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
- ), reply.additionalAnswers)
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_singleSubtypePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"), 0L, false,
+ LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_duplicatePtrQuestions_doesNotReturnDuplicateRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_multiplePtrQuestionsWithSubtype_doesNotReturnDuplicateRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+ MdnsPointerRecord(
+ arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"),
+ 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_txtQuestion_returnsNoNsecRecord() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(TYPE_TXT to serviceName)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf())),
+ reply.answers)
+ // No NSEC records because the reply doesn't include the SRV record
+ assertTrue(reply.additionalAnswers.isEmpty())
+ }
+
+ @Test
+ fun testGetReply_AAAAQuestionButNoIpv6Address_returnsNsecRecord() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(
+ TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE),
+ listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+
+ val query = makeQuery(TYPE_AAAA to TEST_HOSTNAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertTrue(reply.answers.isEmpty())
+ assertEquals(listOf(
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, LONG_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_ptrAndSrvQuestions_doesNotReturnSrvRecordInAdditionalAnswerSection() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_SRV to serviceName)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+ MdnsServiceRecord(
+ serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME)),
+ reply.answers)
+ assertFalse(reply.additionalAnswers.any { it -> it is MdnsServiceRecord })
+ }
+
+ @Test
+ fun testGetReply_srvTxtAddressQuestions_returnsAllRecordsInAnswerSectionExceptNsec() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_SRV to serviceName,
+ TYPE_TXT to serviceName,
+ TYPE_SRV to serviceName,
+ TYPE_A to TEST_HOSTNAME,
+ TYPE_AAAA to TEST_HOSTNAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_queryWithIpv4Address_replyWithIpv4Address() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+ val srcIpv4 = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val replyIpv4 = repository.getReply(query, srcIpv4)
+
+ assertNotNull(replyIpv4)
+ assertEquals(MdnsConstants.getMdnsIPv4Address(), replyIpv4.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, replyIpv4.destination.port)
+ }
+
+ @Test
+ fun testGetReply_queryWithIpv6Address_replyWithIpv6Address() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+ val srcIpv6 = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+ val replyIpv6 = repository.getReply(query, srcIpv6)
+
+ assertNotNull(replyIpv6)
+ assertEquals(MdnsConstants.getMdnsIPv6Address(), replyIpv6.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, replyIpv6.destination.port)
}
@Test
fun testGetConflictingServices() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
@@ -619,7 +821,7 @@
@Test
fun testGetConflictingServicesCaseInsensitive() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
@@ -647,7 +849,7 @@
@Test
fun testGetConflictingServices_IdenticalService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
@@ -676,7 +878,7 @@
@Test
fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
@@ -705,7 +907,7 @@
@Test
fun testGetServiceRepliedRequestsCount() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
// Verify that there is no packet replied.
assertEquals(MdnsConstants.NO_PACKET,
@@ -730,7 +932,7 @@
@Test
fun testIncludeInetAddressRecordsInProbing() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
- MdnsFeatureFlags.newBuilder().setIncludeInetAddressRecordsInProbing(true).build())
+ makeFlags(includeInetAddressesInProbing = true))
repository.updateAddresses(TEST_ADDRESSES)
assertEquals(0, repository.servicesCount)
assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
@@ -760,7 +962,7 @@
expectedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
0 /* servicePriority */,
0 /* serviceWeight */,
TEST_PORT,
@@ -769,33 +971,386 @@
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[0].address),
MdnsInetAddressRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[1].address),
MdnsInetAddressRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[2].address)
), packet.authorityRecords)
assertContentEquals(intArrayOf(TEST_SERVICE_ID_1), repository.clearServices())
}
+
+ private fun doGetReplyWithAnswersTest(
+ questions: List<MdnsRecord>,
+ knownAnswers: List<MdnsRecord>,
+ replyAnswers: List<MdnsRecord>,
+ additionalAnswers: List<MdnsRecord>,
+ expectReply: Boolean
+ ) {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+ makeFlags(isKnownAnswerSuppressionEnabled = true))
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val query = MdnsPacket(0 /* flags */, questions, knownAnswers,
+ listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val reply = repository.getReply(query, src)
+
+ if (!expectReply) {
+ assertNull(reply)
+ return
+ }
+
+ assertNotNull(reply)
+ // Source address is IPv4
+ assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
+ assertEquals(replyAnswers, reply.answers)
+ assertEquals(additionalAnswers, reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+ doGetReplyWithAnswersTest(questions, knownAnswers, listOf() /* replyAnswers */,
+ listOf() /* additionalAnswers */, false /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_TtlLessThanHalf() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ (LONG_TTL / 2 - 1000L),
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+ val replyAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ serviceName))
+ val additionalAnswers = listOf(
+ MdnsTextRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ listOf() /* entries */),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
+ true /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnotherAnswer() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ arrayOf("MyOtherTestService", "_testservice", "_tcp", "local")))
+ val replyAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ serviceName))
+ val additionalAnswers = listOf(
+ MdnsTextRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ listOf() /* entries */),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
+ true /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_MultiQuestions() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(
+ MdnsPointerRecord(queriedName, false /* isUnicast */),
+ MdnsServiceRecord(serviceName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL - 1000L,
+ serviceName))
+ val replyAnswers = listOf(MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ SHORT_TTL /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME))
+ val additionalAnswers = listOf(
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
+ true /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_MultiQuestions_NoReply() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(
+ MdnsPointerRecord(queriedName, false /* isUnicast */),
+ MdnsServiceRecord(serviceName, false /* isUnicast */))
+ val knownAnswers = listOf(
+ MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL - 1000L,
+ serviceName
+ ),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ SHORT_TTL - 15_000L,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME
+ )
+ )
+ doGetReplyWithAnswersTest(
+ questions, knownAnswers, listOf() /* replyAnswers */,
+ listOf() /* additionalAnswers */, false /* expectReply */
+ )
+ }
+
+ @Test
+ fun testReplyUnicastToQueryUnicastQuestions() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ // Ask for 2 services, only the first one is known and requests unicast reply
+ val questions = listOf(
+ MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */),
+ MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), true /* isUnicast */))
+ val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
+ listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+ // Reply to the question and verify it is sent to the source.
+ val reply = repository.getReply(query, src)
+ assertNotNull(reply)
+ assertEquals(src, reply.destination)
+ }
+
+ @Test
+ fun testReplyMulticastToQueryUnicastAndMulticastMixedQuestions() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply {
+ serviceType = "_otherservice._tcp"
+ serviceName = "OtherTestService"
+ port = TEST_PORT
+ })
+
+ // Ask for 2 services, both are known and only the first one requests unicast reply
+ val questions = listOf(
+ MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */),
+ MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), false /* isUnicast */))
+ val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
+ listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+ // Reply to the question and verify it is sent multicast.
+ val reply = repository.getReply(query, src)
+ assertNotNull(reply)
+ assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
+ }
+
+ @Test
+ fun testReplyMulticastWhenNoUnicastQueryMatches() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ // Ask for 2 services, the first one requests a unicast reply but is unknown
+ val questions = listOf(
+ MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), true /* isUnicast */),
+ MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), false /* isUnicast */))
+ val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
+ listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+ // Reply to the question and verify it is sent multicast.
+ val reply = repository.getReply(query, src)
+ assertNotNull(reply)
+ assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
+ }
+
+ @Test
+ fun testReplyMulticastWhenUnicastFeatureDisabled() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+ makeFlags(unicastReplyEnabled = false))
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ // The service is known and requests unicast reply, but the feature is disabled
+ val questions = listOf(
+ MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */))
+ val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
+ listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+ // Reply to the question and verify it is sent multicast.
+ val reply = repository.getReply(query, src)
+ assertNotNull(reply)
+ assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
+ }
}
private fun MdnsRecordRepository.initWithService(
serviceId: Int,
serviceInfo: NsdServiceInfo,
subtypes: Set<String> = setOf(),
+ addresses: List<LinkAddress> = TEST_ADDRESSES
): AnnouncementInfo {
- updateAddresses(TEST_ADDRESSES)
+ updateAddresses(addresses)
serviceInfo.setSubtypes(subtypes)
+ return addServiceAndFinishProbing(serviceId, serviceInfo)
+}
+
+private fun MdnsRecordRepository.addServiceAndFinishProbing(
+ serviceId: Int,
+ serviceInfo: NsdServiceInfo
+): AnnouncementInfo {
addService(serviceId, serviceInfo)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
new file mode 100644
index 0000000..9e2933f
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.connectivity.mdns
+
+import android.net.InetAddresses
+import android.net.LinkAddress
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Message
+import com.android.net.module.util.SharedLog
+import com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import java.net.InetSocketAddress
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val TEST_PORT = 12345
+private const val DEFAULT_TIMEOUT_MS = 2000L
+private const val LONG_TTL = 4_500_000L
+private const val SHORT_TTL = 120_000L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class MdnsReplySenderTest {
+ private val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ private val serviceType = arrayOf("_testservice", "_tcp", "local")
+ private val hostname = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
+ private val hostAddresses = listOf(
+ LinkAddress(InetAddresses.parseNumericAddress("192.0.2.111"), 24),
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::111"), 64),
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::222"), 64))
+ private val answers = listOf(
+ MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+ LONG_TTL, serviceName))
+ private val additionalAnswers = listOf(
+ MdnsTextRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
+ listOf() /* entries */),
+ MdnsServiceRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, 0 /* servicePriority */, 0 /* serviceWeight */, TEST_PORT, hostname),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[0].address),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[1].address),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[2].address),
+ MdnsNsecRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */, SHORT_TTL,
+ hostname /* nextDomain */, intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ private val thread = HandlerThread(MdnsReplySenderTest::class.simpleName)
+ private val socket = mock(MdnsInterfaceSocket::class.java)
+ private val buffer = ByteArray(1500)
+ private val sharedLog = SharedLog(MdnsReplySenderTest::class.simpleName)
+ private val deps = mock(MdnsReplySender.Dependencies::class.java)
+ private val handler by lazy { Handler(thread.looper) }
+ private val replySender by lazy {
+ MdnsReplySender(thread.looper, socket, buffer, sharedLog, false /* enableDebugLog */, deps)
+ }
+
+ @Before
+ fun setUp() {
+ thread.start()
+ doReturn(true).`when`(socket).hasJoinedIpv4()
+ doReturn(true).`when`(socket).hasJoinedIpv6()
+ }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ private fun <T> runningOnHandlerAndReturn(functor: (() -> T)): T {
+ val future = CompletableFuture<T>()
+ handler.post {
+ future.complete(functor())
+ }
+ return future.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ private fun sendNow(packet: MdnsPacket, destination: InetSocketAddress):
+ Unit = runningOnHandlerAndReturn { replySender.sendNow(packet, destination) }
+
+ private fun queueReply(reply: MdnsReplyInfo):
+ Unit = runningOnHandlerAndReturn { replySender.queueReply(reply) }
+
+ @Test
+ fun testSendNow() {
+ val packet = MdnsPacket(0x8400,
+ listOf() /* questions */,
+ answers,
+ listOf() /* authorityRecords */,
+ additionalAnswers)
+ sendNow(packet, IPV4_SOCKET_ADDR)
+ verify(socket).send(argThat{ it.socketAddress.equals(IPV4_SOCKET_ADDR) })
+ }
+
+ @Test
+ fun testQueueReply() {
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 20L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR)
+ val handlerCaptor = ArgumentCaptor.forClass(Handler::class.java)
+ val messageCaptor = ArgumentCaptor.forClass(Message::class.java)
+ queueReply(reply)
+ verify(deps).sendMessageDelayed(handlerCaptor.capture(), messageCaptor.capture(), eq(20L))
+
+ val realHandler = handlerCaptor.value
+ val delayMessage = messageCaptor.value
+ realHandler.sendMessage(delayMessage)
+ verify(socket, timeout(DEFAULT_TIMEOUT_MS)).send(argThat{
+ it.socketAddress.equals(IPV4_SOCKET_ADDR)
+ })
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
index 58f20a9..a5d5297 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
@@ -23,11 +23,12 @@
import androidx.test.filters.SmallTest
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.Test
-import org.junit.runner.RunWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
index c26ec53..8155fd0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -38,6 +38,7 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.S_V2) // Bpf only supports in T+.
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
index 572c7bb..5c29e3a 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
@@ -30,6 +30,7 @@
private const val LONG_TIMEOUT_MS = 5_000
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
index a753922..94c68c0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -22,8 +22,8 @@
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
import android.net.NetworkScore
-import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
import android.os.Build
import androidx.test.filters.SmallTest
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
@@ -33,6 +33,7 @@
import org.junit.Test
import org.junit.runner.RunWith
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index 6add6b9..cb98454 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -33,6 +33,7 @@
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.Available
import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertFailsWith
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,7 +42,6 @@
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.timeout
-import kotlin.test.assertFailsWith
private const val TIMEOUT_MS = 2_000L
private const val NO_CALLBACK_TIMEOUT_MS = 200L
@@ -51,6 +51,7 @@
private fun defaultLnc() = FromS(LocalNetworkConfig.Builder().build())
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index dd0706b..c1730a4 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -20,6 +20,8 @@
import android.net.LinkAddress
import android.net.LinkProperties
import android.net.LocalNetworkConfig
+import android.net.MulticastRoutingConfig
+import android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_DUN
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
@@ -42,14 +44,15 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.eq
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.timeout
import org.mockito.Mockito.verify
-import kotlin.test.assertFailsWith
private const val TIMEOUT_MS = 200L
private const val MEDIUM_TIMEOUT_MS = 1_000L
@@ -79,9 +82,28 @@
NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
)
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
class CSLocalAgentTests : CSTest() {
+ val multicastRoutingConfigMinScope =
+ MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, 4)
+ .build();
+ val multicastRoutingConfigSelected =
+ MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_SELECTED)
+ .build();
+ val upstreamSelectorAny = NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build()
+ val upstreamSelectorWifi = NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .addTransportType(TRANSPORT_WIFI)
+ .build()
+ val upstreamSelectorCell = NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .build()
+
@Test
fun testBadAgents() {
deps.setBuildSdk(VERSION_V)
@@ -177,6 +199,266 @@
localAgent.disconnect()
}
+ private fun createLocalAgent(name: String, localNetworkConfig: FromS<LocalNetworkConfig>):
+ CSAgentWrapper {
+ val localAgent = Agent(
+ nc = nc(TRANSPORT_THREAD, NET_CAPABILITY_LOCAL_NETWORK),
+ lp = lp(name),
+ lnc = localNetworkConfig,
+ )
+ return localAgent
+ }
+
+ private fun createWifiAgent(name: String): CSAgentWrapper {
+ return Agent(score = keepScore(), lp = lp(name),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ }
+
+ private fun createCellAgent(name: String): CSAgentWrapper {
+ return Agent(score = keepScore(), lp = lp(name),
+ nc = nc(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET))
+ }
+
+ private fun sendLocalNetworkConfig(localAgent: CSAgentWrapper,
+ upstreamSelector: NetworkRequest?, upstreamConfig: MulticastRoutingConfig,
+ downstreamConfig: MulticastRoutingConfig) {
+ val newLnc = LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelector)
+ .setUpstreamMulticastRoutingConfig(upstreamConfig)
+ .setDownstreamMulticastRoutingConfig(downstreamConfig)
+ .build()
+ localAgent.sendLocalNetworkConfig(newLnc)
+ }
+
+ @Test
+ fun testMulticastRoutingConfig() {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+ val inOrder = inOrder(multicastRoutingCoordinatorService)
+
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelectorWifi)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()
+ )
+ val localAgent = createLocalAgent("local0", lnc)
+ localAgent.connect()
+
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+ val wifiAgent = createWifiAgent("wifi0")
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local0", multicastRoutingConfigSelected)
+
+ wifiAgent.disconnect()
+
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
+
+ localAgent.disconnect()
+ }
+
+ @Test
+ fun testMulticastRoutingConfig_2LocalNetworks() {
+ deps.setBuildSdk(VERSION_V)
+ val inOrder = inOrder(multicastRoutingCoordinatorService)
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelectorWifi)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()
+ )
+ val localAgent0 = createLocalAgent("local0", lnc)
+ localAgent0.connect()
+
+ val wifiAgent = createWifiAgent("wifi0")
+ wifiAgent.connect()
+ waitForIdle()
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local0", multicastRoutingConfigSelected)
+
+ val localAgent1 = createLocalAgent("local1", lnc)
+ localAgent1.connect()
+ waitForIdle()
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local1", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local1", multicastRoutingConfigSelected)
+
+ localAgent0.disconnect()
+ localAgent1.disconnect()
+ wifiAgent.disconnect()
+ }
+
+ @Test
+ fun testMulticastRoutingConfig_UpstreamNetworkCellToWifi() {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(), cb)
+ val inOrder = inOrder(multicastRoutingCoordinatorService)
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelectorAny)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()
+ )
+ val localAgent = createLocalAgent("local0", lnc)
+ val wifiAgent = createWifiAgent("wifi0")
+ val cellAgent = createCellAgent("cell0")
+
+ localAgent.connect()
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+ cellAgent.connect()
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == cellAgent.network
+ }
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "cell0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "cell0", "local0", multicastRoutingConfigSelected)
+
+ wifiAgent.connect()
+
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ // upstream should have been switched to wifi
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("local0", "cell0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("cell0", "local0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local0", multicastRoutingConfigSelected)
+
+ localAgent.disconnect()
+ cellAgent.disconnect()
+ wifiAgent.disconnect()
+ }
+
+ @Test
+ fun testMulticastRoutingConfig_UpstreamSelectorCellToWifi() {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(), cb)
+ val inOrder = inOrder(multicastRoutingCoordinatorService)
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelectorCell)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()
+ )
+ val localAgent = createLocalAgent("local0", lnc)
+ val wifiAgent = createWifiAgent("wifi0")
+ val cellAgent = createCellAgent("cell0")
+
+ localAgent.connect()
+ cellAgent.connect()
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == cellAgent.network
+ }
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "cell0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "cell0", "local0", multicastRoutingConfigSelected)
+
+ sendLocalNetworkConfig(localAgent, upstreamSelectorWifi, multicastRoutingConfigMinScope,
+ multicastRoutingConfigSelected)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ // upstream should have been switched to wifi
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("local0", "cell0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("cell0", "local0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local0", multicastRoutingConfigSelected)
+
+ localAgent.disconnect()
+ cellAgent.disconnect()
+ wifiAgent.disconnect()
+ }
+
+ @Test
+ fun testMulticastRoutingConfig_UpstreamSelectorWifiToNull() {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(), cb)
+ val inOrder = inOrder(multicastRoutingCoordinatorService)
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelectorWifi)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()
+ )
+ val localAgent = createLocalAgent("local0", lnc)
+ localAgent.connect()
+ val wifiAgent = createWifiAgent("wifi0")
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local0", multicastRoutingConfigSelected)
+
+ sendLocalNetworkConfig(localAgent, null, multicastRoutingConfigMinScope,
+ multicastRoutingConfigSelected)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == null
+ }
+
+ // upstream should have been switched to null
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService, never()).applyMulticastRoutingConfig(
+ eq("local0"), any(), eq(multicastRoutingConfigMinScope))
+ inOrder.verify(multicastRoutingCoordinatorService, never()).applyMulticastRoutingConfig(
+ any(), eq("local0"), eq(multicastRoutingConfigSelected))
+
+ localAgent.disconnect()
+ wifiAgent.disconnect()
+ }
+
+
@Test
fun testUnregisterUpstreamAfterReplacement_SameIfaceName() {
doTestUnregisterUpstreamAfterReplacement(true)
@@ -196,11 +478,10 @@
val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
lp = lp("local0"),
lnc = FromS(LocalNetworkConfig.Builder()
- .setUpstreamSelector(NetworkRequest.Builder()
- .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
- .addTransportType(TRANSPORT_WIFI)
- .build())
- .build()),
+ .setUpstreamSelector(upstreamSelectorWifi)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()),
score = FromS(NetworkScore.Builder()
.setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
.build())
@@ -219,10 +500,15 @@
}
clearInvocations(netd)
- val inOrder = inOrder(netd)
+ clearInvocations(multicastRoutingCoordinatorService)
+ val inOrder = inOrder(netd, multicastRoutingCoordinatorService)
wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
waitForIdle()
inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi0")
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
inOrder.verify(netd).networkDestroy(wifiAgent.network.netId)
val wifiIface2 = if (sameIfaceName) "wifi0" else "wifi1"
@@ -235,9 +521,16 @@
cb.expect<Lost> { it.network == wifiAgent.network }
inOrder.verify(netd).ipfwdAddInterfaceForward("local0", wifiIface2)
- if (sameIfaceName) {
- inOrder.verify(netd, never()).ipfwdRemoveInterfaceForward(any(), any())
- }
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", wifiIface2, multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ wifiIface2, "local0", multicastRoutingConfigSelected)
+
+ inOrder.verify(netd, never()).ipfwdRemoveInterfaceForward(any(), any())
+ inOrder.verify(multicastRoutingCoordinatorService, never())
+ .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService, never())
+ .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
index 526ec9d..df0a2cc 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
@@ -63,6 +63,7 @@
private const val PACKAGE_UID = 123
private const val TIMEOUT_MS = 250L
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
new file mode 100644
index 0000000..35f8ae5
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.NetworkCapabilities
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkRequestStateStatsMetricsTests : CSTest() {
+ private val CELL_INTERNET_NOT_METERED_NC = NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ .build().setRequestorUidAndPackageName(Process.myUid(), context.getPackageName())
+
+ private val CELL_INTERNET_NOT_METERED_NR = NetworkRequest.Builder()
+ .setCapabilities(CELL_INTERNET_NOT_METERED_NC).build()
+
+ @Before
+ fun setup() {
+ waitForIdle()
+ clearInvocations(networkRequestStateStatsMetrics)
+ }
+
+ @Test
+ fun testRequestTypeNRProduceMetrics() {
+ cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+ waitForIdle()
+
+ verify(networkRequestStateStatsMetrics).onNetworkRequestReceived(
+ argThat{req -> req.networkCapabilities.equals(
+ CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+ }
+
+ @Test
+ fun testListenTypeNRProduceNoMetrics() {
+ cm.registerNetworkCallback(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+ waitForIdle()
+ verify(networkRequestStateStatsMetrics, never()).onNetworkRequestReceived(any())
+ }
+
+ @Test
+ fun testRemoveRequestTypeNRProduceMetrics() {
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, cb)
+
+ waitForIdle()
+ clearInvocations(networkRequestStateStatsMetrics)
+
+ cm.unregisterNetworkCallback(cb)
+ waitForIdle()
+ verify(networkRequestStateStatsMetrics).onNetworkRequestRemoved(
+ argThat{req -> req.networkCapabilities.equals(
+ CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+ }
+}
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 958c4f2..0708669 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -61,14 +61,18 @@
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
import com.android.server.connectivity.ClatCoordinator
import com.android.server.connectivity.ConnectivityFlags
+import com.android.server.connectivity.MulticastRoutingCoordinatorService
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
+import com.android.server.connectivity.NetworkRequestStateStatsMetrics
import com.android.server.connectivity.ProxyTracker
+import com.android.server.connectivity.RoutingCoordinatorService
import com.android.testutils.visibleOnHandlerThread
import com.android.testutils.waitForIdle
import java.util.concurrent.Executors
import kotlin.test.assertNull
import kotlin.test.fail
+import org.junit.After
import org.mockito.AdditionalAnswers.delegatesTo
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
@@ -157,8 +161,10 @@
val netd = mock<INetd>()
val bpfNetMaps = mock<BpfNetMaps>()
val clatCoordinator = mock<ClatCoordinator>()
+ val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
- val alarmManager = makeMockAlarmManager()
+ val alrmHandlerThread = HandlerThread("TestAlarmManager").also { it.start() }
+ val alarmManager = makeMockAlarmManager(alrmHandlerThread)
val systemConfigManager = makeMockSystemConfigManager()
val batteryStats = mock<IBatteryStats>()
val batteryManager = BatteryStatsManager(batteryStats)
@@ -166,11 +172,21 @@
doReturn(true).`when`(it).isDataCapable()
}
+ val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
+
val deps = CSDeps()
val service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
val cm = ConnectivityManager(context, service)
val csHandler = Handler(csHandlerThread.looper)
+ @After
+ fun tearDown() {
+ csHandlerThread.quitSafely()
+ csHandlerThread.join()
+ alrmHandlerThread.quitSafely()
+ alrmHandlerThread.join()
+ }
+
inner class CSDeps : ConnectivityService.Dependencies() {
override fun getResources(ctx: Context) = connResources
override fun getBpfNetMaps(context: Context, netd: INetd) = this@CSTest.bpfNetMaps
@@ -179,6 +195,8 @@
override fun makeHandlerThread(tag: String) = csHandlerThread
override fun makeProxyTracker(context: Context, connServiceHandler: Handler) = proxyTracker
+ override fun makeMulticastRoutingCoordinatorService(handler: Handler) =
+ this@CSTest.multicastRoutingCoordinatorService
override fun makeCarrierPrivilegeAuthenticator(
context: Context,
@@ -197,6 +215,9 @@
MultinetworkPolicyTracker(c, h, r,
MultinetworkPolicyTrackerTestDependencies(connResources.get()))
+ override fun makeNetworkRequestStateStatsMetrics(c: Context) =
+ this@CSTest.networkRequestStateStatsMetrics
+
// All queried features must be mocked, because the test cannot hold the
// READ_DEVICE_CONFIG permission and device config utils use static methods for
// checking permissions.
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 c1828b2..8ff790c 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -53,6 +53,7 @@
import com.android.modules.utils.build.SdkLevel
import com.android.server.ConnectivityService.Dependencies
import com.android.server.connectivity.ConnectivityResources
+import kotlin.test.fail
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
@@ -64,7 +65,6 @@
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn
-import kotlin.test.fail
internal inline fun <reified T> mock() = Mockito.mock(T::class.java)
internal inline fun <reified T> any() = any(T::class.java)
@@ -128,8 +128,8 @@
}
private val UNREASONABLY_LONG_ALARM_WAIT_MS = 1000
-internal fun makeMockAlarmManager() = mock<AlarmManager>().also { am ->
- val alrmHdlr = HandlerThread("TestAlarmManager").also { it.start() }.threadHandler
+internal fun makeMockAlarmManager(handlerThread: HandlerThread) = mock<AlarmManager>().also { am ->
+ val alrmHdlr = handlerThread.threadHandler
doAnswer {
val (_, date, _, wakeupMsg, handler) = it.arguments
wakeupMsg as WakeupMessage
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 7a4dfed..a5fee5b 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -67,6 +67,8 @@
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME;
@@ -83,6 +85,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -128,6 +131,7 @@
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
import android.util.Pair;
import androidx.annotation.Nullable;
@@ -169,7 +173,6 @@
import java.io.File;
import java.io.FileDescriptor;
-import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
@@ -284,9 +287,14 @@
private @Mock PersistentInt mImportLegacyAttemptsCounter;
private @Mock PersistentInt mImportLegacySuccessesCounter;
private @Mock PersistentInt mImportLegacyFallbacksCounter;
+ private int mFastDataInputTargetAttempts = 0;
+ private @Mock PersistentInt mFastDataInputSuccessesCounter;
+ private @Mock PersistentInt mFastDataInputFallbacksCounter;
+ private String mCompareStatsResult = null;
private @Mock Resources mResources;
private Boolean mIsDebuggable;
private HandlerThread mObserverHandlerThread;
+ final TestDependencies mDeps = new TestDependencies();
private class MockContext extends BroadcastInterceptingContext {
private final Context mBaseContext;
@@ -369,7 +377,6 @@
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mHandlerThread = new HandlerThread("NetworkStatsServiceTest-HandlerThread");
- final NetworkStatsService.Dependencies deps = makeDependencies();
// Create a separate thread for observers to run on. This thread cannot be the same
// as the handler thread, because the observer callback is fired on this thread, and
// it should not be blocked by client code. Additionally, creating the observers
@@ -384,7 +391,7 @@
}
};
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
- mClock, mSettings, mStatsFactory, statsObservers, deps);
+ mClock, mSettings, mStatsFactory, statsObservers, mDeps);
mElapsedRealtime = 0L;
@@ -423,132 +430,150 @@
mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
}
- @NonNull
- private NetworkStatsService.Dependencies makeDependencies() {
- return new NetworkStatsService.Dependencies() {
- @Override
- public File getLegacyStatsDir() {
- return mLegacyStatsDir;
- }
+ class TestDependencies extends NetworkStatsService.Dependencies {
+ private int mCompareStatsInvocation = 0;
- @Override
- public File getOrCreateStatsDir() {
- return mStatsDir;
- }
+ @Override
+ public File getLegacyStatsDir() {
+ return mLegacyStatsDir;
+ }
- @Override
- public boolean getStoreFilesInApexData() {
- return mStoreFilesInApexData;
- }
+ @Override
+ public File getOrCreateStatsDir() {
+ return mStatsDir;
+ }
- @Override
- public int getImportLegacyTargetAttempts() {
- return mImportLegacyTargetAttempts;
- }
+ @Override
+ public boolean getStoreFilesInApexData() {
+ return mStoreFilesInApexData;
+ }
- @Override
- public PersistentInt createPersistentCounter(@androidx.annotation.NonNull Path dir,
- @androidx.annotation.NonNull String name) throws IOException {
- switch (name) {
- case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
- return mImportLegacyAttemptsCounter;
- case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
- return mImportLegacySuccessesCounter;
- case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
- return mImportLegacyFallbacksCounter;
- default:
- throw new IllegalArgumentException("Unknown counter name: " + name);
- }
- }
+ @Override
+ public int getImportLegacyTargetAttempts() {
+ return mImportLegacyTargetAttempts;
+ }
- @Override
- public NetworkStatsCollection readPlatformCollection(
- @NonNull String prefix, long bucketDuration) {
- return mPlatformNetworkStatsCollection.get(prefix);
- }
+ @Override
+ public int getUseFastDataInputTargetAttempts() {
+ return mFastDataInputTargetAttempts;
+ }
- @Override
- public HandlerThread makeHandlerThread() {
- return mHandlerThread;
- }
+ @Override
+ public String compareStats(NetworkStatsCollection a, NetworkStatsCollection b,
+ boolean allowKeyChange) {
+ mCompareStatsInvocation++;
+ return mCompareStatsResult;
+ }
- @Override
- public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
- @NonNull Context context, @NonNull Executor executor,
- @NonNull NetworkStatsService service) {
+ int getCompareStatsInvocation() {
+ return mCompareStatsInvocation;
+ }
- return mNetworkStatsSubscriptionsMonitor;
+ @Override
+ public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name) {
+ switch (name) {
+ case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
+ return mImportLegacyAttemptsCounter;
+ case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
+ return mImportLegacySuccessesCounter;
+ case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
+ return mImportLegacyFallbacksCounter;
+ case NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME:
+ return mFastDataInputSuccessesCounter;
+ case NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME:
+ return mFastDataInputFallbacksCounter;
+ default:
+ throw new IllegalArgumentException("Unknown counter name: " + name);
}
+ }
- @Override
- public ContentObserver makeContentObserver(Handler handler,
- NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
- mHandler = handler;
- return mContentObserver = super.makeContentObserver(handler, settings, monitor);
- }
+ @Override
+ public NetworkStatsCollection readPlatformCollection(
+ @NonNull String prefix, long bucketDuration) {
+ return mPlatformNetworkStatsCollection.get(prefix);
+ }
- @Override
- public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
- return mLocationPermissionChecker;
- }
+ @Override
+ public HandlerThread makeHandlerThread() {
+ return mHandlerThread;
+ }
- @Override
- public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
- @NonNull Context ctx, @NonNull Handler handler) {
- return mBpfInterfaceMapUpdater;
- }
+ @Override
+ public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
+ @NonNull Context context, @NonNull Executor executor,
+ @NonNull NetworkStatsService service) {
- @Override
- public IBpfMap<S32, U8> getUidCounterSetMap() {
- return mUidCounterSetMap;
- }
+ return mNetworkStatsSubscriptionsMonitor;
+ }
- @Override
- public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
- return mCookieTagMap;
- }
+ @Override
+ public ContentObserver makeContentObserver(Handler handler,
+ NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
+ mHandler = handler;
+ return mContentObserver = super.makeContentObserver(handler, settings, monitor);
+ }
- @Override
- public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
- return mStatsMapA;
- }
+ @Override
+ public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
+ return mLocationPermissionChecker;
+ }
- @Override
- public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
- return mStatsMapB;
- }
+ @Override
+ public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
+ @NonNull Context ctx, @NonNull Handler handler) {
+ return mBpfInterfaceMapUpdater;
+ }
- @Override
- public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
- return mAppUidStatsMap;
- }
+ @Override
+ public IBpfMap<S32, U8> getUidCounterSetMap() {
+ return mUidCounterSetMap;
+ }
- @Override
- public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
- return mIfaceStatsMap;
- }
+ @Override
+ public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
+ return mCookieTagMap;
+ }
- @Override
- public boolean isDebuggable() {
- return mIsDebuggable == Boolean.TRUE;
- }
+ @Override
+ public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
+ return mStatsMapA;
+ }
- @Override
- public BpfNetMaps makeBpfNetMaps(Context ctx) {
- return mBpfNetMaps;
- }
+ @Override
+ public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
+ return mStatsMapB;
+ }
- @Override
- public SkDestroyListener makeSkDestroyListener(
- IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
- return mSkDestroyListener;
- }
+ @Override
+ public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
+ return mAppUidStatsMap;
+ }
- @Override
- public boolean supportEventLogger(@NonNull Context cts) {
- return true;
- }
- };
+ @Override
+ public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
+ return mIfaceStatsMap;
+ }
+
+ @Override
+ public boolean isDebuggable() {
+ return mIsDebuggable == Boolean.TRUE;
+ }
+
+ @Override
+ public BpfNetMaps makeBpfNetMaps(Context ctx) {
+ return mBpfNetMaps;
+ }
+
+ @Override
+ public SkDestroyListener makeSkDestroyListener(
+ IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
+ return mSkDestroyListener;
+ }
+
+ @Override
+ public boolean supportEventLogger(@NonNull Context cts) {
+ return true;
+ }
}
@After
@@ -2166,6 +2191,71 @@
}
@Test
+ public void testAdoptFastDataInput_featureDisabled() throws Exception {
+ // Boot through serviceReady() with flag disabled, verify the persistent
+ // counters are not increased.
+ mFastDataInputTargetAttempts = 0;
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ assertEquals(0, mDeps.getCompareStatsInvocation());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noRetryAfterFail() throws Exception {
+ // Boot through serviceReady(), verify the service won't retry unexpectedly
+ // since the target attempt remains the same.
+ mFastDataInputTargetAttempts = 1;
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(1).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noRetryAfterSuccess() throws Exception {
+ // Boot through serviceReady(), verify the service won't retry unexpectedly
+ // since the target attempt remains the same.
+ mFastDataInputTargetAttempts = 1;
+ doReturn(1).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_hasDiff() throws Exception {
+ // Boot through serviceReady() with flag enabled and assumes the stats are
+ // failed to compare, verify the fallbacks counter is increased.
+ mockDefaultSettings();
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mFastDataInputTargetAttempts = 1;
+ mCompareStatsResult = "Has differences";
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter).set(1);
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noDiff() throws Exception {
+ // Boot through serviceReady() with target attempts increased,
+ // assumes there was a previous failure,
+ // and assumes the stats are successfully compared,
+ // verify the successes counter is increased.
+ mFastDataInputTargetAttempts = 2;
+ doReturn(1).when(mFastDataInputFallbacksCounter).get();
+ mCompareStatsResult = null;
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter).set(1);
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
public void testStatsFactoryRemoveUids() throws Exception {
// pretend that network comes online
mockDefaultSettings();
@@ -2230,7 +2320,8 @@
final DropBoxManager dropBox = mock(DropBoxManager.class);
return new NetworkStatsRecorder(new FileRotator(
directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
- observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError);
+ observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError,
+ false /* useFastDataInput */, directory);
}
private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
@@ -2713,4 +2804,16 @@
final String dump = getDump();
assertDumpContains(dump, pollReasonNameOf(POLL_REASON_RAT_CHANGED));
}
+
+ @Test
+ public void testDumpSkDestroyListenerLogs() throws ErrnoException {
+ doAnswer((invocation) -> {
+ final IndentingPrintWriter ipw = (IndentingPrintWriter) invocation.getArgument(0);
+ ipw.println("Log for testing");
+ return null;
+ }).when(mSkDestroyListener).dump(any());
+
+ final String dump = getDump();
+ assertDumpContains(dump, "Log for testing");
+ }
}
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 30aeca5..ebbb9af 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -2,11 +2,14 @@
"presubmit": [
{
"name": "CtsThreadNetworkTestCases"
+ },
+ {
+ "name": "ThreadNetworkUnitTests"
+ },
+ {
+ "name": "ThreadNetworkIntegrationTests"
}
],
"postsubmit": [
- {
- "name": "ThreadNetworkUnitTests"
- }
]
}
diff --git a/thread/flags/Android.bp b/thread/flags/Android.bp
new file mode 100644
index 0000000..225022c
--- /dev/null
+++ b/thread/flags/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aconfig_declarations {
+ name: "thread_aconfig_flags",
+ package: "com.android.net.thread.flags",
+ srcs: ["thread_base.aconfig"],
+}
+
+java_aconfig_library {
+ name: "thread_aconfig_flags_lib",
+ aconfig_declarations: "thread_aconfig_flags",
+ min_sdk_version: "30",
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.tethering",
+ ],
+}
diff --git a/thread/flags/thread_base.aconfig b/thread/flags/thread_base.aconfig
index bf1f288..f73ea6b 100644
--- a/thread/flags/thread_base.aconfig
+++ b/thread/flags/thread_base.aconfig
@@ -6,3 +6,10 @@
description: "Controls whether the Android Thread feature is enabled"
bug: "301473012"
}
+
+flag {
+ name: "thread_user_restriction_enabled"
+ namespace: "thread_network"
+ description: "Controls whether user restriction on thread networks is enabled"
+ bug: "307679182"
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index 89dcd39..a9da8d6 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -38,6 +38,8 @@
void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
void leave(in IOperationReceiver receiver);
+ void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+
int getThreadVersion();
void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 34b0b06..7242ed7 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -31,6 +31,7 @@
import android.os.RemoteException;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -499,6 +500,32 @@
}
}
+ /**
+ * Sets to use a specified test network as the upstream.
+ *
+ * @param testNetworkInterfaceName The name of the test network interface. When it's null,
+ * forbids using test network as an upstream.
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ * @hide
+ */
+ @VisibleForTesting
+ @RequiresPermission(
+ allOf = {"android.permission.THREAD_NETWORK_PRIVILEGED", permission.NETWORK_SETTINGS})
+ public void setTestNetworkAsUpstream(
+ @Nullable String testNetworkInterfaceName,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.setTestNetworkAsUpstream(
+ testNetworkInterfaceName, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private static <T> void propagateError(
Executor executor,
OutcomeReceiver<T, ThreadNetworkException> receiver,
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index c5e1e97..4fd445b 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -47,6 +47,7 @@
ERROR_REJECTED_BY_PEER,
ERROR_RESPONSE_BAD_FORMAT,
ERROR_RESOURCE_EXHAUSTED,
+ ERROR_UNKNOWN,
})
public @interface ErrorCode {}
@@ -122,6 +123,12 @@
*/
public static final int ERROR_RESOURCE_EXHAUSTED = 10;
+ /**
+ * The operation failed because of an unknown error in the system. This typically indicates
+ * that the caller doesn't understand error codes added in newer Android versions.
+ */
+ public static final int ERROR_UNKNOWN = 11;
+
private final int mErrorCode;
/** Creates a new {@link ThreadNetworkException} object with given error code and message. */
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index 35ae3c2..69295cc 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -35,9 +35,13 @@
libs: [
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
+ "framework-location.stubs.module_lib",
+ "framework-wifi",
"service-connectivity-pre-jarjar",
+ "ServiceConnectivityResources",
],
static_libs: [
+ "modules-utils-shell-command-handler",
"net-utils-device-common",
"net-utils-device-common-netlink",
"ot-daemon-aidl-java",
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
index d7c49a0..be54cbc 100644
--- a/thread/service/java/com/android/server/thread/InfraInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -36,8 +36,7 @@
* @return an ICMPv6 socket file descriptor on the Infrastructure network interface.
* @throws IOException when fails to create the socket.
*/
- public static ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName)
- throws IOException {
+ public ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName) throws IOException {
return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 60c97bf..1c51c42 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -14,6 +14,7 @@
package com.android.server.thread;
+import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE;
import static android.net.MulticastRoutingConfig.FORWARD_NONE;
import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
@@ -36,7 +37,6 @@
import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT;
import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
-import static android.net.thread.ThreadNetworkException.ErrorCode;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
@@ -53,14 +53,17 @@
import android.Manifest.permission;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.TargetApi;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.LocalNetworkConfig;
-import android.net.MulticastRoutingConfig;
import android.net.LocalNetworkInfo;
+import android.net.MulticastRoutingConfig;
import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
@@ -69,6 +72,7 @@
import android.net.NetworkRequest;
import android.net.NetworkScore;
import android.net.RouteInfo;
+import android.net.TestNetworkSpecifier;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.net.thread.IActiveOperationalDatasetReceiver;
@@ -80,6 +84,8 @@
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.DeviceRole;
+import android.net.thread.ThreadNetworkException.ErrorCode;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
@@ -91,12 +97,12 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.ServiceManagerWrapper;
+import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
import com.android.server.thread.openthread.IOtDaemon;
import com.android.server.thread.openthread.IOtDaemonCallback;
import com.android.server.thread.openthread.IOtStatusReceiver;
import com.android.server.thread.openthread.Ipv6AddressInfo;
import com.android.server.thread.openthread.OtDaemonState;
-import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
import java.io.IOException;
import java.net.Inet6Address;
@@ -115,10 +121,11 @@
*
* <p>Threading model: This class is not Thread-safe and should only be accessed from the
* ThreadNetworkService class. Additional attention should be paid to handle the threading code
- * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from
- * `mHandlerThread` 2. In the @Override methods, the actual work MUST be dispatched to the
+ * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from the
+ * thread of `mHandler` 2. In the @Override methods, the actual work MUST be dispatched to the
* HandlerThread except for arguments or permissions checking
*/
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
private static final String TAG = "ThreadNetworkService";
@@ -127,57 +134,52 @@
private final Context mContext;
private final Handler mHandler;
- // Below member fields can only be accessed from the handler thread (`mHandlerThread`). In
+ // Below member fields can only be accessed from the handler thread (`mHandler`). In
// particular, the constructor does not run on the handler thread, so it must not touch any of
// the non-final fields, nor must it mutate any of the non-final fields inside these objects.
- private final HandlerThread mHandlerThread;
private final NetworkProvider mNetworkProvider;
private final Supplier<IOtDaemon> mOtDaemonSupplier;
private final ConnectivityManager mConnectivityManager;
private final TunInterfaceController mTunIfController;
+ private final InfraInterfaceController mInfraIfController;
private final LinkProperties mLinkProperties = new LinkProperties();
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
// TODO(b/308310823): read supported channel from Thread dameon
private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
- private IOtDaemon mOtDaemon;
- private NetworkAgent mNetworkAgent;
+ @Nullable private IOtDaemon mOtDaemon;
+ @Nullable private NetworkAgent mNetworkAgent;
+ @Nullable private NetworkAgent mTestNetworkAgent;
+
private MulticastRoutingConfig mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private MulticastRoutingConfig mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private Network mUpstreamNetwork;
- private final NetworkRequest mUpstreamNetworkRequest;
+ private NetworkRequest mUpstreamNetworkRequest;
+ private UpstreamNetworkCallback mUpstreamNetworkCallback;
+ private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
private final HashMap<Network, String> mNetworkToInterface;
- private final LocalNetworkConfig mLocalNetworkConfig;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@VisibleForTesting
ThreadNetworkControllerService(
Context context,
- HandlerThread handlerThread,
+ Handler handler,
NetworkProvider networkProvider,
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
- TunInterfaceController tunIfController) {
+ TunInterfaceController tunIfController,
+ InfraInterfaceController infraIfController) {
mContext = context;
- mHandlerThread = handlerThread;
- mHandler = new Handler(handlerThread.getLooper());
+ mHandler = handler;
mNetworkProvider = networkProvider;
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
mTunIfController = tunIfController;
- mUpstreamNetworkRequest =
- new NetworkRequest.Builder()
- .clearCapabilities()
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .build();
- mLocalNetworkConfig =
- new LocalNetworkConfig.Builder()
- .setUpstreamSelector(mUpstreamNetworkRequest)
- .build();
+ mInfraIfController = infraIfController;
+ mUpstreamNetworkRequest = newUpstreamNetworkRequest();
mNetworkToInterface = new HashMap<Network, String>();
mBorderRouterConfig = new BorderRouterConfigurationParcel();
}
@@ -190,19 +192,12 @@
return new ThreadNetworkControllerService(
context,
- handlerThread,
+ new Handler(handlerThread.getLooper()),
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
context.getSystemService(ConnectivityManager.class),
- new TunInterfaceController(TUN_IF_NAME));
- }
-
- private static NetworkCapabilities newNetworkCapabilities() {
- return new NetworkCapabilities.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
- .build();
+ new TunInterfaceController(TUN_IF_NAME),
+ new InfraInterfaceController());
}
private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
@@ -237,6 +232,28 @@
LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
}
+ private NetworkRequest newUpstreamNetworkRequest() {
+ NetworkRequest.Builder builder = new NetworkRequest.Builder().clearCapabilities();
+
+ if (mUpstreamTestNetworkSpecifier != null) {
+ return builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(mUpstreamTestNetworkSpecifier)
+ .build();
+ }
+ return builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ }
+
+ private LocalNetworkConfig newLocalNetworkConfig() {
+ return new LocalNetworkConfig.Builder()
+ .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
+ .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
+ .setUpstreamSelector(mUpstreamNetworkRequest)
+ .build();
+ }
+
private void initializeOtDaemon() {
try {
getOtDaemon();
@@ -246,6 +263,8 @@
}
private IOtDaemon getOtDaemon() throws RemoteException {
+ checkOnHandlerThread();
+
if (mOtDaemon != null) {
return mOtDaemon;
}
@@ -254,9 +273,9 @@
if (otDaemon == null) {
throw new RemoteException("Internal error: failed to start OT daemon");
}
- otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
otDaemon.initialize(mTunIfController.getTunFd());
otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
+ otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
return mOtDaemon;
}
@@ -283,51 +302,70 @@
mLinkProperties.setMtu(TunInterfaceController.MTU);
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
requestUpstreamNetwork();
+ requestThreadNetwork();
initializeOtDaemon();
});
}
private void requestUpstreamNetwork() {
+ if (mUpstreamNetworkCallback != null) {
+ throw new AssertionError("The upstream network request is already there.");
+ }
+ mUpstreamNetworkCallback = new UpstreamNetworkCallback();
mConnectivityManager.registerNetworkCallback(
- mUpstreamNetworkRequest,
- new ConnectivityManager.NetworkCallback() {
- @Override
- public void onAvailable(@NonNull Network network) {
- Log.i(TAG, "onAvailable: " + network);
- }
+ mUpstreamNetworkRequest, mUpstreamNetworkCallback, mHandler);
+ }
- @Override
- public void onLost(@NonNull Network network) {
- Log.i(TAG, "onLost: " + network);
- }
+ private void cancelRequestUpstreamNetwork() {
+ if (mUpstreamNetworkCallback == null) {
+ throw new AssertionError("The upstream network request null.");
+ }
+ mNetworkToInterface.clear();
+ mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
+ mUpstreamNetworkCallback = null;
+ }
- @Override
- public void onLinkPropertiesChanged(
- @NonNull Network network, @NonNull LinkProperties linkProperties) {
- Log.i(
- TAG,
- String.format(
- "onLinkPropertiesChanged: {network: %s, interface: %s}",
- network, linkProperties.getInterfaceName()));
- mNetworkToInterface.put(network, linkProperties.getInterfaceName());
- if (network.equals(mUpstreamNetwork)) {
- enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
- }
- }
- },
- mHandler);
+ private final class UpstreamNetworkCallback extends ConnectivityManager.NetworkCallback {
+ @Override
+ public void onAvailable(@NonNull Network network) {
+ checkOnHandlerThread();
+ Log.i(TAG, "onAvailable: " + network);
+ }
+
+ @Override
+ public void onLost(@NonNull Network network) {
+ checkOnHandlerThread();
+ Log.i(TAG, "onLost: " + network);
+ }
+
+ @Override
+ public void onLinkPropertiesChanged(
+ @NonNull Network network, @NonNull LinkProperties linkProperties) {
+ checkOnHandlerThread();
+ Log.i(
+ TAG,
+ String.format(
+ "onLinkPropertiesChanged: {network: %s, interface: %s}",
+ network, linkProperties.getInterfaceName()));
+ mNetworkToInterface.put(network, linkProperties.getInterfaceName());
+ if (network.equals(mUpstreamNetwork)) {
+ enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+ }
+ }
}
private final class ThreadNetworkCallback extends ConnectivityManager.NetworkCallback {
@Override
public void onAvailable(@NonNull Network network) {
+ checkOnHandlerThread();
Log.i(TAG, "onAvailable: Thread network Available");
}
@Override
public void onLocalNetworkInfoChanged(
@NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
+ checkOnHandlerThread();
Log.i(TAG, "onLocalNetworkInfoChanged: " + localNetworkInfo);
if (localNetworkInfo.getUpstreamNetwork() == null) {
mUpstreamNetwork = null;
@@ -345,35 +383,54 @@
private void requestThreadNetwork() {
mConnectivityManager.registerNetworkCallback(
new NetworkRequest.Builder()
+ // clearCapabilities() is needed to remove forbidden capabilities and UID
+ // requirement.
.clearCapabilities()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
- .removeForbiddenCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
.build(),
new ThreadNetworkCallback(),
mHandler);
}
+ /** Injects a {@link NetworkAgent} for testing. */
+ @VisibleForTesting
+ void setTestNetworkAgent(@Nullable NetworkAgent testNetworkAgent) {
+ mTestNetworkAgent = testNetworkAgent;
+ }
+
+ private NetworkAgent newNetworkAgent() {
+ if (mTestNetworkAgent != null) {
+ return mTestNetworkAgent;
+ }
+
+ final NetworkCapabilities netCaps =
+ new NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build();
+ final NetworkScore score =
+ new NetworkScore.Builder()
+ .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
+ .build();
+ return new NetworkAgent(
+ mContext,
+ mHandler.getLooper(),
+ TAG,
+ netCaps,
+ mLinkProperties,
+ newLocalNetworkConfig(),
+ score,
+ new NetworkAgentConfig.Builder().build(),
+ mNetworkProvider) {};
+ }
+
private void registerThreadNetwork() {
if (mNetworkAgent != null) {
return;
}
- NetworkCapabilities netCaps = newNetworkCapabilities();
- NetworkScore score =
- new NetworkScore.Builder()
- .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
- .build();
- requestThreadNetwork();
- mNetworkAgent =
- new NetworkAgent(
- mContext,
- mHandlerThread.getLooper(),
- TAG,
- netCaps,
- mLinkProperties,
- mLocalNetworkConfig,
- score,
- new NetworkAgentConfig.Builder().build(),
- mNetworkProvider) {};
+
+ mNetworkAgent = newNetworkAgent();
mNetworkAgent.register();
mNetworkAgent.markConnected();
Log.i(TAG, "Registered Thread network");
@@ -524,29 +581,29 @@
return -1;
}
- private void enforceAllCallingPermissionsGranted(String... permissions) {
+ private void enforceAllPermissionsGranted(String... permissions) {
for (String permission : permissions) {
- mContext.enforceCallingPermission(
+ mContext.enforceCallingOrSelfPermission(
permission, "Permission " + permission + " is missing");
}
}
@Override
public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
- enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
}
@Override
public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
- enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
}
@Override
public void registerOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
- enforceAllCallingPermissionsGranted(
+ enforceAllPermissionsGranted(
permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> mOtDaemonCallbackProxy.registerDatasetCallback(callback));
}
@@ -554,13 +611,13 @@
@Override
public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
- enforceAllCallingPermissionsGranted(
+ enforceAllPermissionsGranted(
permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
}
private void checkOnHandlerThread() {
- if (Looper.myLooper() != mHandlerThread.getLooper()) {
+ if (Looper.myLooper() != mHandler.getLooper()) {
Log.wtf(TAG, "Must be on the handler thread!");
}
}
@@ -609,7 +666,7 @@
@Override
public void join(
@NonNull ActiveOperationalDataset activeDataset, @NonNull IOperationReceiver receiver) {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
mHandler.post(() -> joinInternal(activeDataset, receiverWrapper));
@@ -633,7 +690,7 @@
public void scheduleMigration(
@NonNull PendingOperationalDataset pendingDataset,
@NonNull IOperationReceiver receiver) {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
mHandler.post(() -> scheduleMigrationInternal(pendingDataset, receiverWrapper));
@@ -656,7 +713,7 @@
@Override
public void leave(@NonNull IOperationReceiver receiver) throws RemoteException {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
}
@@ -672,6 +729,64 @@
}
}
+ /**
+ * Sets the country code.
+ *
+ * @param countryCode 2 characters string country code (as defined in ISO 3166) to set.
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+ public void setCountryCode(@NonNull String countryCode, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+ mHandler.post(() -> setCountryCodeInternal(countryCode, receiverWrapper));
+ }
+
+ private void setCountryCodeInternal(
+ String countryCode, @NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.setCountryCode failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ @Override
+ public void setTestNetworkAsUpstream(
+ @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED, NETWORK_SETTINGS);
+
+ Log.i(TAG, "setTestNetworkAsUpstream: " + testNetworkInterfaceName);
+ mHandler.post(() -> setTestNetworkAsUpstreamInternal(testNetworkInterfaceName, receiver));
+ }
+
+ private void setTestNetworkAsUpstreamInternal(
+ @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
+ checkOnHandlerThread();
+
+ TestNetworkSpecifier testNetworkSpecifier = null;
+ if (testNetworkInterfaceName != null) {
+ testNetworkSpecifier = new TestNetworkSpecifier(testNetworkInterfaceName);
+ }
+
+ if (!Objects.equals(mUpstreamTestNetworkSpecifier, testNetworkSpecifier)) {
+ cancelRequestUpstreamNetwork();
+ mUpstreamTestNetworkSpecifier = testNetworkSpecifier;
+ mUpstreamNetworkRequest = newUpstreamNetworkRequest();
+ requestUpstreamNetwork();
+ sendLocalNetworkConfig();
+ }
+ try {
+ receiver.onSuccess();
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+
private void enableBorderRouting(String infraIfName) {
if (mBorderRouterConfig.isBorderRoutingEnabled
&& infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
@@ -681,7 +796,7 @@
try {
mBorderRouterConfig.infraInterfaceName = infraIfName;
mBorderRouterConfig.infraInterfaceIcmp6Socket =
- InfraInterfaceController.createIcmp6Socket(infraIfName);
+ mInfraIfController.createIcmp6Socket(infraIfName);
mBorderRouterConfig.isBorderRoutingEnabled = true;
mOtDaemon.configureBorderRouter(
@@ -754,20 +869,9 @@
if (mNetworkAgent == null) {
return;
}
- final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
- LocalNetworkConfig localNetworkConfig =
- configBuilder
- .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
- .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
- .setUpstreamSelector(mUpstreamNetworkRequest)
- .build();
+ final LocalNetworkConfig localNetworkConfig = newLocalNetworkConfig();
mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
- Log.d(
- TAG,
- "Sent localNetworkConfig with upstreamConfig "
- + mUpstreamMulticastRoutingConfig
- + " downstreamConfig"
- + mDownstreamMulticastRoutingConfig);
+ Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
}
private void handleMulticastForwardingStateChanged(boolean isEnabled) {
@@ -800,8 +904,8 @@
MulticastRoutingConfig newDownstreamConfig;
MulticastRoutingConfig.Builder builder;
- if (mDownstreamMulticastRoutingConfig.getForwardingMode() !=
- MulticastRoutingConfig.FORWARD_SELECTED) {
+ if (mDownstreamMulticastRoutingConfig.getForwardingMode()
+ != MulticastRoutingConfig.FORWARD_SELECTED) {
Log.e(
TAG,
"Ignore multicast listening address updates when downstream multicast "
@@ -809,8 +913,8 @@
// Don't update the address set if downstream multicast forwarding is disabled.
return;
}
- if (isAdded ==
- mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
+ if (isAdded
+ == mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
return;
}
@@ -861,8 +965,8 @@
}
/**
- * Handles and forwards Thread daemon callbacks. This class must be accessed from the {@code
- * mHandlerThread}.
+ * Handles and forwards Thread daemon callbacks. This class must be accessed from the thread of
+ * {@code mHandler}.
*/
private final class OtDaemonCallbackProxy extends IOtDaemonCallback.Stub {
private final Map<IStateCallback, CallbackMetadata> mStateCallbacks = new HashMap<>();
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
new file mode 100644
index 0000000..b7b6233
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationManager;
+import android.net.thread.IOperationReceiver;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
+import android.os.Build;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.ConnectivityResources;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Provide functions for making changes to Thread Network country code. This Country Code is from
+ * location, WiFi or telephony configuration. This class sends Country Code to Thread Network native
+ * layer.
+ *
+ * <p>This class is thread-safe.
+ */
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class ThreadNetworkCountryCode {
+ private static final String TAG = "ThreadNetworkCountryCode";
+ // To be used when there is no country code available.
+ @VisibleForTesting public static final String DEFAULT_COUNTRY_CODE = "WW";
+
+ // Wait 1 hour between updates.
+ private static final long TIME_BETWEEN_LOCATION_UPDATES_MS = 1000L * 60 * 60 * 1;
+ // Minimum distance before an update is triggered, in meters. We don't need this to be too
+ // exact because all we care about is what country the user is in.
+ private static final float DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS = 5_000.0f;
+
+ /** List of country code sources. */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(
+ prefix = "COUNTRY_CODE_SOURCE_",
+ value = {
+ COUNTRY_CODE_SOURCE_DEFAULT,
+ COUNTRY_CODE_SOURCE_LOCATION,
+ COUNTRY_CODE_SOURCE_OVERRIDE,
+ COUNTRY_CODE_SOURCE_TELEPHONY,
+ COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
+ COUNTRY_CODE_SOURCE_WIFI,
+ })
+ private @interface CountryCodeSource {}
+
+ private static final String COUNTRY_CODE_SOURCE_DEFAULT = "Default";
+ private static final String COUNTRY_CODE_SOURCE_LOCATION = "Location";
+ private static final String COUNTRY_CODE_SOURCE_OVERRIDE = "Override";
+ private static final String COUNTRY_CODE_SOURCE_TELEPHONY = "Telephony";
+ private static final String COUNTRY_CODE_SOURCE_TELEPHONY_LAST = "TelephonyLast";
+ private static final String COUNTRY_CODE_SOURCE_WIFI = "Wifi";
+
+ private static final CountryCodeInfo DEFAULT_COUNTRY_CODE_INFO =
+ new CountryCodeInfo(DEFAULT_COUNTRY_CODE, COUNTRY_CODE_SOURCE_DEFAULT);
+
+ private final ConnectivityResources mResources;
+ private final Context mContext;
+ private final LocationManager mLocationManager;
+ @Nullable private final Geocoder mGeocoder;
+ private final ThreadNetworkControllerService mThreadNetworkControllerService;
+ private final WifiManager mWifiManager;
+ private final TelephonyManager mTelephonyManager;
+ private final SubscriptionManager mSubscriptionManager;
+ private final Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeSlotInfoMap =
+ new ArrayMap();
+
+ @Nullable private CountryCodeInfo mCurrentCountryCodeInfo;
+ @Nullable private CountryCodeInfo mLocationCountryCodeInfo;
+ @Nullable private CountryCodeInfo mOverrideCountryCodeInfo;
+ @Nullable private CountryCodeInfo mWifiCountryCodeInfo;
+ @Nullable private CountryCodeInfo mTelephonyCountryCodeInfo;
+ @Nullable private CountryCodeInfo mTelephonyLastCountryCodeInfo;
+
+ /** Container class to store Thread country code information. */
+ private static final class CountryCodeInfo {
+ private String mCountryCode;
+ @CountryCodeSource private String mSource;
+ private final Instant mUpdatedTimestamp;
+
+ public CountryCodeInfo(
+ String countryCode, @CountryCodeSource String countryCodeSource, Instant instant) {
+ mCountryCode = countryCode;
+ mSource = countryCodeSource;
+ mUpdatedTimestamp = instant;
+ }
+
+ public CountryCodeInfo(String countryCode, @CountryCodeSource String countryCodeSource) {
+ this(countryCode, countryCodeSource, Instant.now());
+ }
+
+ public String getCountryCode() {
+ return mCountryCode;
+ }
+
+ public boolean isCountryCodeMatch(CountryCodeInfo countryCodeInfo) {
+ if (countryCodeInfo == null) {
+ return false;
+ }
+
+ return Objects.equals(countryCodeInfo.mCountryCode, mCountryCode);
+ }
+
+ @Override
+ public String toString() {
+ return "CountryCodeInfo{ mCountryCode: "
+ + mCountryCode
+ + ", mSource: "
+ + mSource
+ + ", mUpdatedTimestamp: "
+ + mUpdatedTimestamp
+ + "}";
+ }
+ }
+
+ /** Container class to store country code per SIM slot. */
+ private static final class TelephonyCountryCodeSlotInfo {
+ public int slotIndex;
+ public String countryCode;
+ public String lastKnownCountryCode;
+ public Instant timestamp;
+
+ @Override
+ public String toString() {
+ return "TelephonyCountryCodeSlotInfo{ slotIndex: "
+ + slotIndex
+ + ", countryCode: "
+ + countryCode
+ + ", lastKnownCountryCode: "
+ + lastKnownCountryCode
+ + ", timestamp: "
+ + timestamp
+ + "}";
+ }
+ }
+
+ private boolean isLocationUseForCountryCodeEnabled() {
+ return mResources
+ .get()
+ .getBoolean(R.bool.config_thread_location_use_for_country_code_enabled);
+ }
+
+ public ThreadNetworkCountryCode(
+ LocationManager locationManager,
+ ThreadNetworkControllerService threadNetworkControllerService,
+ @Nullable Geocoder geocoder,
+ ConnectivityResources resources,
+ WifiManager wifiManager,
+ Context context,
+ TelephonyManager telephonyManager,
+ SubscriptionManager subscriptionManager) {
+ mLocationManager = locationManager;
+ mThreadNetworkControllerService = threadNetworkControllerService;
+ mGeocoder = geocoder;
+ mResources = resources;
+ mWifiManager = wifiManager;
+ mContext = context;
+ mTelephonyManager = telephonyManager;
+ mSubscriptionManager = subscriptionManager;
+ }
+
+ public static ThreadNetworkCountryCode newInstance(
+ Context context, ThreadNetworkControllerService controllerService) {
+ return new ThreadNetworkCountryCode(
+ context.getSystemService(LocationManager.class),
+ controllerService,
+ Geocoder.isPresent() ? new Geocoder(context) : null,
+ new ConnectivityResources(context),
+ context.getSystemService(WifiManager.class),
+ context,
+ context.getSystemService(TelephonyManager.class),
+ context.getSystemService(SubscriptionManager.class));
+ }
+
+ /** Sets up this country code module to listen to location country code changes. */
+ public synchronized void initialize() {
+ registerGeocoderCountryCodeCallback();
+ registerWifiCountryCodeCallback();
+ registerTelephonyCountryCodeCallback();
+ updateTelephonyCountryCodeFromSimCard();
+ updateCountryCode(false /* forceUpdate */);
+ }
+
+ private synchronized void registerGeocoderCountryCodeCallback() {
+ if ((mGeocoder != null) && isLocationUseForCountryCodeEnabled()) {
+ mLocationManager.requestLocationUpdates(
+ LocationManager.PASSIVE_PROVIDER,
+ TIME_BETWEEN_LOCATION_UPDATES_MS,
+ DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS,
+ location -> setCountryCodeFromGeocodingLocation(location));
+ }
+ }
+
+ private synchronized void geocodeListener(List<Address> addresses) {
+ if (addresses != null && !addresses.isEmpty()) {
+ String countryCode = addresses.get(0).getCountryCode();
+
+ if (isValidCountryCode(countryCode)) {
+ Log.d(TAG, "Set location country code to: " + countryCode);
+ mLocationCountryCodeInfo =
+ new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_LOCATION);
+ } else {
+ Log.d(TAG, "Received invalid location country code");
+ mLocationCountryCodeInfo = null;
+ }
+
+ updateCountryCode(false /* forceUpdate */);
+ }
+ }
+
+ private synchronized void setCountryCodeFromGeocodingLocation(@Nullable Location location) {
+ if ((location == null) || (mGeocoder == null)) return;
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
+ Log.wtf(
+ TAG,
+ "Unexpected call to set country code from the Geocoding location, "
+ + "Thread code never runs under T or lower.");
+ return;
+ }
+
+ mGeocoder.getFromLocation(
+ location.getLatitude(),
+ location.getLongitude(),
+ 1 /* maxResults */,
+ this::geocodeListener);
+ }
+
+ private synchronized void registerWifiCountryCodeCallback() {
+ if (mWifiManager != null) {
+ mWifiManager.registerActiveCountryCodeChangedCallback(
+ r -> r.run(), new WifiCountryCodeCallback());
+ }
+ }
+
+ private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback {
+ @Override
+ public void onActiveCountryCodeChanged(String countryCode) {
+ Log.d(TAG, "Wifi country code is changed to " + countryCode);
+ synchronized ("ThreadNetworkCountryCode.this") {
+ mWifiCountryCodeInfo = new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_WIFI);
+ updateCountryCode(false /* forceUpdate */);
+ }
+ }
+
+ @Override
+ public void onCountryCodeInactive() {
+ Log.d(TAG, "Wifi country code is inactived");
+ synchronized ("ThreadNetworkCountryCode.this") {
+ mWifiCountryCodeInfo = null;
+ updateCountryCode(false /* forceUpdate */);
+ }
+ }
+ }
+
+ private synchronized void registerTelephonyCountryCodeCallback() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ Log.wtf(
+ TAG,
+ "Unexpected call to register the telephony country code changed callback, "
+ + "Thread code never runs under T or lower.");
+ return;
+ }
+
+ BroadcastReceiver broadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int slotIndex =
+ intent.getIntExtra(
+ SubscriptionManager.EXTRA_SLOT_INDEX,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ String lastKnownCountryCode = null;
+ String countryCode =
+ intent.getStringExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ lastKnownCountryCode =
+ intent.getStringExtra(
+ TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY);
+ }
+
+ setTelephonyCountryCodeAndLastKnownCountryCode(
+ slotIndex, countryCode, lastKnownCountryCode);
+ }
+ };
+
+ mContext.registerReceiver(
+ broadcastReceiver,
+ new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED),
+ Context.RECEIVER_EXPORTED);
+ }
+
+ private synchronized void updateTelephonyCountryCodeFromSimCard() {
+ List<SubscriptionInfo> subscriptionInfoList =
+ mSubscriptionManager.getActiveSubscriptionInfoList();
+
+ if (subscriptionInfoList == null) {
+ Log.d(TAG, "No SIM card is found");
+ return;
+ }
+
+ for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
+ String countryCode;
+ int slotIndex;
+
+ slotIndex = subscriptionInfo.getSimSlotIndex();
+ try {
+ countryCode = mTelephonyManager.getNetworkCountryIso(slotIndex);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to get country code for slot index:" + slotIndex, e);
+ continue;
+ }
+
+ Log.d(TAG, "Telephony slot " + slotIndex + " country code is " + countryCode);
+ setTelephonyCountryCodeAndLastKnownCountryCode(
+ slotIndex, countryCode, null /* lastKnownCountryCode */);
+ }
+ }
+
+ private synchronized void setTelephonyCountryCodeAndLastKnownCountryCode(
+ int slotIndex, String countryCode, String lastKnownCountryCode) {
+ Log.d(
+ TAG,
+ "Set telephony country code to: "
+ + countryCode
+ + ", last country code to: "
+ + lastKnownCountryCode
+ + " for slotIndex: "
+ + slotIndex);
+
+ TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot =
+ mTelephonyCountryCodeSlotInfoMap.computeIfAbsent(
+ slotIndex, k -> new TelephonyCountryCodeSlotInfo());
+ telephonyCountryCodeInfoSlot.slotIndex = slotIndex;
+ telephonyCountryCodeInfoSlot.timestamp = Instant.now();
+ telephonyCountryCodeInfoSlot.countryCode = countryCode;
+ telephonyCountryCodeInfoSlot.lastKnownCountryCode = lastKnownCountryCode;
+
+ mTelephonyCountryCodeInfo = null;
+ mTelephonyLastCountryCodeInfo = null;
+
+ for (TelephonyCountryCodeSlotInfo slotInfo : mTelephonyCountryCodeSlotInfoMap.values()) {
+ if ((mTelephonyCountryCodeInfo == null) && isValidCountryCode(slotInfo.countryCode)) {
+ mTelephonyCountryCodeInfo =
+ new CountryCodeInfo(
+ slotInfo.countryCode,
+ COUNTRY_CODE_SOURCE_TELEPHONY,
+ slotInfo.timestamp);
+ }
+
+ if ((mTelephonyLastCountryCodeInfo == null)
+ && isValidCountryCode(slotInfo.lastKnownCountryCode)) {
+ mTelephonyLastCountryCodeInfo =
+ new CountryCodeInfo(
+ slotInfo.lastKnownCountryCode,
+ COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
+ slotInfo.timestamp);
+ }
+ }
+
+ updateCountryCode(false /* forceUpdate */);
+ }
+
+ /**
+ * Priority order of country code sources (we stop at the first known country code source):
+ *
+ * <ul>
+ * <li>1. Override country code - Country code forced via shell command (local/automated
+ * testing)
+ * <li>2. Telephony country code - Current country code retrieved via cellular. If there are
+ * multiple SIM's, the country code chosen is non-deterministic if they return different
+ * codes. The first valid country code with the lowest slot number will be used.
+ * <li>3. Wifi country code - Current country code retrieved via wifi (via 80211.ad).
+ * <li>4. Last known telephony country code - Last known country code retrieved via cellular.
+ * If there are multiple SIM's, the country code chosen is non-deterministic if they
+ * return different codes. The first valid last known country code with the lowest slot
+ * number will be used.
+ * <li>5. Location country code - Country code retrieved from LocationManager passive location
+ * provider.
+ * </ul>
+ *
+ * @return the selected country code information.
+ */
+ private CountryCodeInfo pickCountryCode() {
+ if (mOverrideCountryCodeInfo != null) {
+ return mOverrideCountryCodeInfo;
+ }
+
+ if (mTelephonyCountryCodeInfo != null) {
+ return mTelephonyCountryCodeInfo;
+ }
+
+ if (mWifiCountryCodeInfo != null) {
+ return mWifiCountryCodeInfo;
+ }
+
+ if (mTelephonyLastCountryCodeInfo != null) {
+ return mTelephonyLastCountryCodeInfo;
+ }
+
+ if (mLocationCountryCodeInfo != null) {
+ return mLocationCountryCodeInfo;
+ }
+
+ return DEFAULT_COUNTRY_CODE_INFO;
+ }
+
+ private IOperationReceiver newOperationReceiver(CountryCodeInfo countryCodeInfo) {
+ return new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ synchronized ("ThreadNetworkCountryCode.this") {
+ mCurrentCountryCodeInfo = countryCodeInfo;
+ }
+ }
+
+ @Override
+ public void onError(int otError, String message) {
+ Log.e(
+ TAG,
+ "Error "
+ + otError
+ + ": "
+ + message
+ + ". Failed to set country code "
+ + countryCodeInfo);
+ }
+ };
+ }
+
+ /**
+ * Updates country code to the Thread native layer.
+ *
+ * @param forceUpdate Force update the country code even if it was the same as previously cached
+ * value.
+ */
+ @VisibleForTesting
+ public synchronized void updateCountryCode(boolean forceUpdate) {
+ CountryCodeInfo countryCodeInfo = pickCountryCode();
+
+ if (!forceUpdate && countryCodeInfo.isCountryCodeMatch(mCurrentCountryCodeInfo)) {
+ Log.i(TAG, "Ignoring already set country code " + countryCodeInfo.getCountryCode());
+ return;
+ }
+
+ Log.i(TAG, "Set country code: " + countryCodeInfo);
+ mThreadNetworkControllerService.setCountryCode(
+ countryCodeInfo.getCountryCode().toUpperCase(Locale.ROOT),
+ newOperationReceiver(countryCodeInfo));
+ }
+
+ /** Returns the current country code or {@code null} if no country code is set. */
+ @Nullable
+ public synchronized String getCountryCode() {
+ return (mCurrentCountryCodeInfo != null) ? mCurrentCountryCodeInfo.getCountryCode() : null;
+ }
+
+ /**
+ * Returns {@code true} if {@code countryCode} is a valid country code.
+ *
+ * <p>A country code is valid if it consists of 2 alphabets.
+ */
+ public static boolean isValidCountryCode(String countryCode) {
+ return countryCode != null
+ && countryCode.length() == 2
+ && countryCode.chars().allMatch(Character::isLetter);
+ }
+
+ /**
+ * Overrides any existing country code.
+ *
+ * @param countryCode A 2-Character alphabetical country code (as defined in ISO 3166).
+ * @throws IllegalArgumentException if {@code countryCode} is an invalid country code.
+ */
+ public synchronized void setOverrideCountryCode(String countryCode) {
+ if (!isValidCountryCode(countryCode)) {
+ throw new IllegalArgumentException("The override country code is invalid");
+ }
+
+ mOverrideCountryCodeInfo = new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_OVERRIDE);
+ updateCountryCode(true /* forceUpdate */);
+ }
+
+ /** Clears the country code previously set through {@link #setOverrideCountryCode} method. */
+ public synchronized void clearOverrideCountryCode() {
+ mOverrideCountryCodeInfo = null;
+ updateCountryCode(true /* forceUpdate */);
+ }
+
+ /** Dumps the current state of this ThreadNetworkCountryCode object. */
+ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("---- Dump of ThreadNetworkCountryCode begin ----");
+ pw.println("mOverrideCountryCodeInfo: " + mOverrideCountryCodeInfo);
+ pw.println("mTelephonyCountryCodeSlotInfoMap: " + mTelephonyCountryCodeSlotInfoMap);
+ pw.println("mTelephonyCountryCodeInfo: " + mTelephonyCountryCodeInfo);
+ pw.println("mWifiCountryCodeInfo: " + mWifiCountryCodeInfo);
+ pw.println("mTelephonyLastCountryCodeInfo: " + mTelephonyLastCountryCodeInfo);
+ pw.println("mLocationCountryCodeInfo: " + mLocationCountryCodeInfo);
+ pw.println("mCurrentCountryCodeInfo: " + mCurrentCountryCodeInfo);
+ pw.println("---- Dump of ThreadNetworkCountryCode end ------");
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
index cc694a1..53f2d4f 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -16,13 +16,20 @@
package com.android.server.thread;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.IThreadNetworkController;
import android.net.thread.IThreadNetworkManager;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
import com.android.server.SystemService;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
import java.util.Collections;
import java.util.List;
@@ -31,7 +38,9 @@
*/
public class ThreadNetworkService extends IThreadNetworkManager.Stub {
private final Context mContext;
+ @Nullable private ThreadNetworkCountryCode mCountryCode;
@Nullable private ThreadNetworkControllerService mControllerService;
+ @Nullable private ThreadNetworkShellCommand mShellCommand;
/** Creates a new {@link ThreadNetworkService} object. */
public ThreadNetworkService(Context context) {
@@ -39,14 +48,21 @@
}
/**
- * Called by the service initializer.
+ * Called by {@link com.android.server.ConnectivityServiceInitializer}.
*
* @see com.android.server.SystemService#onBootPhase
*/
public void onBootPhase(int phase) {
- if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+ if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
mControllerService = ThreadNetworkControllerService.newInstance(mContext);
mControllerService.initialize();
+ } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+ // Country code initialization is delayed to the BOOT_COMPLETED phase because it will
+ // call into Wi-Fi and Telephony service whose country code module is ready after
+ // PHASE_ACTIVITY_MANAGER_READY and PHASE_THIRD_PARTY_APPS_CAN_START
+ mCountryCode = ThreadNetworkCountryCode.newInstance(mContext, mControllerService);
+ mCountryCode.initialize();
+ mShellCommand = new ThreadNetworkShellCommand(mCountryCode);
}
}
@@ -57,4 +73,40 @@
}
return Collections.singletonList(mControllerService);
}
+
+ @Override
+ public int handleShellCommand(
+ @NonNull ParcelFileDescriptor in,
+ @NonNull ParcelFileDescriptor out,
+ @NonNull ParcelFileDescriptor err,
+ @NonNull String[] args) {
+ if (mShellCommand == null) {
+ return -1;
+ }
+ return mShellCommand.exec(
+ this,
+ in.getFileDescriptor(),
+ out.getFileDescriptor(),
+ err.getFileDescriptor(),
+ args);
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PERMISSION_GRANTED) {
+ pw.println(
+ "Permission Denial: can't dump ThreadNetworkService from from pid="
+ + Binder.getCallingPid()
+ + ", uid="
+ + Binder.getCallingUid());
+ return;
+ }
+
+ if (mCountryCode != null) {
+ mCountryCode.dump(fd, pw, args);
+ }
+
+ pw.println();
+ }
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
new file mode 100644
index 0000000..c17c5a7
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.Process;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.BasicShellCommandHandler;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Interprets and executes 'adb shell cmd thread_network [args]'.
+ *
+ * <p>To add new commands: - onCommand: Add a case "<command>" execute. Return a 0 if command
+ * executed successfully. - onHelp: add a description string.
+ *
+ * <p>Permissions: currently root permission is required for some commands. Others will enforce the
+ * corresponding API permissions.
+ */
+public class ThreadNetworkShellCommand extends BasicShellCommandHandler {
+ private static final String TAG = "ThreadNetworkShellCommand";
+
+ // These don't require root access.
+ private static final List<String> NON_PRIVILEGED_COMMANDS = List.of("help", "get-country-code");
+
+ @Nullable private final ThreadNetworkCountryCode mCountryCode;
+ @Nullable private PrintWriter mOutputWriter;
+ @Nullable private PrintWriter mErrorWriter;
+
+ ThreadNetworkShellCommand(@Nullable ThreadNetworkCountryCode countryCode) {
+ mCountryCode = countryCode;
+ }
+
+ @VisibleForTesting
+ public void setPrintWriters(PrintWriter outputWriter, PrintWriter errorWriter) {
+ mOutputWriter = outputWriter;
+ mErrorWriter = errorWriter;
+ }
+
+ private PrintWriter getOutputWriter() {
+ return (mOutputWriter != null) ? mOutputWriter : getOutPrintWriter();
+ }
+
+ private PrintWriter getErrorWriter() {
+ return (mErrorWriter != null) ? mErrorWriter : getErrPrintWriter();
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ // Treat no command as help command.
+ if (TextUtils.isEmpty(cmd)) {
+ cmd = "help";
+ }
+
+ final PrintWriter pw = getOutputWriter();
+ final PrintWriter perr = getErrorWriter();
+
+ // Explicit exclusion from root permission
+ if (!NON_PRIVILEGED_COMMANDS.contains(cmd)) {
+ final int uid = Binder.getCallingUid();
+
+ if (uid != Process.ROOT_UID) {
+ perr.println(
+ "Uid "
+ + uid
+ + " does not have access to "
+ + cmd
+ + " thread command "
+ + "(or such command doesn't exist)");
+ return -1;
+ }
+ }
+
+ switch (cmd) {
+ case "force-country-code":
+ boolean enabled;
+
+ if (mCountryCode == null) {
+ perr.println("Thread country code operations are not supported");
+ return -1;
+ }
+
+ try {
+ enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
+ } catch (IllegalArgumentException e) {
+ perr.println("Invalid argument: " + e.getMessage());
+ return -1;
+ }
+
+ if (enabled) {
+ String countryCode = getNextArgRequired();
+ if (!ThreadNetworkCountryCode.isValidCountryCode(countryCode)) {
+ perr.println(
+ "Invalid argument: Country code must be a 2-Character"
+ + " string. But got country code "
+ + countryCode
+ + " instead");
+ return -1;
+ }
+ mCountryCode.setOverrideCountryCode(countryCode);
+ pw.println("Set Thread country code: " + countryCode);
+
+ } else {
+ mCountryCode.clearOverrideCountryCode();
+ }
+ return 0;
+ case "get-country-code":
+ if (mCountryCode == null) {
+ perr.println("Thread country code operations are not supported");
+ return -1;
+ }
+
+ pw.println("Thread country code = " + mCountryCode.getCountryCode());
+ return 0;
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ }
+
+ private static boolean argTrueOrFalse(String arg, String trueString, String falseString) {
+ if (trueString.equals(arg)) {
+ return true;
+ } else if (falseString.equals(arg)) {
+ return false;
+ } else {
+ throw new IllegalArgumentException(
+ "Expected '"
+ + trueString
+ + "' or '"
+ + falseString
+ + "' as next arg but got '"
+ + arg
+ + "'");
+ }
+ }
+
+ private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString) {
+ String nextArg = getNextArgRequired();
+ return argTrueOrFalse(nextArg, trueString, falseString);
+ }
+
+ private void onHelpNonPrivileged(PrintWriter pw) {
+ pw.println(" get-country-code");
+ pw.println(" Gets country code as a two-letter string");
+ }
+
+ private void onHelpPrivileged(PrintWriter pw) {
+ pw.println(" force-country-code enabled <two-letter code> | disabled ");
+ pw.println(" Sets country code to <two-letter code> or left for normal value");
+ }
+
+ @Override
+ public void onHelp() {
+ final PrintWriter pw = getOutputWriter();
+ pw.println("Thread network commands:");
+ pw.println(" help or -h");
+ pw.println(" Print this help text.");
+ onHelpNonPrivileged(pw);
+ if (Binder.getCallingUid() == Process.ROOT_UID) {
+ onHelpPrivileged(pw);
+ }
+ pw.println();
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
new file mode 100644
index 0000000..d32f0bf
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.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.thread;
+
+import android.annotation.Nullable;
+import android.os.PersistableBundle;
+import android.util.AtomicFile;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Store persistent data for Thread network settings. These are key (string) / value pairs that are
+ * stored in ThreadPersistentSetting.xml file. The values allowed are those that can be serialized
+ * via {@link PersistableBundle}.
+ */
+public class ThreadPersistentSettings {
+ private static final String TAG = "ThreadPersistentSettings";
+ /** File name used for storing settings. */
+ public static final String FILE_NAME = "ThreadPersistentSettings.xml";
+ /** Current config store data version. This will be incremented for any additions. */
+ private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1;
+ /**
+ * Stores the version of the data. This can be used to handle migration of data if some
+ * non-backward compatible change introduced.
+ */
+ private static final String VERSION_KEY = "version";
+
+ /******** Thread persistent setting keys ***************/
+ /** Stores the Thread feature toggle state, true for enabled and false for disabled. */
+ public static final Key<Boolean> THREAD_ENABLED = new Key<>("Thread_enabled", true);
+
+ /******** Thread persistent setting keys ***************/
+
+ @GuardedBy("mLock")
+ private final AtomicFile mAtomicFile;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final PersistableBundle mSettings = new PersistableBundle();
+
+ public ThreadPersistentSettings(AtomicFile atomicFile) {
+ mAtomicFile = atomicFile;
+ }
+
+ /** Initialize the settings by reading from the settings file. */
+ public void initialize() {
+ readFromStoreFile();
+ synchronized (mLock) {
+ if (mSettings.isEmpty()) {
+ put(THREAD_ENABLED.key, THREAD_ENABLED.defaultValue);
+ }
+ }
+ }
+
+ private void putObject(String key, @Nullable Object value) {
+ synchronized (mLock) {
+ if (value == null) {
+ mSettings.putString(key, null);
+ } else if (value instanceof Boolean) {
+ mSettings.putBoolean(key, (Boolean) value);
+ } else if (value instanceof Integer) {
+ mSettings.putInt(key, (Integer) value);
+ } else if (value instanceof Long) {
+ mSettings.putLong(key, (Long) value);
+ } else if (value instanceof Double) {
+ mSettings.putDouble(key, (Double) value);
+ } else if (value instanceof String) {
+ mSettings.putString(key, (String) value);
+ } else {
+ throw new IllegalArgumentException("Unsupported type " + value.getClass());
+ }
+ }
+ }
+
+ private <T> T getObject(String key, T defaultValue) {
+ Object value;
+ synchronized (mLock) {
+ if (defaultValue instanceof Boolean) {
+ value = mSettings.getBoolean(key, (Boolean) defaultValue);
+ } else if (defaultValue instanceof Integer) {
+ value = mSettings.getInt(key, (Integer) defaultValue);
+ } else if (defaultValue instanceof Long) {
+ value = mSettings.getLong(key, (Long) defaultValue);
+ } else if (defaultValue instanceof Double) {
+ value = mSettings.getDouble(key, (Double) defaultValue);
+ } else if (defaultValue instanceof String) {
+ value = mSettings.getString(key, (String) defaultValue);
+ } else {
+ throw new IllegalArgumentException("Unsupported type " + defaultValue.getClass());
+ }
+ }
+ return (T) value;
+ }
+
+ /**
+ * Store a value to the stored settings.
+ *
+ * @param key One of the settings keys.
+ * @param value Value to be stored.
+ */
+ public <T> void put(String key, @Nullable T value) {
+ putObject(key, value);
+ writeToStoreFile();
+ }
+
+ /**
+ * Retrieve a value from the stored settings.
+ *
+ * @param key One of the settings keys.
+ * @return value stored in settings, defValue if the key does not exist.
+ */
+ public <T> T get(Key<T> key) {
+ return getObject(key.key, key.defaultValue);
+ }
+
+ /**
+ * Base class to store string key and its default value.
+ *
+ * @param <T> Type of the value.
+ */
+ public static class Key<T> {
+ public final String key;
+ public final T defaultValue;
+
+ private Key(String key, T defaultValue) {
+ this.key = key;
+ this.defaultValue = defaultValue;
+ }
+
+ @Override
+ public String toString() {
+ return "[Key: " + key + ", DefaultValue: " + defaultValue + "]";
+ }
+ }
+
+ private void writeToStoreFile() {
+ try {
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final PersistableBundle bundleToWrite;
+ synchronized (mLock) {
+ bundleToWrite = new PersistableBundle(mSettings);
+ }
+ bundleToWrite.putInt(VERSION_KEY, CURRENT_SETTINGS_STORE_DATA_VERSION);
+ bundleToWrite.writeToStream(outputStream);
+ synchronized (mLock) {
+ writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
+ }
+ } catch (IOException e) {
+ Log.wtf(TAG, "Write to store file failed", e);
+ }
+ }
+
+ private void readFromStoreFile() {
+ try {
+ final byte[] readData;
+ synchronized (mLock) {
+ Log.i(TAG, "Reading from store file: " + mAtomicFile.getBaseFile());
+ readData = readFromAtomicFile(mAtomicFile);
+ }
+ final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
+ final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream);
+ // Version unused for now. May be needed in the future for handling migrations.
+ bundleRead.remove(VERSION_KEY);
+ synchronized (mLock) {
+ mSettings.putAll(bundleRead);
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "No store file to read", e);
+ } catch (IOException e) {
+ Log.e(TAG, "Read from store file failed", e);
+ }
+ }
+
+ /**
+ * Read raw data from the atomic file. Note: This is a copy of {@link AtomicFile#readFully()}
+ * modified to use the passed in {@link InputStream} which was returned using {@link
+ * AtomicFile#openRead()}.
+ */
+ private static byte[] readFromAtomicFile(AtomicFile file) throws IOException {
+ FileInputStream stream = null;
+ try {
+ stream = file.openRead();
+ int pos = 0;
+ int avail = stream.available();
+ byte[] data = new byte[avail];
+ while (true) {
+ int amt = stream.read(data, pos, data.length - pos);
+ if (amt <= 0) {
+ return data;
+ }
+ pos += amt;
+ avail = stream.available();
+ if (avail > data.length - pos) {
+ byte[] newData = new byte[pos + avail];
+ System.arraycopy(data, 0, newData, 0, pos);
+ data = newData;
+ }
+ }
+ } finally {
+ if (stream != null) stream.close();
+ }
+ }
+
+ /** Write the raw data to the atomic file. */
+ private static void writeToAtomicFile(AtomicFile file, byte[] data) throws IOException {
+ // Write the data to the atomic file.
+ FileOutputStream out = null;
+ try {
+ out = file.startWrite();
+ out.write(data);
+ file.finishWrite(out);
+ } catch (IOException e) {
+ if (out != null) {
+ file.failWrite(out);
+ }
+ throw e;
+ }
+ }
+}
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 362ff39..7a6c9aa 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -24,6 +24,8 @@
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@@ -34,6 +36,10 @@
import android.Manifest.permission;
import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
@@ -74,6 +80,8 @@
@RunWith(DevSdkIgnoreRunner.class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
public class ThreadNetworkControllerTest {
+ private static final int JOIN_TIMEOUT_MILLIS = 30 * 1000;
+ private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
private static final int CALLBACK_TIMEOUT_MILLIS = 1000;
private static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
@@ -521,7 +529,7 @@
}
@Test
- public void scheduleMigration_withPrivilegedPermission_success() throws Exception {
+ public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
@@ -548,11 +556,32 @@
controller.scheduleMigration(
pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
-
migrateFuture.get();
- Thread.sleep(35 * 1000);
- assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
- assertThat(getPendingOperationalDataset(controller)).isNull();
+
+ SettableFuture<Boolean> dataset2IsApplied = SettableFuture.create();
+ SettableFuture<Boolean> pendingDatasetIsRemoved = SettableFuture.create();
+ OperationalDatasetCallback datasetCallback =
+ new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeDataset) {
+ if (activeDataset.equals(activeDataset2)) {
+ dataset2IsApplied.set(true);
+ }
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingDataset) {
+ if (pendingDataset == null) {
+ pendingDatasetIsRemoved.set(true);
+ }
+ }
+ };
+ controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+ assertThat(dataset2IsApplied.get()).isTrue();
+ assertThat(pendingDatasetIsRemoved.get()).isTrue();
+ controller.unregisterOperationalDatasetCallback(datasetCallback);
}
}
@@ -629,7 +658,8 @@
}
@Test
- public void scheduleMigration_secondRequestHasLargerTimestamp_success() throws Exception {
+ public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied()
+ throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
@@ -669,11 +699,32 @@
migrateFuture1.get();
controller.scheduleMigration(
pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
-
migrateFuture2.get();
- Thread.sleep(35 * 1000);
- assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
- assertThat(getPendingOperationalDataset(controller)).isNull();
+
+ SettableFuture<Boolean> dataset2IsApplied = SettableFuture.create();
+ SettableFuture<Boolean> pendingDatasetIsRemoved = SettableFuture.create();
+ OperationalDatasetCallback datasetCallback =
+ new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeDataset) {
+ if (activeDataset.equals(activeDataset2)) {
+ dataset2IsApplied.set(true);
+ }
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingDataset) {
+ if (pendingDataset == null) {
+ pendingDatasetIsRemoved.set(true);
+ }
+ }
+ };
+ controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+ assertThat(dataset2IsApplied.get()).isTrue();
+ assertThat(pendingDatasetIsRemoved.get()).isTrue();
+ controller.unregisterOperationalDatasetCallback(datasetCallback);
}
}
@@ -707,4 +758,36 @@
assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
}
}
+
+ @Test
+ public void threadNetworkCallback_deviceAttached_threadNetworkIsAvailable() throws Exception {
+ ThreadNetworkController controller = mManager.getAllThreadNetworkControllers().get(0);
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Network> networkFuture = SettableFuture.create();
+ ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ NetworkRequest networkRequest =
+ new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .build();
+ ConnectivityManager.NetworkCallback networkCallback =
+ new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ networkFuture.set(network);
+ }
+ };
+
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+ runAsShell(
+ permission.ACCESS_NETWORK_STATE,
+ () -> cm.registerNetworkCallback(networkRequest, networkCallback));
+
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+ runAsShell(
+ permission.ACCESS_NETWORK_STATE, () -> assertThat(isAttached(controller)).isTrue());
+ assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNotNull();
+ }
}
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
new file mode 100644
index 0000000..405fb76
--- /dev/null
+++ b/thread/tests/integration/Android.bp
@@ -0,0 +1,55 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+ name: "ThreadNetworkIntegrationTestsDefaults",
+ min_sdk_version: "30",
+ static_libs: [
+ "androidx.test.rules",
+ "guava",
+ "mockito-target-minus-junit4",
+ "net-tests-utils",
+ "net-utils-device-common",
+ "net-utils-device-common-bpf",
+ "testables",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ ],
+}
+
+android_test {
+ name: "ThreadNetworkIntegrationTests",
+ platform_apis: true,
+ manifest: "AndroidManifest.xml",
+ defaults: [
+ "framework-connectivity-test-defaults",
+ "ThreadNetworkIntegrationTestsDefaults"
+ ],
+ test_suites: [
+ "general-tests",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ compile_multilib: "both",
+}
diff --git a/thread/tests/integration/AndroidManifest.xml b/thread/tests/integration/AndroidManifest.xml
new file mode 100644
index 0000000..a049184
--- /dev/null
+++ b/thread/tests/integration/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.thread.tests.integration">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <!-- The test need CHANGE_NETWORK_STATE permission to use requestNetwork API to setup test
+ network. Since R shell application don't have such permission, grant permission to the test
+ here. TODO: Remove CHANGE_NETWORK_STATE permission here and use adopt shell permission to
+ obtain CHANGE_NETWORK_STATE for testing once R device is no longer supported. -->
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
+ <uses-permission android:name="android.permission.NETWORK_SETTINGS"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.thread.tests.integration"
+ android:label="Thread integration tests">
+ </instrumentation>
+</manifest>
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
new file mode 100644
index 0000000..25f5bd3
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.thread.IntegrationTestUtils.isExpectedIcmpv6Packet;
+import static android.net.thread.IntegrationTestUtils.isSimulatedThreadRadioSupported;
+import static android.net.thread.IntegrationTestUtils.newPacketReader;
+import static android.net.thread.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.IntegrationTestUtils.waitFor;
+import static android.net.thread.IntegrationTestUtils.waitForStateAnyOf;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.TapPacketReader;
+import com.android.testutils.TestNetworkTracker;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** Integration test cases for Thread Border Routing feature. */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class BorderRoutingTest {
+ private static final String TAG = BorderRoutingTest.class.getSimpleName();
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkManager mThreadNetworkManager =
+ mContext.getSystemService(ThreadNetworkManager.class);
+ private ThreadNetworkController mThreadNetworkController;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private TestNetworkTracker mInfraNetworkTracker;
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+ private static final byte[] DEFAULT_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+ private static final ActiveOperationalDataset DEFAULT_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+ @Before
+ public void setUp() throws Exception {
+ mHandlerThread = new HandlerThread(getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ var threadControllers = mThreadNetworkManager.getAllThreadNetworkControllers();
+ assertEquals(threadControllers.size(), 1);
+ mThreadNetworkController = threadControllers.get(0);
+ mInfraNetworkTracker =
+ runAsShell(
+ MANAGE_TEST_NETWORKS,
+ () ->
+ initTestNetwork(
+ mContext, new LinkProperties(), 5000 /* timeoutMs */));
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ NETWORK_SETTINGS,
+ () -> {
+ CountDownLatch latch = new CountDownLatch(1);
+ mThreadNetworkController.setTestNetworkAsUpstream(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(),
+ MoreExecutors.directExecutor(),
+ v -> {
+ latch.countDown();
+ });
+ latch.await();
+ });
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ NETWORK_SETTINGS,
+ () -> {
+ CountDownLatch latch = new CountDownLatch(2);
+ mThreadNetworkController.setTestNetworkAsUpstream(
+ null, MoreExecutors.directExecutor(), v -> latch.countDown());
+ mThreadNetworkController.leave(
+ MoreExecutors.directExecutor(), v -> latch.countDown());
+ latch.await(10, TimeUnit.SECONDS);
+ });
+ runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+
+ @Test
+ public void unicastRouting_infraDevicePingTheadDeviceOmr_replyReceived() throws Exception {
+ assumeTrue(isSimulatedThreadRadioSupported());
+
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ // BR forms a network.
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mThreadNetworkController.join(
+ DEFAULT_DATASET, MoreExecutors.directExecutor(), result -> {});
+ });
+ waitForStateAnyOf(
+ mThreadNetworkController, List.of(DEVICE_ROLE_LEADER), 30 /* timeoutSeconds */);
+
+ // Creates a Full Thread Device (FTD) and lets it join the network.
+ FullThreadDevice ftd = new FullThreadDevice(5 /* node ID */);
+ ftd.factoryReset();
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), 10 /* timeoutSeconds */);
+ waitFor(() -> ftd.getOmrAddress() != null, 60 /* timeoutSeconds */);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+ assertNotNull(ftdOmr);
+
+ // Creates a infra network device.
+ TapPacketReader infraNetworkReader =
+ newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ InfraNetworkDevice infraDevice =
+ new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), infraNetworkReader);
+ infraDevice.runSlaac(60 /* timeoutSeconds */);
+ assertNotNull(infraDevice.ipv6Addr);
+
+ // Infra device sends an echo request to FTD's OMR.
+ infraDevice.sendEchoRequest(ftdOmr);
+
+ // Infra device receives an echo reply sent by FTD.
+ assertNotNull(
+ readPacketFrom(
+ infraNetworkReader,
+ p -> isExpectedIcmpv6Packet(p, ICMPV6_ECHO_REPLY_TYPE)));
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
new file mode 100644
index 0000000..01638f3
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread;
+
+import static android.net.thread.IntegrationTestUtils.waitFor;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import static org.junit.Assert.fail;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.Inet6Address;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A class that launches and controls a simulation Full Thread Device (FTD).
+ *
+ * <p>This class launches an `ot-cli-ftd` process and communicates with it via command line input
+ * and output. See <a
+ * href="https://github.com/openthread/openthread/blob/main/src/cli/README.md">this page</a> for
+ * available commands.
+ */
+public final class FullThreadDevice {
+ private final Process mProcess;
+ private final BufferedReader mReader;
+ private final BufferedWriter mWriter;
+
+ private ActiveOperationalDataset mActiveOperationalDataset;
+
+ /**
+ * Constructs a {@link FullThreadDevice} for the given node ID.
+ *
+ * <p>It launches an `ot-cli-ftd` process using the given node ID. The node ID is an integer in
+ * range [1, OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE]. `OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE`
+ * is defined in `external/openthread/examples/platforms/simulation/platform-config.h`.
+ *
+ * @param nodeId the node ID for the simulation Full Thread Device.
+ * @throws IllegalStateException the node ID is already occupied by another simulation Thread
+ * device.
+ */
+ public FullThreadDevice(int nodeId) {
+ try {
+ mProcess = Runtime.getRuntime().exec("/system/bin/ot-cli-ftd " + nodeId);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to start ot-cli-ftd (id=" + nodeId + ")", e);
+ }
+ mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
+ mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
+ mActiveOperationalDataset = null;
+ }
+
+ /**
+ * Returns an OMR (Off-Mesh-Routable) address on this device if any.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the first
+ * address which is neither link-local nor mesh-local.
+ */
+ public Inet6Address getOmrAddress() {
+ List<String> addresses = executeCommand("ipaddr");
+ IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
+ for (String address : addresses) {
+ if (address.startsWith("fe80:")) {
+ continue;
+ }
+ Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
+ if (!meshLocalPrefix.contains(addr)) {
+ return addr;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Joins the Thread network using the given {@link ActiveOperationalDataset}.
+ *
+ * @param dataset the Active Operational Dataset
+ */
+ public void joinNetwork(ActiveOperationalDataset dataset) {
+ mActiveOperationalDataset = dataset;
+ executeCommand("dataset set active " + base16().lowerCase().encode(dataset.toThreadTlvs()));
+ executeCommand("ifconfig up");
+ executeCommand("thread start");
+ }
+
+ /** Stops the Thread network radio. */
+ public void stopThreadRadio() {
+ executeCommand("thread stop");
+ executeCommand("ifconfig down");
+ }
+
+ /**
+ * Waits for the Thread device to enter the any state of the given {@link List<String>}.
+ *
+ * @param states the list of states to wait for. Valid states are "disabled", "detached",
+ * "child", "router" and "leader".
+ * @param timeoutSeconds the number of seconds to wait for.
+ */
+ public void waitForStateAnyOf(List<String> states, int timeoutSeconds) throws TimeoutException {
+ waitFor(() -> states.contains(getState()), timeoutSeconds);
+ }
+
+ /**
+ * Gets the state of the Thread device.
+ *
+ * @return a string representing the state.
+ */
+ public String getState() {
+ return executeCommand("state").get(0);
+ }
+
+ /** Runs the "factoryreset" command on the device. */
+ public void factoryReset() {
+ try {
+ mWriter.write("factoryreset\n");
+ mWriter.flush();
+ // fill the input buffer to avoid truncating next command
+ for (int i = 0; i < 1000; ++i) {
+ mWriter.write("\n");
+ }
+ mWriter.flush();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to run factoryreset on ot-cli-ftd", e);
+ }
+ }
+
+ private List<String> executeCommand(String command) {
+ try {
+ mWriter.write(command + "\n");
+ mWriter.flush();
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Failed to write the command " + command + " to ot-cli-ftd", e);
+ }
+ try {
+ return readUntilDone();
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Failed to read the ot-cli-ftd output of command: " + command, e);
+ }
+ }
+
+ private List<String> readUntilDone() throws IOException {
+ ArrayList<String> result = new ArrayList<>();
+ String line;
+ while ((line = mReader.readLine()) != null) {
+ if (line.equals("Done")) {
+ break;
+ }
+ if (line.startsWith("Error:")) {
+ fail("ot-cli-ftd reported an error: " + line);
+ }
+ if (!line.startsWith("> ")) {
+ result.add(line);
+ }
+ }
+ return result;
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
new file mode 100644
index 0000000..43a800d
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread;
+
+import static android.net.thread.IntegrationTestUtils.getRaPios;
+import static android.net.thread.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.IntegrationTestUtils.waitFor;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+
+import android.net.InetAddresses;
+import android.net.MacAddress;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.testutils.TapPacketReader;
+
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A class that simulates a device on the infrastructure network.
+ *
+ * <p>This class directly interacts with the TUN interface of the test network to pretend there's a
+ * device on the infrastructure network.
+ */
+public final class InfraNetworkDevice {
+ // The MAC address of this device.
+ public final MacAddress macAddr;
+ // The packet reader of the TUN interface of the test network.
+ public final TapPacketReader packetReader;
+ // The IPv6 address generated by SLAAC for the device.
+ public Inet6Address ipv6Addr;
+
+ /**
+ * Constructs an InfraNetworkDevice with the given {@link MAC address} and {@link
+ * TapPacketReader}.
+ *
+ * @param macAddr the MAC address of the device
+ * @param packetReader the packet reader of the TUN interface of the test network.
+ */
+ public InfraNetworkDevice(MacAddress macAddr, TapPacketReader packetReader) {
+ this.macAddr = macAddr;
+ this.packetReader = packetReader;
+ }
+
+ /**
+ * Sends an ICMPv6 echo request message to the given {@link Inet6Address}.
+ *
+ * @param dstAddr the destination address of the packet.
+ * @throws IOException when it fails to send the packet.
+ */
+ public void sendEchoRequest(Inet6Address dstAddr) throws IOException {
+ ByteBuffer icmp6Packet = Ipv6Utils.buildEchoRequestPacket(ipv6Addr, dstAddr);
+ packetReader.sendResponse(icmp6Packet);
+ }
+
+ /**
+ * Sends an ICMPv6 Router Solicitation (RS) message to all routers on the network.
+ *
+ * @throws IOException when it fails to send the packet.
+ */
+ public void sendRsPacket() throws IOException {
+ ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, macAddr);
+ ByteBuffer rs =
+ Ipv6Utils.buildRsPacket(
+ (Inet6Address) InetAddresses.parseNumericAddress("fe80::1"),
+ IPV6_ADDR_ALL_ROUTERS_MULTICAST,
+ slla);
+ packetReader.sendResponse(rs);
+ }
+
+ /**
+ * Runs SLAAC to generate an IPv6 address for the device.
+ *
+ * <p>The devices sends an RS message, processes the received RA messages and generates an IPv6
+ * address if there's any available Prefix Information Option (PIO). For now it only generates
+ * one address in total and doesn't track the expiration.
+ *
+ * @param timeoutSeconds the number of seconds to wait for.
+ * @throws TimeoutException when the device fails to generate a SLAAC address in given timeout.
+ */
+ public void runSlaac(int timeoutSeconds) throws TimeoutException {
+ waitFor(() -> (ipv6Addr = runSlaac()) != null, timeoutSeconds, 5 /* intervalSeconds */);
+ }
+
+ private Inet6Address runSlaac() {
+ try {
+ sendRsPacket();
+
+ final byte[] raPacket = readPacketFrom(packetReader, p -> !getRaPios(p).isEmpty());
+
+ final List<PrefixInformationOption> options = getRaPios(raPacket);
+
+ for (PrefixInformationOption pio : options) {
+ if (pio.validLifetime > 0 && pio.preferredLifetime > 0) {
+ final byte[] addressBytes = pio.prefix;
+ addressBytes[addressBytes.length - 1] = (byte) (new Random()).nextInt();
+ addressBytes[addressBytes.length - 2] = (byte) (new Random()).nextInt();
+ return (Inet6Address) InetAddress.getByAddress(addressBytes);
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to generate an address by SLAAC", e);
+ }
+ return null;
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
new file mode 100644
index 0000000..c465d57
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import android.net.TestNetworkInterface;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.testutils.HandlerUtils;
+import com.android.testutils.TapPacketReader;
+
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.FileDescriptor;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/** Static utility methods relating to Thread integration tests. */
+public final class IntegrationTestUtils {
+ private IntegrationTestUtils() {}
+
+ /** Returns whether the device supports simulated Thread radio. */
+ public static boolean isSimulatedThreadRadioSupported() {
+ // The integration test uses SIMULATION Thread radio so that it only supports CuttleFish.
+ return SystemProperties.get("ro.product.model").startsWith("Cuttlefish");
+ }
+
+ /**
+ * Waits for the given {@link Supplier} to be true until given timeout.
+ *
+ * <p>It checks the condition once every second.
+ *
+ * @param condition the condition to check.
+ * @param timeoutSeconds the number of seconds to wait for.
+ * @throws TimeoutException if the condition is not met after the timeout.
+ */
+ public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds)
+ throws TimeoutException {
+ waitFor(condition, timeoutSeconds, 1);
+ }
+
+ /**
+ * Waits for the given {@link Supplier} to be true until given timeout.
+ *
+ * <p>It checks the condition once every {@code intervalSeconds}.
+ *
+ * @param condition the condition to check.
+ * @param timeoutSeconds the number of seconds to wait for.
+ * @param intervalSeconds the period to check the {@code condition}.
+ * @throws TimeoutException if the condition is still not met when the timeout expires.
+ */
+ public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds, int intervalSeconds)
+ throws TimeoutException {
+ for (int i = 0; i < timeoutSeconds; i += intervalSeconds) {
+ if (condition.get()) {
+ return;
+ }
+ SystemClock.sleep(intervalSeconds * 1000L);
+ }
+ if (condition.get()) {
+ return;
+ }
+ throw new TimeoutException(
+ String.format(
+ "The condition failed to become true in %d seconds.", timeoutSeconds));
+ }
+
+ /**
+ * Creates a {@link TapPacketReader} given the {@link TestNetworkInterface} and {@link Handler}.
+ *
+ * @param testNetworkInterface the TUN interface of the test network.
+ * @param handler the handler to process the packets.
+ * @return the {@link TapPacketReader}.
+ */
+ public static TapPacketReader newPacketReader(
+ TestNetworkInterface testNetworkInterface, Handler handler) {
+ FileDescriptor fd = testNetworkInterface.getFileDescriptor().getFileDescriptor();
+ final TapPacketReader reader =
+ new TapPacketReader(handler, fd, testNetworkInterface.getMtu());
+ handler.post(() -> reader.start());
+ HandlerUtils.waitForIdle(handler, 5000 /* timeout in milliseconds */);
+ return reader;
+ }
+
+ /**
+ * Waits for the Thread module to enter any state of the given {@code deviceRoles}.
+ *
+ * @param controller the {@link ThreadNetworkController}.
+ * @param deviceRoles the desired device roles. See also {@link
+ * ThreadNetworkController.DeviceRole}.
+ * @param timeoutSeconds the number of seconds ot wait for.
+ * @return the {@link ThreadNetworkController.DeviceRole} after waiting.
+ * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
+ * expires.
+ */
+ public static int waitForStateAnyOf(
+ ThreadNetworkController controller, List<Integer> deviceRoles, int timeoutSeconds)
+ throws TimeoutException {
+ SettableFuture<Integer> future = SettableFuture.create();
+ ThreadNetworkController.StateCallback callback =
+ newRole -> {
+ if (deviceRoles.contains(newRole)) {
+ future.set(newRole);
+ }
+ };
+ controller.registerStateCallback(directExecutor(), callback);
+ try {
+ int role = future.get(timeoutSeconds, TimeUnit.SECONDS);
+ controller.unregisterStateCallback(callback);
+ return role;
+ } catch (InterruptedException | ExecutionException e) {
+ throw new TimeoutException(
+ String.format(
+ "The device didn't become an expected role in %d seconds.",
+ timeoutSeconds));
+ }
+ }
+
+ /**
+ * Reads a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
+ *
+ * @param packetReader a TUN packet reader.
+ * @param filter the filter to be applied on the packet.
+ * @return the first IPv6 packet that satisfies the {@code filter}. If it has waited for more
+ * than 3000ms to read the next packet, the method will return null.
+ */
+ public static byte[] readPacketFrom(TapPacketReader packetReader, Predicate<byte[]> filter) {
+ byte[] packet;
+ while ((packet = packetReader.poll(3000 /* timeoutMs */)) != null) {
+ if (filter.test(packet)) return packet;
+ }
+ return null;
+ }
+
+ /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
+ public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
+ return false;
+ }
+ return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
+ /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
+ public static List<PrefixInformationOption> getRaPios(byte[] raMsg) {
+ final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
+
+ if (raMsg == null) {
+ return pioList;
+ }
+
+ final ByteBuffer buf = ByteBuffer.wrap(raMsg);
+ final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buf);
+ if (ipv6Header.nextHeader != (byte) IPPROTO_ICMPV6) {
+ return pioList;
+ }
+
+ final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
+ if (icmpv6Header.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) {
+ return pioList;
+ }
+
+ Struct.parse(RaHeader.class, buf);
+ while (buf.position() < raMsg.length) {
+ final int currentPos = buf.position();
+ final int type = Byte.toUnsignedInt(buf.get());
+ final int length = Byte.toUnsignedInt(buf.get());
+ if (type == ICMPV6_ND_OPTION_PIO) {
+ final ByteBuffer pioBuf =
+ ByteBuffer.wrap(
+ buf.array(),
+ currentPos,
+ Struct.getSize(PrefixInformationOption.class));
+ final PrefixInformationOption pio =
+ Struct.parse(PrefixInformationOption.class, pioBuf);
+ pioList.add(pio);
+
+ // Move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+ } else {
+ // The length is in units of 8 octets.
+ buf.position(currentPos + (length * 8));
+ }
+ }
+ return pioList;
+ }
+}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 8092693..291475e 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -31,20 +31,35 @@
"general-tests",
],
static_libs: [
- "androidx.test.ext.junit",
- "compatibility-device-util-axt",
+ "frameworks-base-testutils",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
+ "framework-location.stubs.module_lib",
"guava",
"guava-android-testlib",
- "mockito-target-minus-junit4",
+ "mockito-target-extended-minus-junit4",
"net-tests-utils",
+ "ot-daemon-aidl-java",
+ "ot-daemon-testing",
+ "service-connectivity-pre-jarjar",
+ "service-thread-pre-jarjar",
"truth",
+ "service-thread-pre-jarjar",
],
libs: [
"android.test.base",
"android.test.runner",
+ "ServiceConnectivityResources",
+ "framework-wifi",
],
+ jni_libs: [
+ "libservice-thread-jni",
+
+ // these are needed for Extended Mockito
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+ jni_uses_platform_apis: true,
jarjar_rules: ":connectivity-jarjar-rules",
// Test coverage system runs on different devices. Need to
// compile for all architectures.
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index 597c6a8..26813c1 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -30,5 +30,8 @@
<option name="hidden-api-checks" value="false"/>
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
+ <!-- Ignores tests introduced by frameworks-base-testutils -->
+ <option name="exclude-filter" value="android.os.test.TestLooperTest"/>
+ <option name="exclude-filter" value="com.android.test.filters.SelectTestTests"/>
</test>
</configuration>
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
index 7284968..e92dcb9 100644
--- a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -33,12 +33,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.security.SecureRandom;
-import java.util.Random;
-
/** Unit tests for {@link ActiveOperationalDataset}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -62,9 +58,6 @@
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
- @Mock private Random mockRandom;
- @Mock private SecureRandom mockSecureRandom;
-
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index 2f120b2..75eb043 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -28,11 +28,6 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
-import android.net.thread.IActiveOperationalDatasetReceiver;
-import android.net.thread.IOperationReceiver;
-import android.net.thread.IOperationalDatasetCallback;
-import android.net.thread.IStateCallback;
-import android.net.thread.IThreadNetworkController;
import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
import android.net.thread.ThreadNetworkController.StateCallback;
import android.os.Binder;
@@ -111,6 +106,11 @@
return (IOperationReceiver) invocation.getArguments()[1];
}
+ private static IOperationReceiver getSetTestNetworkAsUpstreamReceiver(
+ InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
private static IActiveOperationalDatasetReceiver getCreateDatasetReceiver(
InvocationOnMock invocation) {
return (IActiveOperationalDatasetReceiver) invocation.getArguments()[1];
@@ -359,4 +359,27 @@
assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
}
+
+ @Test
+ public void setTestNetworkAsUpstream_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+
+ AtomicInteger callbackUid = new AtomicInteger(0);
+
+ doAnswer(
+ invoke -> {
+ getSetTestNetworkAsUpstreamReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .setTestNetworkAsUpstream(anyString(), any(IOperationReceiver.class));
+ mController.setTestNetworkAsUpstream(
+ null, Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+ mController.setTestNetworkAsUpstream(
+ new String("test0"), Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+
+ assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(callbackUid.get()).isEqualTo(Process.myUid());
+ }
}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
new file mode 100644
index 0000000..11aabb8
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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 static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.PersistableBundle;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.AtomicFile;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+
+/** Unit tests for {@link ThreadPersistentSettings}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadPersistentSettingsTest {
+ @Mock private AtomicFile mAtomicFile;
+
+ private ThreadPersistentSettings mThreadPersistentSetting;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ FileOutputStream fos = mock(FileOutputStream.class);
+ when(mAtomicFile.startWrite()).thenReturn(fos);
+ mThreadPersistentSetting = new ThreadPersistentSettings(mAtomicFile);
+ }
+
+ /** Called after each test */
+ @After
+ public void tearDown() {
+ validateMockitoUsage();
+ }
+
+ @Test
+ public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
+ mThreadPersistentSetting.put(THREAD_ENABLED.key, true);
+
+ assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isTrue();
+ // Confirm that file writes have been triggered.
+ verify(mAtomicFile).startWrite();
+ verify(mAtomicFile).finishWrite(any());
+ }
+
+ @Test
+ public void put_ThreadFeatureEnabledFalse_returnsFalse() throws Exception {
+ mThreadPersistentSetting.put(THREAD_ENABLED.key, false);
+
+ assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse();
+ // Confirm that file writes have been triggered.
+ verify(mAtomicFile).startWrite();
+ verify(mAtomicFile).finishWrite(any());
+ }
+
+ @Test
+ public void initialize_readsFromFile() throws Exception {
+ byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
+ setupAtomicFileMockForRead(data);
+
+ // Trigger file read.
+ mThreadPersistentSetting.initialize();
+
+ assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse();
+ verify(mAtomicFile, never()).startWrite();
+ }
+
+ private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
+ PersistableBundle bundle = new PersistableBundle();
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ bundle.putBoolean(key, value);
+ bundle.writeToStream(outputStream);
+ return outputStream.toByteArray();
+ }
+
+ private void setupAtomicFileMockForRead(byte[] dataToRead) throws Exception {
+ FileInputStream is = mock(FileInputStream.class);
+ when(mAtomicFile.openRead()).thenReturn(is);
+ when(is.available()).thenReturn(dataToRead.length).thenReturn(0);
+ doAnswer(
+ invocation -> {
+ byte[] data = invocation.getArgument(0);
+ int pos = invocation.getArgument(1);
+ if (pos == dataToRead.length) return 0; // read complete.
+ System.arraycopy(dataToRead, 0, data, 0, dataToRead.length);
+ return dataToRead.length;
+ })
+ .when(is)
+ .read(any(), anyInt(), anyInt());
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/BinderUtil.java b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java
new file mode 100644
index 0000000..3614bce
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import android.os.Binder;
+
+/** Utilities for faking the calling uid in Binder. */
+public class BinderUtil {
+ /**
+ * Fake the calling uid in Binder.
+ *
+ * @param uid the calling uid that Binder should return from now on
+ */
+ public static void setUid(int uid) {
+ Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid());
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
new file mode 100644
index 0000000..44a8ab7
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+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.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkAgent;
+import android.net.NetworkProvider;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IOperationReceiver;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.thread.openthread.testing.FakeOtDaemon;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link ThreadNetworkControllerService}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkControllerServiceTest {
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+ private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
+
+ @Mock private ConnectivityManager mMockConnectivityManager;
+ @Mock private NetworkAgent mMockNetworkAgent;
+ @Mock private TunInterfaceController mMockTunIfController;
+ @Mock private ParcelFileDescriptor mMockTunFd;
+ @Mock private InfraInterfaceController mMockInfraIfController;
+ private Context mContext;
+ private TestLooper mTestLooper;
+ private FakeOtDaemon mFakeOtDaemon;
+ private ThreadNetworkControllerService mService;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = ApplicationProvider.getApplicationContext();
+ mTestLooper = new TestLooper();
+ final Handler handler = new Handler(mTestLooper.getLooper());
+ NetworkProvider networkProvider =
+ new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
+
+ mFakeOtDaemon = new FakeOtDaemon(handler);
+
+ when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
+
+ mService =
+ new ThreadNetworkControllerService(
+ ApplicationProvider.getApplicationContext(),
+ handler,
+ networkProvider,
+ () -> mFakeOtDaemon,
+ mMockConnectivityManager,
+ mMockTunIfController,
+ mMockInfraIfController);
+ mService.setTestNetworkAgent(mMockNetworkAgent);
+ }
+
+ @Test
+ public void initialize_tunInterfaceSetToOtDaemon() throws Exception {
+ when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ verify(mMockTunIfController, times(1)).createTunInterface();
+ assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd);
+ }
+
+ @Test
+ public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ mFakeOtDaemon.setJoinException(new RemoteException("ot-daemon join() throws"));
+
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onSuccess();
+ verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
+ }
+
+ @Test
+ public void join_succeed_threadNetworkRegistered() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
+ // Here needs to call Testlooper#dispatchAll twices because TestLooper#moveTimeForward
+ // operates on only currently enqueued messages but the delayed message is posted from
+ // another Handler task.
+ mTestLooper.dispatchAll();
+ mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ verify(mMockNetworkAgent, times(1)).register();
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
new file mode 100644
index 0000000..17cdd01
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+
+import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyDouble;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.thread.IOperationReceiver;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+/** Unit tests for {@link ThreadNetworkCountryCode}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkCountryCodeTest {
+ private static final String TEST_COUNTRY_CODE_US = "US";
+ private static final String TEST_COUNTRY_CODE_CN = "CN";
+ private static final int TEST_SIM_SLOT_INDEX_0 = 0;
+ private static final int TEST_SIM_SLOT_INDEX_1 = 1;
+
+ @Mock Context mContext;
+ @Mock LocationManager mLocationManager;
+ @Mock Geocoder mGeocoder;
+ @Mock ThreadNetworkControllerService mThreadNetworkControllerService;
+ @Mock PackageManager mPackageManager;
+ @Mock Location mLocation;
+ @Mock Resources mResources;
+ @Mock ConnectivityResources mConnectivityResources;
+ @Mock WifiManager mWifiManager;
+ @Mock SubscriptionManager mSubscriptionManager;
+ @Mock TelephonyManager mTelephonyManager;
+ @Mock List<SubscriptionInfo> mSubscriptionInfoList;
+ @Mock SubscriptionInfo mSubscriptionInfo0;
+ @Mock SubscriptionInfo mSubscriptionInfo1;
+
+ private ThreadNetworkCountryCode mThreadNetworkCountryCode;
+ private boolean mErrorSetCountryCode;
+
+ @Captor private ArgumentCaptor<LocationListener> mLocationListenerCaptor;
+ @Captor private ArgumentCaptor<Geocoder.GeocodeListener> mGeocodeListenerCaptor;
+ @Captor private ArgumentCaptor<IOperationReceiver> mOperationReceiverCaptor;
+ @Captor private ArgumentCaptor<ActiveCountryCodeChangedCallback> mWifiCountryCodeReceiverCaptor;
+ @Captor private ArgumentCaptor<BroadcastReceiver> mTelephonyCountryCodeReceiverCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mConnectivityResources.get()).thenReturn(mResources);
+ when(mResources.getBoolean(anyInt())).thenReturn(true);
+
+ when(mSubscriptionManager.getActiveSubscriptionInfoList())
+ .thenReturn(mSubscriptionInfoList);
+ Iterator<SubscriptionInfo> iteratorMock = mock(Iterator.class);
+ when(mSubscriptionInfoList.size()).thenReturn(2);
+ when(mSubscriptionInfoList.iterator()).thenReturn(iteratorMock);
+ when(iteratorMock.hasNext()).thenReturn(true).thenReturn(true).thenReturn(false);
+ when(iteratorMock.next()).thenReturn(mSubscriptionInfo0).thenReturn(mSubscriptionInfo1);
+ when(mSubscriptionInfo0.getSimSlotIndex()).thenReturn(TEST_SIM_SLOT_INDEX_0);
+ when(mSubscriptionInfo1.getSimSlotIndex()).thenReturn(TEST_SIM_SLOT_INDEX_1);
+
+ when(mLocation.getLatitude()).thenReturn(0.0);
+ when(mLocation.getLongitude()).thenReturn(0.0);
+
+ Answer setCountryCodeCallback =
+ invocation -> {
+ Object[] args = invocation.getArguments();
+ IOperationReceiver cb = (IOperationReceiver) args[1];
+
+ if (mErrorSetCountryCode) {
+ cb.onError(ERROR_INTERNAL_ERROR, new String("Invalid country code"));
+ } else {
+ cb.onSuccess();
+ }
+ return new Object();
+ };
+
+ doAnswer(setCountryCodeCallback)
+ .when(mThreadNetworkControllerService)
+ .setCountryCode(any(), any(IOperationReceiver.class));
+
+ mThreadNetworkCountryCode =
+ new ThreadNetworkCountryCode(
+ mLocationManager,
+ mThreadNetworkControllerService,
+ mGeocoder,
+ mConnectivityResources,
+ mWifiManager,
+ mContext,
+ mTelephonyManager,
+ mSubscriptionManager);
+ }
+
+ private static Address newAddress(String countryCode) {
+ Address address = new Address(Locale.ROOT);
+ address.setCountryCode(countryCode);
+ return address;
+ }
+
+ @Test
+ public void initialize_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void initialize_locationUseIsDisabled_locationFunctionIsNotCalled() {
+ when(mResources.getBoolean(R.bool.config_thread_location_use_for_country_code_enabled))
+ .thenReturn(false);
+
+ mThreadNetworkCountryCode.initialize();
+
+ verifyNoMoreInteractions(mGeocoder);
+ verifyNoMoreInteractions(mLocationManager);
+ }
+
+ @Test
+ public void locationCountryCode_locationChanged_locationCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+ mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void wifiCountryCode_bothWifiAndLocationAreAvailable_wifiCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+
+ Address mockAddress = mock(Address.class);
+ when(mockAddress.getCountryCode()).thenReturn(TEST_COUNTRY_CODE_US);
+ List<Address> addresses = List.of(mockAddress);
+ mGeocodeListenerCaptor.getValue().onGeocode(addresses);
+
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_CN);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void wifiCountryCode_wifiCountryCodeIsActive_wifiCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_US);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void wifiCountryCode_wifiCountryCodeIsInactive_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_US);
+
+ mWifiCountryCodeReceiverCaptor.getValue().onCountryCodeInactive();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode())
+ .isEqualTo(ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void telephonyCountryCode_bothTelephonyAndLocationAvailable_telephonyCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+ mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void telephonyCountryCode_locationIsAvailable_lastKnownTelephonyCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+ mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, "")
+ .putExtra(
+ TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY,
+ TEST_COUNTRY_CODE_US)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void telephonyCountryCode_lastKnownCountryCodeAvailable_telephonyCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent0 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, "")
+ .putExtra(
+ TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY,
+ TEST_COUNTRY_CODE_US)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent0);
+
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent1 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_1);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent1);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void telephonyCountryCode_multipleSims_firstSimIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent1 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_1);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent1);
+
+ Intent intent0 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent0);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void updateCountryCode_noForceUpdateDefaultCountryCode_noCountryCodeIsUpdated() {
+ mThreadNetworkCountryCode.initialize();
+ clearInvocations(mThreadNetworkControllerService);
+
+ mThreadNetworkCountryCode.updateCountryCode(false /* forceUpdate */);
+
+ verify(mThreadNetworkControllerService, never()).setCountryCode(any(), any());
+ }
+
+ @Test
+ public void updateCountryCode_forceUpdateDefaultCountryCode_countryCodeIsUpdated() {
+ mThreadNetworkCountryCode.initialize();
+ clearInvocations(mThreadNetworkControllerService);
+
+ mThreadNetworkCountryCode.updateCountryCode(true /* forceUpdate */);
+
+ verify(mThreadNetworkControllerService)
+ .setCountryCode(eq(DEFAULT_COUNTRY_CODE), mOperationReceiverCaptor.capture());
+ }
+
+ @Test
+ public void setOverrideCountryCode_defaultCountryCodeAvailable_overrideCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void clearOverrideCountryCode_defaultCountryCodeAvailable_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ mThreadNetworkCountryCode.clearOverrideCountryCode();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void setCountryCodeFailed_defaultCountryCodeAvailable_countryCodeIsNotUpdated() {
+ mThreadNetworkCountryCode.initialize();
+
+ mErrorSetCountryCode = true;
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ verify(mThreadNetworkControllerService)
+ .setCountryCode(eq(TEST_COUNTRY_CODE_CN), mOperationReceiverCaptor.capture());
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
new file mode 100644
index 0000000..c7e0eca
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Binder;
+import android.os.Process;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/** Unit tests for {@link ThreadNetworkShellCommand}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkShellCommandTest {
+ private static final String TAG = "ThreadNetworkShellCommandTTest";
+ @Mock ThreadNetworkService mThreadNetworkService;
+ @Mock ThreadNetworkCountryCode mThreadNetworkCountryCode;
+ @Mock PrintWriter mErrorWriter;
+ @Mock PrintWriter mOutputWriter;
+
+ ThreadNetworkShellCommand mThreadNetworkShellCommand;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mThreadNetworkShellCommand = new ThreadNetworkShellCommand(mThreadNetworkCountryCode);
+ mThreadNetworkShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ validateMockitoUsage();
+ }
+
+ @Test
+ public void getCountryCode_executeInUnrootedShell_allowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+ when(mThreadNetworkCountryCode.getCountryCode()).thenReturn("US");
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"get-country-code"});
+
+ verify(mOutputWriter).println(contains("US"));
+ }
+
+ @Test
+ public void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "enabled", "US"});
+
+ verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(eq("US"));
+ verify(mErrorWriter).println(contains("force-country-code"));
+ }
+
+ @Test
+ public void forceSetCountryCodeEnabled_executeInRootedShell_allowed() {
+ BinderUtil.setUid(Process.ROOT_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "enabled", "US"});
+
+ verify(mThreadNetworkCountryCode).setOverrideCountryCode(eq("US"));
+ }
+
+ @Test
+ public void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "disabled"});
+
+ verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(any());
+ verify(mErrorWriter).println(contains("force-country-code"));
+ }
+
+ @Test
+ public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() {
+ BinderUtil.setUid(Process.ROOT_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "disabled"});
+
+ verify(mThreadNetworkCountryCode).clearOverrideCountryCode();
+ }
+}